diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemProvider.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemProvider.js new file mode 100644 index 000000000..86ce8b8a1 --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemProvider.js @@ -0,0 +1,38 @@ +import React, { createContext, useContext } from 'react'; +import FinancialReportPage from '../FinancialReportPage'; +import { useSalesByItems } from 'hooks/query'; +import { transformFilterFormToQuery } from '../common'; + +const SalesByItemsContext = createContext(); + +function SalesByItemProvider({ query, ...props }) { + const { + data: salesByItems, + isFetching, + isLoading, + refetch, + } = useSalesByItems( + { + ...transformFilterFormToQuery(query), + }, + { + keepPreviousData: true, + }, + ); + + const provider = { + salesByItems, + isFetching, + isLoading, + refetchSheet: refetch, + }; + return ( + + + + ); +} + +const useSalesByItemsContext = () => useContext(SalesByItemsContext); + +export { SalesByItemProvider, useSalesByItemsContext }; diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItems.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItems.js new file mode 100644 index 000000000..871315729 --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItems.js @@ -0,0 +1,89 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import moment from 'moment'; + +import 'style/pages/FinancialStatements/SalesByItems.scss'; + +import { SalesByItemProvider } from './SalesByItemProvider'; +import SalesByItemsActionsBar from './SalesByItemsActionsBar'; +import SalesByItemsHeader from './SalesByItemsHeader'; +import SalesByItemsTable from './SalesByItemsTable'; + +import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; +import { SalesByItemsLoadingBar } from './components'; + +import withSalesByItemsActions from './withSalesByItemsActions'; +import withSettings from 'containers/Settings/withSettings'; + +import { compose } from 'utils'; + +/** + * Sales by items. + */ +function SalesByItems({ + // #withPreferences + organizationName, + + // #withSellsByItemsActions + toggleSalesByItemsFilterDrawer, +}) { + const [filter, setFilter] = useState({ + fromDate: moment().startOf('year').format('YYYY-MM-DD'), + toDate: moment().endOf('year').format('YYYY-MM-DD'), + }); + + // Handle filter form submit. + const handleFilterSubmit = useCallback( + (filter) => { + const parsedFilter = { + ...filter, + fromDate: moment(filter.fromDate).format('YYYY-MM-DD'), + toDate: moment(filter.toDate).format('YYYY-MM-DD'), + }; + setFilter(parsedFilter); + }, + [setFilter], + ); + + // Handle number format form submit. + const handleNumberFormatSubmit = (numberFormat) => { + setFilter({ + ...filter, + numberFormat, + }); + }; + // Hide the filter drawer once the page unmount. + useEffect( + () => () => { + toggleSalesByItemsFilterDrawer(false); + }, + [toggleSalesByItemsFilterDrawer], + ); + + return ( + + + + +
+ +
+ +
+
+
+
+ ); +} + +export default compose( + withSalesByItemsActions, + withSettings(({ organizationSettings }) => ({ + organizationName: organizationSettings.name, + })), +)(SalesByItems); diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsActionsBar.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsActionsBar.js new file mode 100644 index 000000000..e36574469 --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsActionsBar.js @@ -0,0 +1,127 @@ +import React from 'react'; +import { + NavbarGroup, + Button, + Classes, + NavbarDivider, + Popover, + PopoverInteractionKind, + Position, +} from '@blueprintjs/core'; +import classNames from 'classnames'; +import { FormattedMessage as T } from 'react-intl'; + +import { Icon } from 'components'; +import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; +import NumberFormatDropdown from 'components/NumberFormatDropdown'; + +import withSalesByItems from './withSalesByItems'; +import withSalesByItemsActions from './withSalesByItemsActions'; + +import { compose, saveInvoke } from 'utils'; +import { useSalesByItemsContext } from './SalesByItemProvider'; + +function SalesByItemsActionsBar({ + // #withSalesByItems + salesByItemsDrawerFilter, + + // #withSalesByItemsActions + toggleSalesByItemsFilterDrawer, + + // #ownProps + numberFormat, + onNumberFormatSubmit, +}) { + const { refetchSheet, isLoading } = useSalesByItemsContext(); + + // Handle filter toggle click. + const handleFilterToggleClick = () => { + toggleSalesByItemsFilterDrawer(); + }; + // Handle re-calc button click. + const handleRecalculateReport = () => { + refetchSheet(); + }; + + // Handle number format submit. + const handleNumberFormatSubmit = (values) => { + saveInvoke(onNumberFormatSubmit, values); + }; + + return ( + + + + + + + + + ); +} + +export default compose( + withSalesByItems(({ salesByItemsDrawerFilter }) => ({ + salesByItemsDrawerFilter, + })), + withSalesByItemsActions, +)(SalesByItemsHeader); diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeaderGeneralPanel.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeaderGeneralPanel.js new file mode 100644 index 000000000..a31f6cd7e --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeaderGeneralPanel.js @@ -0,0 +1,13 @@ +import React from 'react'; +import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; + +/** + * sells by items - Drawer header - General panel. + */ +export default function SalesByItemsHeaderGeneralPanel() { + return ( +
+ +
+ ); +} diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsTable.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsTable.js new file mode 100644 index 000000000..94611e888 --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsTable.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; + +import FinancialSheet from 'components/FinancialSheet'; +import { DataTable } from 'components'; +import { useSalesByItemsContext } from './SalesByItemProvider'; +import { useSalesByItemsTableColumns } from './components'; + +/** + * Sales by items data table. + */ +export default function SalesByItemsTable({ companyName }) { + const { formatMessage } = useIntl(); + + // Sales by items context. + const { + salesByItems: { tableRows, query }, + isLoading, + } = useSalesByItemsContext(); + + // Sales by items table columns. + const columns = useSalesByItemsTableColumns(); + + const rowClassNames = (row) => { + const { original } = row; + const rowTypes = Array.isArray(original.rowType) + ? original.rowType + : [original.rowType]; + + return { + ...rowTypes.reduce((acc, rowType) => { + acc[`row_type--${rowType}`] = rowType; + return acc; + }, {}), + }; + }; + + return ( + + + + ); +} diff --git a/client/src/containers/FinancialStatements/SalesByItems/components.js b/client/src/containers/FinancialStatements/SalesByItems/components.js new file mode 100644 index 000000000..bbf8deeee --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/components.js @@ -0,0 +1,75 @@ +import React, { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { Button } from '@blueprintjs/core'; +import { getColumnWidth } from 'utils'; +import { If, Icon } from 'components'; +import { CellTextSpan } from 'components/Datatable/Cells'; +import { useSalesByItemsContext } from './SalesByItemProvider'; +import FinancialLoadingBar from '../FinancialLoadingBar'; + +/** + * Retrieve sales by items table columns. + */ +export const useSalesByItemsTableColumns = () => { + const { formatMessage } = useIntl(); + + //sales by items context. + const { + salesByItems: { tableRows }, + } = useSalesByItemsContext(); + + return useMemo( + () => [ + { + Header: formatMessage({ id: 'item_name' }), + accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name), + className: 'name', + width: 180, + textOverview: true, + }, + { + Header: formatMessage({ id: 'sold_quantity' }), + accessor: 'quantity_sold_formatted', + Cell: CellTextSpan, + className: 'quantity_sold', + width: getColumnWidth(tableRows, `quantity_sold_formatted`, { + minWidth: 150, + }), + textOverview: true, + }, + { + Header: formatMessage({ id: 'sold_amount' }), + accessor: 'sold_cost_formatted', + Cell: CellTextSpan, + className: 'sold_cost', + width: getColumnWidth(tableRows, `sold_cost_formatted`, { + minWidth: 150, + }), + textOverview: true, + }, + { + Header: formatMessage({ id: 'average_price' }), + accessor: 'average_sell_price_formatted', + Cell: CellTextSpan, + className: 'average_sell_price', + width: getColumnWidth(tableRows, `average_sell_price_formatted`, { + minWidth: 150, + }), + textOverview: true, + }, + ], + [tableRows, formatMessage], + ); +}; + +/** + * sales by items progress loading bar. + */ +export function SalesByItemsLoadingBar() { + const { isFetching } = useSalesByItemsContext(); + return ( + + + + ); +} diff --git a/client/src/containers/FinancialStatements/SalesByItems/withSalesByItems.js b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItems.js new file mode 100644 index 000000000..4318b2681 --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItems.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { getSalesByItemsFilterDrawer } from 'store/financialStatement/financialStatements.selectors'; + +export default (mapState) => { + const mapStateToProps = (state, props) => { + const mapped = { + salesByItemsDrawerFilter: getSalesByItemsFilterDrawer(state), + }; + return mapState ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; diff --git a/client/src/containers/FinancialStatements/SalesByItems/withSalesByItemsActions.js b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItemsActions.js new file mode 100644 index 000000000..f0bb9ca76 --- /dev/null +++ b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItemsActions.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux'; +import { toggleSalesByItemsFilterDrawer } from 'store/financialStatement/financialStatements.actions'; + +export const mapDispatchToProps = (dispatch) => ({ + toggleSalesByItemsFilterDrawer: (toggle) => + dispatch(toggleSalesByItemsFilterDrawer(toggle)), +}); + +export default connect(null, mapDispatchToProps); diff --git a/client/src/style/pages/FinancialStatements/PurchasesByItems.scss b/client/src/style/pages/FinancialStatements/PurchasesByItems.scss new file mode 100644 index 000000000..12ab84fcd --- /dev/null +++ b/client/src/style/pages/FinancialStatements/PurchasesByItems.scss @@ -0,0 +1,27 @@ +.financial-sheet { + &--purchases-by-items { + min-width: 800px; + + .financial-sheet__table { + .thead, + .tbody { + .tr .td:not(:first-child), + .tr .th:not(:first-child) { + text-align: right; + } + } + .tbody { + .tr .td { + border-bottom: 0; + padding-top: 0.4rem; + padding-bottom: 0.4rem; + } + .tr.row_type--total .td { + border-top: 1px solid #000; + font-weight: 500; + border-bottom: 3px double #000; + } + } + } + } +} diff --git a/client/src/style/pages/FinancialStatements/SalesByItems.scss b/client/src/style/pages/FinancialStatements/SalesByItems.scss new file mode 100644 index 000000000..fda5840c8 --- /dev/null +++ b/client/src/style/pages/FinancialStatements/SalesByItems.scss @@ -0,0 +1,27 @@ +.financial-sheet { + &--sales-by-items { + min-width: 800px; + + .financial-sheet__table { + .thead, + .tbody { + .tr .td:not(:first-child), + .tr .th:not(:first-child) { + text-align: right; + } + } + .tbody { + .tr .td { + border-bottom: 0; + padding-top: 0.4rem; + padding-bottom: 0.4rem; + } + .tr.row_type--total .td { + border-top: 1px solid #000; + font-weight: 500; + border-bottom: 3px double #000; + } + } + } + } +} \ No newline at end of file