mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat(webapp): dynamic columns of sales by items sheet
This commit is contained in:
@@ -7,8 +7,15 @@ import {
|
|||||||
ITableRow,
|
ITableRow,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { tableRowMapper } from '@/utils';
|
import { tableRowMapper } from '@/utils';
|
||||||
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||||
|
import { FinancialTable } from '../FinancialTable';
|
||||||
|
import { ROW_TYPE } from './constants';
|
||||||
|
|
||||||
export class SalesByItemsTable {
|
export class SalesByItemsTable extends R.compose(
|
||||||
|
FinancialTable,
|
||||||
|
FinancialSheetStructure
|
||||||
|
)(FinancialSheet) {
|
||||||
private readonly data: ISalesByItemsSheetStatement;
|
private readonly data: ISalesByItemsSheetStatement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +23,7 @@ export class SalesByItemsTable {
|
|||||||
* @param {ISalesByItemsSheetStatement} data
|
* @param {ISalesByItemsSheetStatement} data
|
||||||
*/
|
*/
|
||||||
constructor(data: ISalesByItemsSheetStatement) {
|
constructor(data: ISalesByItemsSheetStatement) {
|
||||||
|
super();
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +47,9 @@ export class SalesByItemsTable {
|
|||||||
*/
|
*/
|
||||||
private itemMap = (item: ISalesByItemsItem): ITableRow => {
|
private itemMap = (item: ISalesByItemsItem): ITableRow => {
|
||||||
const columns = this.commonTableAccessors();
|
const columns = this.commonTableAccessors();
|
||||||
const meta = {};
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.ITEM],
|
||||||
|
};
|
||||||
return tableRowMapper(item, columns, meta);
|
return tableRowMapper(item, columns, meta);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,8 +69,9 @@ export class SalesByItemsTable {
|
|||||||
*/
|
*/
|
||||||
private totalMap = (total: ISalesByItemsTotal) => {
|
private totalMap = (total: ISalesByItemsTotal) => {
|
||||||
const columns = this.commonTableAccessors();
|
const columns = this.commonTableAccessors();
|
||||||
const meta = {};
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.TOTAL],
|
||||||
|
};
|
||||||
return tableRowMapper(total, columns, meta);
|
return tableRowMapper(total, columns, meta);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,11 +91,12 @@ export class SalesByItemsTable {
|
|||||||
* @returns {ITableColumn[]}
|
* @returns {ITableColumn[]}
|
||||||
*/
|
*/
|
||||||
public tableColumns(): ITableColumn[] {
|
public tableColumns(): ITableColumn[] {
|
||||||
return [
|
const columns = [
|
||||||
{ key: 'item_name', label: 'Item name' },
|
{ key: 'item_name', label: 'Item name' },
|
||||||
{ key: 'sold_quantity', label: 'Sold quantity' },
|
{ key: 'sold_quantity', label: 'Sold quantity' },
|
||||||
{ key: 'sold_amount', label: 'Sold amount' },
|
{ key: 'sold_amount', label: 'Sold amount' },
|
||||||
{ key: 'average_price', label: 'Average price' },
|
{ key: 'average_price', label: 'Average price' },
|
||||||
];
|
];
|
||||||
|
return R.compose(this.tableColumnsCellIndexing)(columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export enum ROW_TYPE {
|
||||||
|
ITEM = 'ITEM',
|
||||||
|
TOTAL = 'TOTAL',
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import FinancialReportPage from '../FinancialReportPage';
|
import FinancialReportPage from '../FinancialReportPage';
|
||||||
import { useSalesByItems } from '@/hooks/query';
|
import { useSalesByItemsTable } from '@/hooks/query';
|
||||||
import { transformFilterFormToQuery } from '../common';
|
import { transformFilterFormToQuery } from '../common';
|
||||||
|
|
||||||
const SalesByItemsContext = createContext();
|
const SalesByItemsContext = createContext();
|
||||||
@@ -12,7 +12,7 @@ function SalesByItemProvider({ query, ...props }) {
|
|||||||
isFetching,
|
isFetching,
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = useSalesByItems(
|
} = useSalesByItemsTable(
|
||||||
{
|
{
|
||||||
...transformFilterFormToQuery(query),
|
...transformFilterFormToQuery(query),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import withSalesByItemsActions from './withSalesByItemsActions';
|
|||||||
|
|
||||||
import { compose, saveInvoke } from '@/utils';
|
import { compose, saveInvoke } from '@/utils';
|
||||||
import { useSalesByItemsContext } from './SalesByItemProvider';
|
import { useSalesByItemsContext } from './SalesByItemProvider';
|
||||||
|
import { SalesByItemsSheetExportMenu } from './components';
|
||||||
|
|
||||||
function SalesByItemsActionsBar({
|
function SalesByItemsActionsBar({
|
||||||
// #withSalesByItems
|
// #withSalesByItems
|
||||||
@@ -108,11 +109,18 @@ function SalesByItemsActionsBar({
|
|||||||
icon={<Icon icon="print-16" iconSize={16} />}
|
icon={<Icon icon="print-16" iconSize={16} />}
|
||||||
text={<T id={'print'} />}
|
text={<T id={'print'} />}
|
||||||
/>
|
/>
|
||||||
|
<Popover
|
||||||
|
content={<SalesByItemsSheetExportMenu />}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
placement="bottom-start"
|
||||||
|
minimal
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
icon={<Icon icon="file-export-16" iconSize={16} />}
|
||||||
text={<T id={'export'} />}
|
text={<T id={'export'} />}
|
||||||
/>
|
/>
|
||||||
|
</Popover>
|
||||||
</NavbarGroup>
|
</NavbarGroup>
|
||||||
</DashboardActionsBar>
|
</DashboardActionsBar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
import { ReportDataTable, FinancialSheet } from '@/components';
|
import { ReportDataTable, FinancialSheet } from '@/components';
|
||||||
import { useSalesByItemsContext } from './SalesByItemProvider';
|
import { useSalesByItemsContext } from './SalesByItemProvider';
|
||||||
import { useSalesByItemsTableColumns } from './components';
|
import { useSalesByItemsTableColumns } from './dynamicColumns';
|
||||||
import { tableRowTypesToClassnames } from '@/utils';
|
import { tableRowTypesToClassnames } from '@/utils';
|
||||||
import { TableStyle } from '@/constants';
|
import { TableStyle } from '@/constants';
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ import { TableStyle } from '@/constants';
|
|||||||
export default function SalesByItemsTable({ companyName }) {
|
export default function SalesByItemsTable({ companyName }) {
|
||||||
// Sales by items context.
|
// Sales by items context.
|
||||||
const {
|
const {
|
||||||
salesByItems: { tableRows, query },
|
salesByItems: { table, query },
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSalesByItemsContext();
|
} = useSalesByItemsContext();
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export default function SalesByItemsTable({ companyName }) {
|
|||||||
>
|
>
|
||||||
<SalesByItemsDataTable
|
<SalesByItemsDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={tableRows}
|
data={table.rows}
|
||||||
expandable={true}
|
expandable={true}
|
||||||
expandToggleColumn={1}
|
expandToggleColumn={1}
|
||||||
expandColumnSpace={1}
|
expandColumnSpace={1}
|
||||||
@@ -59,7 +59,7 @@ const SalesByItemsDataTable = styled(ReportDataTable)`
|
|||||||
padding-top: 0.4rem;
|
padding-top: 0.4rem;
|
||||||
padding-bottom: 0.4rem;
|
padding-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
.tr.row_type--total .td {
|
.tr.row_type--TOTAL .td {
|
||||||
border-top: 1px solid #bbb;
|
border-top: 1px solid #bbb;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-bottom: 3px double #000;
|
border-bottom: 3px double #000;
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import React, { useMemo } from 'react';
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
import { getColumnWidth } from '@/utils';
|
import { getColumnWidth } from '@/utils';
|
||||||
import { If } from '@/components';
|
import { If, Stack } from '@/components';
|
||||||
import { Align } from '@/constants';
|
import { Align } from '@/constants';
|
||||||
import { CellTextSpan } from '@/components/Datatable/Cells';
|
import { CellTextSpan } from '@/components/Datatable/Cells';
|
||||||
import { useSalesByItemsContext } from './SalesByItemProvider';
|
import { useSalesByItemsContext } from './SalesByItemProvider';
|
||||||
import FinancialLoadingBar from '../FinancialLoadingBar';
|
import FinancialLoadingBar from '../FinancialLoadingBar';
|
||||||
|
import { Menu, MenuItem, ProgressBar, Text } from '@blueprintjs/core';
|
||||||
|
import { useBalanceSheetXlsxExport } from '@/hooks/query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sales by items table columns.
|
* Retrieve sales by items table columns.
|
||||||
@@ -77,3 +78,88 @@ export function SalesByItemsLoadingBar() {
|
|||||||
</If>
|
</If>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sales by items export menu.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export const SalesByItemsSheetExportMenu = () => {
|
||||||
|
const toastKey = useRef(null);
|
||||||
|
const commonToastConfig = {
|
||||||
|
isCloseButtonShown: true,
|
||||||
|
timeout: 2000,
|
||||||
|
};
|
||||||
|
const { query } = useBalanceSheetContext();
|
||||||
|
|
||||||
|
const openProgressToast = (amount: number) => {
|
||||||
|
return (
|
||||||
|
<Stack spacing={8}>
|
||||||
|
<Text>The report has been exported successfully.</Text>
|
||||||
|
<ProgressBar
|
||||||
|
className={classNames('toast-progress', {
|
||||||
|
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
|
||||||
|
})}
|
||||||
|
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
|
||||||
|
value={amount / 100}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the report to xlsx.
|
||||||
|
const { mutateAsync: xlsxExport } = useBalanceSheetXlsxExport(query, {
|
||||||
|
onDownloadProgress: (xlsxExportProgress: number) => {
|
||||||
|
if (!toastKey.current) {
|
||||||
|
toastKey.current = AppToaster.show({
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
AppToaster.show(
|
||||||
|
{
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
},
|
||||||
|
toastKey.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Export the report to csv.
|
||||||
|
const { mutateAsync: csvExport } = useBalanceSheetCsvExport(query, {
|
||||||
|
onDownloadProgress: (xlsxExportProgress: number) => {
|
||||||
|
if (!toastKey.current) {
|
||||||
|
toastKey.current = AppToaster.show({
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
AppToaster.show(
|
||||||
|
{
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
},
|
||||||
|
toastKey.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Handle csv export button click.
|
||||||
|
const handleCsvExportBtnClick = () => {
|
||||||
|
csvExport().then(() => {});
|
||||||
|
};
|
||||||
|
// Handle xlsx export button click.
|
||||||
|
const handleXlsxExportBtnClick = () => {
|
||||||
|
xlsxExport().then(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
text={'XLSX (Microsoft Excel)'}
|
||||||
|
onClick={handleXlsxExportBtnClick}
|
||||||
|
/>
|
||||||
|
<MenuItem text={'CSV'} onClick={handleCsvExportBtnClick} />
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { getColumnWidth } from '@/utils';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Align } from '@/constants';
|
||||||
|
import { useSalesByItemsContext } from './SalesByItemProvider';
|
||||||
|
|
||||||
|
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
|
||||||
|
|
||||||
|
const getReportColWidth = (data, accessor, headerText) => {
|
||||||
|
return getColumnWidth(
|
||||||
|
data,
|
||||||
|
accessor,
|
||||||
|
{ magicSpacing: 10, minWidth: 100 },
|
||||||
|
headerText,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account name column mapper.
|
||||||
|
*/
|
||||||
|
const commonColumnMapper = R.curry((data, column) => {
|
||||||
|
const accessor = getTableCellValueAccessor(column.cell_index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: column.key,
|
||||||
|
Header: column.label,
|
||||||
|
accessor,
|
||||||
|
className: column.key,
|
||||||
|
textOverview: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numeric columns accessor.
|
||||||
|
*/
|
||||||
|
const numericColumnAccessor = R.curry((data, column) => {
|
||||||
|
const accessor = getTableCellValueAccessor(column.cell_index);
|
||||||
|
const width = getReportColWidth(data, accessor, column.label);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
align: Align.Right,
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamiColumnMapper = R.curry((data, column) => {
|
||||||
|
const _numericColumnAccessor = numericColumnAccessor(data);
|
||||||
|
|
||||||
|
return R.compose(
|
||||||
|
R.when(R.pathEq(['key'], 'sold_quantity'), _numericColumnAccessor),
|
||||||
|
R.when(R.pathEq(['key'], 'sold_amount'), _numericColumnAccessor),
|
||||||
|
R.when(R.pathEq(['key'], 'average_price'), _numericColumnAccessor),
|
||||||
|
commonColumnMapper(data),
|
||||||
|
)(column);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes the dynamic columns that fetched from request to columns to table component.
|
||||||
|
*/
|
||||||
|
export const dynamicColumns = R.curry((data, columns) => {
|
||||||
|
return R.map(dynamiColumnMapper(data), columns);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the G/L sheet table columns for table component.
|
||||||
|
*/
|
||||||
|
export const useSalesByItemsTableColumns = () => {
|
||||||
|
const { salesByItems } = useSalesByItemsContext();
|
||||||
|
|
||||||
|
if (!salesByItems) {
|
||||||
|
throw new Error('Sales by items context not found');
|
||||||
|
}
|
||||||
|
const { table } = salesByItems;
|
||||||
|
|
||||||
|
return dynamicColumns(table.rows, table.columns);
|
||||||
|
};
|
||||||
@@ -176,7 +176,7 @@ export function useGeneralLedgerSheet(query, props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const useGeneralLedgerSheetXlsxExport = (query, args) => {
|
export const useGeneralLedgerSheetXlsxExport = (query, args) => {
|
||||||
return useDownloadFile({
|
return useDownloadFile({
|
||||||
url: '/financial_statements/general_ledger',
|
url: '/financial_statements/general_ledger',
|
||||||
config: {
|
config: {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -186,11 +186,11 @@ return useDownloadFile({
|
|||||||
},
|
},
|
||||||
filename: 'general_ledger.xlsx',
|
filename: 'general_ledger.xlsx',
|
||||||
...args,
|
...args,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGeneralLedgerSheetCsvExport = (query, args) => {
|
export const useGeneralLedgerSheetCsvExport = (query, args) => {
|
||||||
return useDownloadFile({
|
return useDownloadFile({
|
||||||
url: '/financial_statements/general_ledger',
|
url: '/financial_statements/general_ledger',
|
||||||
config: {
|
config: {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -200,10 +200,9 @@ return useDownloadFile({
|
|||||||
},
|
},
|
||||||
filename: 'general_ledger.csv',
|
filename: 'general_ledger.csv',
|
||||||
...args,
|
...args,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve journal sheet.
|
* Retrieve journal sheet.
|
||||||
*/
|
*/
|
||||||
@@ -414,20 +413,60 @@ export function useSalesByItems(query, props) {
|
|||||||
params: query,
|
params: query,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
select: (res) => ({
|
|
||||||
tableRows: salesByItemsReducer(res.data.data),
|
|
||||||
...res.data,
|
|
||||||
}),
|
|
||||||
defaultData: {
|
|
||||||
tableRows: [],
|
|
||||||
data: [],
|
|
||||||
query: {},
|
|
||||||
},
|
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves sales by items table format.
|
||||||
|
*/
|
||||||
|
export function useSalesByItemsTable(query, props) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.FINANCIAL_REPORT, t.SALES_BY_ITEMS, query],
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
url: '/financial_statements/sales-by-items',
|
||||||
|
params: query,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json+table',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: (res) => res.data,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSalesByItemsCsvExport = (query, args) => {
|
||||||
|
return useDownloadFile({
|
||||||
|
url: '/financial_statements/sales-by-items',
|
||||||
|
config: {
|
||||||
|
headers: {
|
||||||
|
accept: 'application/csv',
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
},
|
||||||
|
filename: 'sales_by_items.csv',
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSalesByItemsXlsxExport = (query, args) => {
|
||||||
|
return useDownloadFile({
|
||||||
|
url: '/financial_statements/sales-by-items',
|
||||||
|
config: {
|
||||||
|
headers: {
|
||||||
|
accept: 'application/xlsx',
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
},
|
||||||
|
filename: 'sales_by_items.xlsx',
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve customers balance summary report.
|
* Retrieve customers balance summary report.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user