mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: export purchases by items to csv/xlsx (#327)
This commit is contained in:
@@ -18,6 +18,7 @@ import withPurchasesByItems from './withPurchasesByItems';
|
||||
import withPurchasesByItemsActions from './withPurchasesByItemsActions';
|
||||
import { compose, saveInvoke } from '@/utils';
|
||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||
import { PurchasesByItemsExportMenu } from './components';
|
||||
|
||||
function PurchasesByItemsActionsBar({
|
||||
// #withPurchasesByItems
|
||||
@@ -106,11 +107,18 @@ function PurchasesByItemsActionsBar({
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
||||
text={<T id={'export'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={<PurchasesByItemsExportMenu />}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
placement="bottom-start"
|
||||
minimal
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
||||
text={<T id={'export'} />}
|
||||
/>
|
||||
</Popover>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import FinancialReportPage from '../FinancialReportPage';
|
||||
import { usePurchasesByItems } from '@/hooks/query';
|
||||
import { usePurchasesByItemsTable } from '@/hooks/query';
|
||||
import { transformFilterFormToQuery } from '../common';
|
||||
|
||||
const PurchasesByItemsContext = createContext();
|
||||
@@ -13,7 +13,7 @@ function PurchasesByItemsProvider({ query, ...props }) {
|
||||
isFetching,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = usePurchasesByItems(
|
||||
} = usePurchasesByItemsTable(
|
||||
{
|
||||
...transformFilterFormToQuery(query),
|
||||
},
|
||||
@@ -26,7 +26,6 @@ function PurchasesByItemsProvider({ query, ...props }) {
|
||||
purchaseByItems,
|
||||
isFetching,
|
||||
isLoading,
|
||||
|
||||
refetchSheet: refetch,
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -6,10 +6,10 @@ import styled from 'styled-components';
|
||||
import { ReportDataTable, FinancialSheet } from '@/components';
|
||||
|
||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||
import { usePurchasesByItemsTableColumns } from './components';
|
||||
|
||||
import { tableRowTypesToClassnames } from '@/utils';
|
||||
import { TableStyle } from '@/constants';
|
||||
import { usePurchasesByItemsTableColumns } from './dynamicColumns';
|
||||
|
||||
/**
|
||||
* Purchases by items data table.
|
||||
@@ -17,7 +17,7 @@ import { TableStyle } from '@/constants';
|
||||
export default function PurchasesByItemsTable({ companyName }) {
|
||||
// Purchases by items context.
|
||||
const {
|
||||
purchaseByItems: { tableRows, query },
|
||||
purchaseByItems: { table, query },
|
||||
} = usePurchaseByItemsContext();
|
||||
|
||||
// Purchases by items table columns.
|
||||
@@ -32,7 +32,7 @@ export default function PurchasesByItemsTable({ companyName }) {
|
||||
>
|
||||
<PurchasesByItemsDataTable
|
||||
columns={columns}
|
||||
data={tableRows}
|
||||
data={table.rows}
|
||||
expandable={true}
|
||||
expandToggleColumn={1}
|
||||
expandColumnSpace={1}
|
||||
@@ -58,7 +58,7 @@ const PurchasesByItemsDataTable = styled(ReportDataTable)`
|
||||
padding-top: 0.36rem;
|
||||
padding-bottom: 0.36rem;
|
||||
}
|
||||
.tr.row_type--total .td {
|
||||
.tr.row_type--TOTAL .td {
|
||||
border-top: 1px solid #bbb;
|
||||
font-weight: 500;
|
||||
border-bottom: 3px double #000;
|
||||
|
||||
@@ -1,69 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Classes,
|
||||
Intent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ProgressBar,
|
||||
Text,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
import { If } from '@/components';
|
||||
import { Align } from '@/constants';
|
||||
import { CellTextSpan } from '@/components/Datatable/Cells';
|
||||
import { AppToaster, If, Stack } from '@/components';
|
||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||
import { getColumnWidth } from '@/utils';
|
||||
import FinancialLoadingBar from '../FinancialLoadingBar';
|
||||
|
||||
/**
|
||||
* Retrieve purchases by items table columns.
|
||||
*/
|
||||
export const usePurchasesByItemsTableColumns = () => {
|
||||
// purchases by items context.
|
||||
const {
|
||||
purchaseByItems: { tableRows },
|
||||
} = usePurchaseByItemsContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: intl.get('item_name'),
|
||||
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
|
||||
className: 'name',
|
||||
width: 180,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
Header: intl.get('quantity_purchased'),
|
||||
accessor: 'quantity_purchased_formatted',
|
||||
Cell: CellTextSpan,
|
||||
className: 'quantity_purchased_formatted',
|
||||
width: getColumnWidth(tableRows, `quantity_purchased_formatted`, {
|
||||
minWidth: 150,
|
||||
}),
|
||||
textOverview: true,
|
||||
align: Align.Right,
|
||||
},
|
||||
{
|
||||
Header: intl.get('purchase_amount'),
|
||||
accessor: 'purchase_cost_formatted',
|
||||
Cell: CellTextSpan,
|
||||
className: 'purchase_cost_formatted',
|
||||
width: getColumnWidth(tableRows, `purchase_cost_formatted`, {
|
||||
minWidth: 150,
|
||||
}),
|
||||
textOverview: true,
|
||||
align: Align.Right,
|
||||
},
|
||||
{
|
||||
Header: intl.get('average_price'),
|
||||
accessor: 'average_cost_price_formatted',
|
||||
Cell: CellTextSpan,
|
||||
className: 'average_cost_price_formatted',
|
||||
width: getColumnWidth(tableRows, `average_cost_price_formatted`, {
|
||||
minWidth: 180,
|
||||
}),
|
||||
textOverview: true,
|
||||
align: Align.Right,
|
||||
},
|
||||
],
|
||||
[tableRows],
|
||||
);
|
||||
};
|
||||
import {
|
||||
usePurchasesByItemsCsvExport,
|
||||
usePurchasesByItemsXlsxExport,
|
||||
} from '@/hooks/query';
|
||||
|
||||
/**
|
||||
* Purchases by items progress loading bar.
|
||||
@@ -77,3 +30,88 @@ export function PurchasesByItemsLoadingBar() {
|
||||
</If>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the purchases by items export menu.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export const PurchasesByItemsExportMenu = () => {
|
||||
const toastKey = useRef(null);
|
||||
const commonToastConfig = {
|
||||
isCloseButtonShown: true,
|
||||
timeout: 2000,
|
||||
};
|
||||
const { query } = usePurchaseByItemsContext();
|
||||
|
||||
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 } = usePurchasesByItemsXlsxExport(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 } = usePurchasesByItemsCsvExport(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();
|
||||
};
|
||||
// Handle xlsx export button click.
|
||||
const handleXlsxExportBtnClick = () => {
|
||||
xlsxExport();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={'XLSX (Microsoft Excel)'}
|
||||
onClick={handleXlsxExportBtnClick}
|
||||
/>
|
||||
<MenuItem text={'CSV'} onClick={handleCsvExportBtnClick} />
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// @ts-nocheck
|
||||
import { getColumnWidth } from '@/utils';
|
||||
import * as R from 'ramda';
|
||||
import { Align } from '@/constants';
|
||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Item name column accessor.
|
||||
*/
|
||||
const itemNameColumnAccessor = R.curry((data, column) => {
|
||||
return {
|
||||
...column,
|
||||
width: 180,
|
||||
};
|
||||
});
|
||||
|
||||
const dynamiColumnMapper = R.curry((data, column) => {
|
||||
const _numericColumnAccessor = numericColumnAccessor(data);
|
||||
const _itemNameColumnAccessor = itemNameColumnAccessor(data);
|
||||
|
||||
return R.compose(
|
||||
R.when(R.pathEq(['key'], 'item_name'), _itemNameColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'quantity_purchases'), _numericColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'purchase_amount'), _numericColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'average_cost'), _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 purchases by items sheet table columns for table component.
|
||||
*/
|
||||
export const usePurchasesByItemsTableColumns = () => {
|
||||
const { purchaseByItems } = usePurchaseByItemsContext();
|
||||
|
||||
if (!purchaseByItems) {
|
||||
throw new Error('Purchases by items context not found');
|
||||
}
|
||||
const { table } = purchaseByItems;
|
||||
|
||||
return dynamicColumns(table.rows, table.columns);
|
||||
};
|
||||
@@ -426,20 +426,58 @@ export function usePurchasesByItems(query, props) {
|
||||
params: query,
|
||||
},
|
||||
{
|
||||
select: (res) => ({
|
||||
tableRows: purchasesByItemsReducer(res.data.data),
|
||||
...res.data,
|
||||
}),
|
||||
defaultData: {
|
||||
tableRows: [],
|
||||
data: [],
|
||||
query: {},
|
||||
},
|
||||
select: (res) => res.data,
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function usePurchasesByItemsTable(query, props) {
|
||||
return useRequestQuery(
|
||||
[t.FINANCIAL_REPORT, t.PURCHASES_BY_ITEMS, query],
|
||||
{
|
||||
method: 'get',
|
||||
url: '/financial_statements/purchases-by-items',
|
||||
params: query,
|
||||
headers: {
|
||||
accept: 'application/json+table',
|
||||
},
|
||||
},
|
||||
{
|
||||
select: (res) => res.data,
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const usePurchasesByItemsCsvExport = (query, args) => {
|
||||
return useDownloadFile({
|
||||
url: '/financial_statements/purchases-by-items',
|
||||
config: {
|
||||
headers: {
|
||||
accept: 'application/csv',
|
||||
},
|
||||
params: query,
|
||||
},
|
||||
filename: 'purchases_by_items.csv',
|
||||
...args,
|
||||
});
|
||||
};
|
||||
|
||||
export const usePurchasesByItemsXlsxExport = (query, args) => {
|
||||
return useDownloadFile({
|
||||
url: '/financial_statements/purchases-by-items',
|
||||
config: {
|
||||
headers: {
|
||||
accept: 'application/xlsx',
|
||||
},
|
||||
params: query,
|
||||
},
|
||||
filename: 'purchases_by_items.xlsx',
|
||||
...args,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve sales by items.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user