mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
feat(server): add structure query flat or tree to accounts chart endpoint
This commit is contained in:
@@ -3,7 +3,12 @@ import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
|
||||
import {
|
||||
AbilitySubject,
|
||||
AccountAction,
|
||||
IAccountDTO,
|
||||
IAccountsStructureType,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('structure')
|
||||
.optional()
|
||||
.isString()
|
||||
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -341,6 +351,7 @@ export default class AccountsController extends BaseController {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
structure: IAccountsStructureType.Tree,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
|
||||
@@ -79,9 +79,15 @@ export interface IAccountTransaction {
|
||||
}
|
||||
export interface IAccountResponse extends IAccount {}
|
||||
|
||||
export enum IAccountsStructureType {
|
||||
Tree = 'tree',
|
||||
Flat = 'flat',
|
||||
}
|
||||
|
||||
export interface IAccountsFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string;
|
||||
onlyInactive: boolean;
|
||||
structure?: IAccountsStructureType;
|
||||
}
|
||||
|
||||
export interface IAccountType {
|
||||
|
||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||
import { formatNumber } from 'utils';
|
||||
import { isArrayLikeObject } from 'lodash/fp';
|
||||
|
||||
export class Transformer {
|
||||
public context: any;
|
||||
@@ -39,12 +40,33 @@ export class Transformer {
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param object
|
||||
* @returns
|
||||
*/
|
||||
protected preCollectionTransform = (object: any) => {
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param object
|
||||
* @returns
|
||||
*/
|
||||
protected postCollectionTransform = (object: any) => {
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public work = (object: any) => {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(this.getTransformation);
|
||||
const preTransformed = this.preCollectionTransform(object);
|
||||
const transformed = preTransformed.map(this.getTransformation);
|
||||
|
||||
return this.postCollectionTransform(transformed);
|
||||
} else if (isObject(object)) {
|
||||
return this.getTransformation(object);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { IAccount } from '@/interfaces';
|
||||
import { IAccount, IAccountsStructureType } from '@/interfaces';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
import {
|
||||
assocDepthLevelToObjectTree,
|
||||
flatToNestedArray,
|
||||
formatNumber,
|
||||
nestedArrayToFlatten,
|
||||
} from 'utils';
|
||||
|
||||
export class AccountTransformer extends Transformer {
|
||||
/**
|
||||
@@ -8,7 +13,23 @@ export class AccountTransformer extends Transformer {
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedAmount'];
|
||||
return ['formattedAmount', 'flattenName'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the flatten name with all dependants accounts names.
|
||||
* @param {IAccount} account -
|
||||
* @returns {string}
|
||||
*/
|
||||
public flattenName = (account: IAccount): string => {
|
||||
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
|
||||
account.id
|
||||
);
|
||||
const prefixAccounts = parentDependantsIds.map((dependId) => {
|
||||
const node = this.options.accountsGraph.getNodeData(dependId);
|
||||
return `${node.name}: `;
|
||||
});
|
||||
return `${prefixAccounts}${account.name}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -17,8 +38,28 @@ export class AccountTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (account: IAccount): string => {
|
||||
return formatNumber(account.amount, {
|
||||
currencyCode: account.currencyCode,
|
||||
return formatNumber(account.amount, { currencyCode: account.currencyCode });
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the accounts collection to flat or nested array.
|
||||
* @param {IAccount[]}
|
||||
* @returns {IAccount[]}
|
||||
*/
|
||||
protected postCollectionTransform = (accounts: IAccount[]) => {
|
||||
// Transfom the flatten to accounts tree.
|
||||
const transformed = flatToNestedArray(accounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
// Associate `accountLevel` attr to indicate object depth.
|
||||
const transformed2 = assocDepthLevelToObjectTree(
|
||||
transformed,
|
||||
1,
|
||||
'accountLevel'
|
||||
);
|
||||
return this.options.structure === IAccountsStructureType.Flat
|
||||
? nestedArrayToFlatten(transformed2)
|
||||
: transformed2;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,15 +22,19 @@ export class GetAccount {
|
||||
*/
|
||||
public getAccount = async (tenantId: number, accountId: number) => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Find the given account or throw not found error.
|
||||
const account = await Account.query().findById(accountId).throwIfNotFound();
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Transformes the account model to POJO.
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
account,
|
||||
new AccountTransformer()
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph }
|
||||
);
|
||||
return this.i18nService.i18nApply(
|
||||
[['accountTypeLabel'], ['accountNormalFormatted']],
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { IAccountsFilter, IAccountResponse, IFilterMeta } from '@/interfaces';
|
||||
import {
|
||||
IAccountsFilter,
|
||||
IAccountResponse,
|
||||
IFilterMeta,
|
||||
IAccountsStructureType,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { AccountTransformer } from './AccountTransform';
|
||||
@@ -38,6 +43,7 @@ export class GetAccounts {
|
||||
filterDTO: IAccountsFilter
|
||||
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Parses the stringified filter roles.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
@@ -53,17 +59,16 @@ export class GetAccounts {
|
||||
dynamicList.buildQuery()(builder);
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
// Retrievs the formatted accounts collection.
|
||||
const preTransformedAccounts = await this.transformer.transform(
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieves the transformed accounts collection.
|
||||
const transformedAccounts = await this.transformer.transform(
|
||||
tenantId,
|
||||
accounts,
|
||||
new AccountTransformer()
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph, structure: filterDTO.structure }
|
||||
);
|
||||
// Transform accounts to nested array.
|
||||
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
|
||||
return {
|
||||
accounts: transformedAccounts,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsRepo {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
|
||||
@@ -419,6 +419,54 @@ export const parseDate = (date: string) => {
|
||||
return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : '';
|
||||
};
|
||||
|
||||
const nestedArrayToFlatten = (
|
||||
collection,
|
||||
property = 'children',
|
||||
parseItem = (a, level) => a,
|
||||
level = 1
|
||||
) => {
|
||||
const parseObject = (obj) =>
|
||||
parseItem(
|
||||
{
|
||||
..._.omit(obj, [property]),
|
||||
},
|
||||
level
|
||||
);
|
||||
|
||||
return collection.reduce((items, currentValue, index) => {
|
||||
let localItems = [...items];
|
||||
const parsedItem = parseObject(currentValue, level);
|
||||
localItems.push(parsedItem);
|
||||
|
||||
if (Array.isArray(currentValue[property])) {
|
||||
const flattenArray = nestedArrayToFlatten(
|
||||
currentValue[property],
|
||||
property,
|
||||
parseItem,
|
||||
level + 1
|
||||
);
|
||||
localItems = _.concat(localItems, flattenArray);
|
||||
}
|
||||
return localItems;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const assocDepthLevelToObjectTree = (
|
||||
objects,
|
||||
level = 1,
|
||||
propertyName = 'level'
|
||||
) => {
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
const object = objects[i];
|
||||
object[propertyName] = level;
|
||||
|
||||
if (object.children) {
|
||||
assocDepthLevelToObjectTree(object.children, level + 1, propertyName);
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export {
|
||||
templateRender,
|
||||
accumSum,
|
||||
@@ -449,4 +497,6 @@ export {
|
||||
dateRangeFromToCollection,
|
||||
transformToMapKeyValue,
|
||||
mergeObjectsBykey,
|
||||
nestedArrayToFlatten,
|
||||
assocDepthLevelToObjectTree,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user