feat: export purchases by items to csv/xlsx (#327)

This commit is contained in:
Ahmed Bouhuolia
2024-01-23 12:33:43 +02:00
committed by GitHub
parent 7eb84474a5
commit 429159acf9
17 changed files with 672 additions and 150 deletions

View File

@@ -1,17 +1,18 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { PurchasesByItemsService } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { PurcahsesByItemsApplication } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication';
@Service()
export default class PurchasesByItemReportController extends BaseFinancialReportController {
@Inject()
purchasesByItemsService: PurchasesByItemsService;
private purchasesByItemsApp: PurcahsesByItemsApplication;
/**
* Router constructor.
@@ -63,20 +64,47 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
* @param {Request} req -
* @param {Response} res -
*/
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
public async purchasesByItems(req: Request, res: Response) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
// JSON table response format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.purchasesByItemsApp.table(tenantId, filter);
return res.status(200).send(table);
// CSV response format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.purchasesByItemsApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Xlsx response format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.purchasesByItemsApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Json response format.
} else {
const sheet = await this.purchasesByItemsApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -1,13 +1,10 @@
import {
IAgingPeriod,
IAgingPeriodTotal,
IAgingAmount,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
} from './AgingReport';
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {

View File

@@ -0,0 +1,54 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IPurchasesByItemsReportQuery {
fromDate: Date | string;
toDate: Date | string;
itemsIds: number[];
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
onlyActive: boolean;
}
export interface IPurchasesByItemsSheetMeta {
organizationName: string;
baseCurrency: string;
}
export interface IPurchasesByItemsItem {
id: number;
name: string;
code: string;
quantitySold: number;
soldCost: number;
averageSellPrice: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
averageSellPriceFormatted: string;
currencyCode: string;
}
export interface IPurchasesByItemsTotal {
quantitySold: number;
soldCost: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
currencyCode: string;
}
export type IPurchasesByItemsSheetData = {
items: IPurchasesByItemsItem[];
total: IPurchasesByItemsTotal;
};
export interface IPurchasesByItemsSheet {
data: IPurchasesByItemsSheetData;
query: IPurchasesByItemsReportQuery;
meta: IPurchasesByItemsSheetMeta;
}
export interface IPurchasesByItemsTable extends IFinancialTable {
query: IPurchasesByItemsReportQuery;
meta: IPurchasesByItemsSheetMeta;
}

View File

@@ -3,8 +3,10 @@ import {
IAccount,
IAccountCreateDTO,
IAccountEditDTO,
IAccountResponse,
IAccountsFilter,
IAccountsTransactionsFilter,
IFilterMeta,
IGetAccountTransactionPOJO,
} from '@/interfaces';
import { CreateAccount } from './CreateAccount';
@@ -14,6 +16,7 @@ import { ActivateAccount } from './ActivateAccount';
import { GetAccounts } from './GetAccounts';
import { GetAccount } from './GetAccount';
import { GetAccountTransactions } from './GetAccountTransactions';
@Service()
export class AccountsApplication {
@Inject()
@@ -113,19 +116,22 @@ export class AccountsApplication {
/**
* Retrieves the accounts list.
* @param {number} tenantId
* @param {IAccountsFilter} filterDTO
* @returns
* @param {number} tenantId
* @param {IAccountsFilter} filterDTO
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
*/
public getAccounts = (tenantId: number, filterDTO: IAccountsFilter) => {
public getAccounts = (
tenantId: number,
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
return this.getAccountsService.getAccountsList(tenantId, filterDTO);
};
/**
* Retrieves the given account transactions.
* @param {number} tenantId
* @param {IAccountsTransactionsFilter} filter
* @returns {Promise<IGetAccountTransactionPOJO[]>}
* @param {number} tenantId
* @param {IAccountsTransactionsFilter} filter
* @returns {Promise<IGetAccountTransactionPOJO[]>}
*/
public getAccountsTransactions = (
tenantId: number,

View File

@@ -2,36 +2,34 @@ import { get, isEmpty, sumBy } from 'lodash';
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, transformToMap } from 'utils';
import { IAccountTransaction, IItem } from '@/interfaces';
import {
IAccountTransaction,
IInventoryValuationTotal,
IInventoryValuationItem,
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IItem,
} from '@/interfaces';
IPurchasesByItemsItem,
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from '@/interfaces/PurchasesByItemsSheet';
export default class InventoryValuationReport extends FinancialSheet {
export class PurchasesByItems extends FinancialSheet {
readonly baseCurrency: string;
readonly items: IItem[];
readonly itemsTransactions: Map<number, IAccountTransaction>;
readonly query: IInventoryValuationReportQuery;
readonly query: IPurchasesByItemsReportQuery;
/**
* Constructor method.
* @param {IInventoryValuationReportQuery} query
* @param {IPurchasesByItemsReportQuery} query
* @param {IItem[]} items
* @param {IAccountTransaction[]} itemsTransactions
* @param {string} baseCurrency
*/
constructor(
query: IInventoryValuationReportQuery,
query: IPurchasesByItemsReportQuery,
items: IItem[],
itemsTransactions: IAccountTransaction[],
baseCurrency: string
) {
super();
this.baseCurrency = baseCurrency;
this.items = items;
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
@@ -98,7 +96,7 @@ export default class InventoryValuationReport extends FinancialSheet {
* @param {IInventoryValuationItem} item
* @returns
*/
private itemSectionMapper = (item: IItem): IInventoryValuationItem => {
private itemSectionMapper = (item: IItem): IPurchasesByItemsItem => {
const meta = this.getItemTransaction(item.id);
return {
@@ -145,9 +143,9 @@ export default class InventoryValuationReport extends FinancialSheet {
/**
* Retrieve the items sections.
* @returns {IInventoryValuationItem[]}
* @returns {IPurchasesByItemsItem[]}
*/
private itemsSection = (): IInventoryValuationItem[] => {
private itemsSection = (): IPurchasesByItemsItem[] => {
return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter),
this.itemsMapper
@@ -156,10 +154,10 @@ export default class InventoryValuationReport extends FinancialSheet {
/**
* Retrieve the total section of the sheet.
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
* @param {IPurchasesByItemsItem[]} items
* @returns {IPurchasesByItemsTotal}
*/
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
private totalSection(items: IPurchasesByItemsItem[]): IPurchasesByItemsTotal {
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
@@ -176,12 +174,12 @@ export default class InventoryValuationReport extends FinancialSheet {
/**
* Retrieve the sheet data.
* @returns
* @returns {IInventoryValuationStatement}
*/
reportData(): IInventoryValuationStatement {
public reportData(): IPurchasesByItemsSheetData {
const items = this.itemsSection();
const total = this.totalSection(items);
return items.length > 0 ? { items, total } : {};
return { items, total };
}
}

View File

@@ -0,0 +1,73 @@
import { Service, Inject } from 'typedi';
import { PurchasesByItemsExport } from './PurchasesByItemsExport';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsTable,
} from '@/interfaces/PurchasesByItemsSheet';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService';
@Service()
export class PurcahsesByItemsApplication {
@Inject()
private purchasesByItemsSheet: PurchasesByItemsService;
@Inject()
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
@Inject()
private purchasesByItemsExport: PurchasesByItemsExport;
/**
* Retrieves the purchases by items in json format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns
*/
public sheet(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsSheet> {
return this.purchasesByItemsSheet.purchasesByItems(tenantId, query);
}
/**
* Retrieves the purchases by items in table format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<IPurchasesByItemsTable>}
*/
public table(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsTable> {
return this.purchasesByItemsTable.table(tenantId, query);
}
/**
* Retrieves the purchases by items in csv format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<string>}
*/
public csv(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<string> {
return this.purchasesByItemsExport.csv(tenantId, query);
}
/**
* Retrieves the purchases by items in xlsx format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<Buffer> {
return this.purchasesByItemsExport.xlsx(tenantId, query);
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { IPurchasesByItemsReportQuery } from '@/interfaces/PurchasesByItemsSheet';
@Service()
export class PurchasesByItemsExport {
@Inject()
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
/**
* Retrieves the purchases by items sheet in XLSX format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<Buffer> {
const table = await this.purchasesByItemsTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the purchases by items sheet in CSV format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<string> {
const table = await this.purchasesByItemsTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -1,24 +1,24 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import {
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import { Service, Inject } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import PurchasesByItems from './PurchasesByItems';
import { PurchasesByItems } from './PurchasesByItems';
import { Tenant } from '@/system/models';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsSheetMeta,
} from '@/interfaces/PurchasesByItemsSheet';
@Service()
export default class InventoryValuationReportService {
export class PurchasesByItemsService {
@Inject()
private tenancy: TenancyService;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
* Defaults purchases by items filter query.
* @return {IPurchasesByItemsReportQuery}
*/
get defaultQuery(): IInventoryValuationReportQuery {
get defaultQuery(): IPurchasesByItemsReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
@@ -40,7 +40,7 @@ export default class InventoryValuationReportService {
* @param {number} tenantId -
* @returns {IBalanceSheetMeta}
*/
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
reportMetadata(tenantId: number): IPurchasesByItemsSheetMeta {
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
@@ -62,18 +62,13 @@ export default class InventoryValuationReportService {
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*
* @return {IBalanceSheetStatement}
* @param {IPurchasesByItemsReportQuery} query
* @return {Promise<IPurchasesByItemsSheet>}
*/
public async purchasesByItems(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<{
data: IInventoryValuationStatement;
query: IInventoryValuationReportQuery;
meta: IInventoryValuationSheetMeta;
}> {
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsSheet> {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -106,7 +101,6 @@ export default class InventoryValuationReportService {
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
}
);
const purchasesByItemsInstance = new PurchasesByItems(
filter,
inventoryItems,

View File

@@ -0,0 +1,111 @@
import * as R from 'ramda';
import { ITableColumn, ITableColumnAccessor, ITableRow } from '@/interfaces';
import { ROW_TYPE } from './_types';
import { tableRowMapper } from '@/utils';
import { FinancialTable } from '../FinancialTable';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import FinancialSheet from '../FinancialSheet';
import {
IPurchasesByItemsItem,
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from '@/interfaces/PurchasesByItemsSheet';
export class PurchasesByItemsTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IPurchasesByItemsSheetData;
/**
* Constructor method.
* @param data
*/
constructor(data) {
super();
this.data = data;
}
/**
* Retrieves thge common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private commonTableAccessors(): ITableColumnAccessor[] {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'quantity_purchases', accessor: 'quantityPurchasedFormatted' },
{ key: 'purchase_amount', accessor: 'purchaseCostFormatted' },
{ key: 'average_cost', accessor: 'averageCostPriceFormatted' },
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonTableColumns(): ITableColumn[] {
return [
{ label: 'Item name', key: 'item_name' },
{ label: 'Quantity Purchased', key: 'quantity_purchases' },
{ label: 'Purchase Amount', key: 'purchase_amount' },
{ label: 'Average Price', key: 'average_cost' },
];
}
/**
* Maps the given item node to table row.
* @param {IPurchasesByItemsItem} item
* @returns {ITableRow}
*/
private itemMap = (item: IPurchasesByItemsItem): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, columns, meta);
};
/**
* Maps the given items nodes to table rows.
* @param {IPurchasesByItemsItem[]} items - Items nodes.
* @returns {ITableRow[]}
*/
private itemsMap = (items: IPurchasesByItemsItem[]): ITableRow[] => {
return R.map(this.itemMap)(items);
};
/**
* Maps the given total node to table rows.
* @param {IPurchasesByItemsTotal} total
* @returns {ITableRow}
*/
private totalNodeMap = (total: IPurchasesByItemsTotal): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, columns, meta);
};
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonTableColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
const itemsRows = this.itemsMap(this.data.items);
const totalRow = this.totalNodeMap(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
)(itemsRows) as ITableRow[];
}
}

View File

@@ -0,0 +1,38 @@
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsTable,
} from '@/interfaces/PurchasesByItemsSheet';
import { Inject, Service } from 'typedi';
import { PurchasesByItemsService } from './PurchasesByItemsService';
import { PurchasesByItemsTable } from './PurchasesByItemsTable';
@Service()
export class PurchasesByItemsTableInjectable {
@Inject()
private purchasesByItemsSheet: PurchasesByItemsService;
/**
* Retrieves the purchases by items table format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} filter
* @returns {Promise<IPurchasesByItemsTable>}
*/
public async table(
tenantId: number,
filter: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsTable> {
const { data, query, meta } =
await this.purchasesByItemsSheet.purchasesByItems(tenantId, filter);
const table = new PurchasesByItemsTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
meta,
query,
};
}
}

View File

@@ -0,0 +1,5 @@
export enum ROW_TYPE {
TOTAL = 'TOTAL',
ITEM = 'ITEM'
}

View File

@@ -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>
);

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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);
};

View File

@@ -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.
*/