mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat: flatten the nested columns of exported data
This commit is contained in:
@@ -45,7 +45,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
|
||||
);
|
||||
const { results, pagination } = await CreditNote.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('entries.item');
|
||||
builder.withGraphFetched('customer');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import xlsx from 'xlsx';
|
||||
import * as R from 'ramda';
|
||||
import { get } from 'lodash';
|
||||
import { sanitizeResourceName } from '../Import/_utils';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import { ExportableResources } from './ExportResources';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Errors } from './common';
|
||||
import { IModelMeta } from '@/interfaces';
|
||||
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
|
||||
import { flatDataCollections, getDataAccessor } from './utils';
|
||||
|
||||
@Service()
|
||||
export class ExportResourceService {
|
||||
@@ -22,16 +24,16 @@ export class ExportResourceService {
|
||||
* @param {string} resourceName - Resource name.
|
||||
* @param {string} format - File format.
|
||||
*/
|
||||
async export(tenantId: number, resourceName: string, format: string = 'csv') {
|
||||
public async export(tenantId: number, resourceName: string, format: string = 'csv') {
|
||||
const resource = sanitizeResourceName(resourceName);
|
||||
const resourceMeta = this.getResourceMeta(tenantId, resource);
|
||||
|
||||
this.validateResourceMeta(resourceMeta);
|
||||
|
||||
const data = await this.getExportableData(tenantId, resource);
|
||||
const transformed = this.transformExportedData(tenantId, resource, data);
|
||||
const exportableColumns = this.getExportableColumns(resourceMeta);
|
||||
|
||||
const workbook = this.createWorkbook(data, exportableColumns);
|
||||
const workbook = this.createWorkbook(transformed, exportableColumns);
|
||||
|
||||
return this.exportWorkbook(workbook, format);
|
||||
}
|
||||
@@ -57,6 +59,29 @@ export class ExportResourceService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the exported data based on the resource metadata.
|
||||
* If the resource metadata specifies a flattening attribute (`exportFlattenOn`),
|
||||
* the data will be flattened based on this attribute using the `flatDataCollections` utility function.
|
||||
*
|
||||
* @param {number} tenantId - The tenant identifier.
|
||||
* @param {string} resource - The name of the resource.
|
||||
* @param {Array<Record<string, any>>} data - The original data to be transformed.
|
||||
* @returns {Array<Record<string, any>>} - The transformed data.
|
||||
*/
|
||||
private transformExportedData(
|
||||
tenantId: number,
|
||||
resource: string,
|
||||
data: Array<Record<string, any>>
|
||||
): Array<Record<string, any>> {
|
||||
const resourceMeta = this.getResourceMeta(tenantId, resource);
|
||||
|
||||
return R.when<Array<Record<string, any>>, Array<Record<string, any>>>(
|
||||
R.always(Boolean(resourceMeta.exportFlattenOn)),
|
||||
(data) => flatDataCollections(data, resourceMeta.exportFlattenOn),
|
||||
data
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Fetches exportable data for a given resource.
|
||||
* @param {number} tenantId - The tenant identifier.
|
||||
@@ -75,13 +100,29 @@ export class ExportResourceService {
|
||||
* @returns An array of exportable columns.
|
||||
*/
|
||||
private getExportableColumns(resourceMeta: IModelMeta) {
|
||||
return Object.entries(resourceMeta.columns)
|
||||
.filter(([_, value]) => value.exportable !== false)
|
||||
.map(([key, value]) => ({
|
||||
name: value.name,
|
||||
type: value.type,
|
||||
accessor: value.accessor || key,
|
||||
}));
|
||||
const processColumns = (
|
||||
columns: { [key: string]: IModelMetaColumn },
|
||||
parent = ''
|
||||
) => {
|
||||
return Object.entries(columns)
|
||||
.filter(([_, value]) => value.exportable !== false)
|
||||
.flatMap(([key, value]) => {
|
||||
if (value.type === 'collection' && value.collectionOf === 'object') {
|
||||
return processColumns(value.columns, key);
|
||||
} else {
|
||||
const group = parent;
|
||||
return [
|
||||
{
|
||||
name: value.name,
|
||||
type: value.type || 'text',
|
||||
accessor: value.accessor || key,
|
||||
group,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
};
|
||||
return processColumns(resourceMeta.columns);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,12 +134,14 @@ export class ExportResourceService {
|
||||
private createWorkbook(data: any[], exportableColumns: any[]) {
|
||||
const workbook = xlsx.utils.book_new();
|
||||
const worksheetData = data.map((item) =>
|
||||
exportableColumns.map((col) => get(item, col.accessor))
|
||||
exportableColumns.map((col) => get(item, getDataAccessor(col)))
|
||||
);
|
||||
|
||||
worksheetData.unshift(exportableColumns.map((col) => col.name));
|
||||
|
||||
const worksheet = xlsx.utils.aoa_to_sheet(worksheetData);
|
||||
xlsx.utils.book_append_sheet(workbook, worksheet, 'Exported Data');
|
||||
|
||||
return workbook;
|
||||
}
|
||||
|
||||
|
||||
27
packages/server/src/services/Export/utils.ts
Normal file
27
packages/server/src/services/Export/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { flatMap } from 'lodash';
|
||||
/**
|
||||
* Flattens the data based on a specified attribute.
|
||||
* @param data - The data to be flattened.
|
||||
* @param flattenAttr - The attribute to be flattened.
|
||||
* @returns - The flattened data.
|
||||
*/
|
||||
export const flatDataCollections = (
|
||||
data: Record<string, any>,
|
||||
flattenAttr: string
|
||||
): Record<string, any>[] => {
|
||||
return flatMap(data, (item) =>
|
||||
item[flattenAttr].map((entry) => ({
|
||||
...item,
|
||||
[flattenAttr]: entry,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the data accessor for a given column.
|
||||
* @param col - The column to get the data accessor for.
|
||||
* @returns - The data accessor.
|
||||
*/
|
||||
export const getDataAccessor = (col: any) => {
|
||||
return col.group ? `${col.group}.${col.accessor}` : col.accessor;
|
||||
};
|
||||
@@ -49,6 +49,7 @@ export class GetBills {
|
||||
const { results, pagination } = await Bill.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('vendor');
|
||||
builder.withGraphFetched('entries.item');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
@@ -14,6 +14,7 @@ export class VendorCreditTransformer extends Transformer {
|
||||
'formattedSubtotal',
|
||||
'formattedVendorCreditDate',
|
||||
'formattedCreditsRemaining',
|
||||
'formattedInvoicedAmount',
|
||||
'entries',
|
||||
];
|
||||
};
|
||||
@@ -58,6 +59,17 @@ export class VendorCreditTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted invoiced amount.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedInvoicedAmount = (credit) => {
|
||||
return formatNumber(credit.invoicedAmount, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the bill.
|
||||
* @param {IVendorCredit} vendorCredit
|
||||
|
||||
@@ -105,7 +105,11 @@ export default class ResourceService {
|
||||
const $enumerationType = (field) =>
|
||||
field.fieldType === 'enumeration' ? field : undefined;
|
||||
|
||||
const $hasFields = (field) => 'undefined' !== typeof field.fields ? field : undefined;
|
||||
const $hasFields = (field) =>
|
||||
'undefined' !== typeof field.fields ? field : undefined;
|
||||
|
||||
const $hasColumns = (column) =>
|
||||
'undefined' !== typeof column.columns ? column : undefined;
|
||||
|
||||
const naviagations = [
|
||||
['fields', qim.$each, 'name'],
|
||||
@@ -114,6 +118,7 @@ export default class ResourceService {
|
||||
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
|
||||
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
|
||||
['columns', qim.$each, 'name'],
|
||||
['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'],
|
||||
];
|
||||
return this.i18nService.i18nApply(naviagations, meta, tenantId);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export class GetSaleEstimates {
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('customer');
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('entries.item');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
@@ -49,7 +49,7 @@ export class GetSaleInvoices {
|
||||
);
|
||||
const { results, pagination } = await SaleInvoice.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('entries.item');
|
||||
builder.withGraphFetched('customer');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
|
||||
@@ -11,6 +11,9 @@ import { SaleReceiptTransformer } from './SaleReceiptTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
|
||||
interface GetSaleReceiptsSettings {
|
||||
fetchEntriesGraph?: boolean;
|
||||
}
|
||||
@Service()
|
||||
export class GetSaleReceipts {
|
||||
@Inject()
|
||||
@@ -50,7 +53,7 @@ export class GetSaleReceipts {
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('depositAccount');
|
||||
builder.withGraphFetched('customer');
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('entries.item');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user