From 83510cfa70d49d0b057b9c6628285a0b2df9c860 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sun, 30 Apr 2023 17:24:49 +0200 Subject: [PATCH] feat(server): add structure query flat or tree to accounts chart endpoint --- .../server/src/api/controllers/Accounts.ts | 13 ++++- .../FinancialStatements/GeneralLedger.ts | 1 + packages/server/src/interfaces/Account.ts | 6 +++ .../server/src/lib/Transformer/Transformer.ts | 24 ++++++++- .../src/services/Accounts/AccountTransform.ts | 51 +++++++++++++++++-- .../src/services/Accounts/GetAccount.ts | 6 ++- .../src/services/Accounts/GetAccounts.ts | 23 +++++---- .../CashflowAccountTransactionsRepo.ts | 2 +- packages/server/src/utils/index.ts | 50 ++++++++++++++++++ 9 files changed, 158 insertions(+), 18 deletions(-) diff --git a/packages/server/src/api/controllers/Accounts.ts b/packages/server/src/api/controllers/Accounts.ts index 7d5b0271a..297201ae7 100644 --- a/packages/server/src/api/controllers/Accounts.ts +++ b/packages/server/src/api/controllers/Accounts.ts @@ -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), }; diff --git a/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts b/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts index 90955be15..ac3f002a7 100644 --- a/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts +++ b/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts @@ -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), diff --git a/packages/server/src/interfaces/Account.ts b/packages/server/src/interfaces/Account.ts index 278840c3a..de8a2332c 100644 --- a/packages/server/src/interfaces/Account.ts +++ b/packages/server/src/interfaces/Account.ts @@ -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 { diff --git a/packages/server/src/lib/Transformer/Transformer.ts b/packages/server/src/lib/Transformer/Transformer.ts index 9a3bce78c..cb9538a64 100644 --- a/packages/server/src/lib/Transformer/Transformer.ts +++ b/packages/server/src/lib/Transformer/Transformer.ts @@ -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); } diff --git a/packages/server/src/services/Accounts/AccountTransform.ts b/packages/server/src/services/Accounts/AccountTransform.ts index 62e3eddd7..9297994be 100644 --- a/packages/server/src/services/Accounts/AccountTransform.ts +++ b/packages/server/src/services/Accounts/AccountTransform.ts @@ -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; }; } diff --git a/packages/server/src/services/Accounts/GetAccount.ts b/packages/server/src/services/Accounts/GetAccount.ts index f14c1afc7..7da213328 100644 --- a/packages/server/src/services/Accounts/GetAccount.ts +++ b/packages/server/src/services/Accounts/GetAccount.ts @@ -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']], diff --git a/packages/server/src/services/Accounts/GetAccounts.ts b/packages/server/src/services/Accounts/GetAccounts.ts index ec2d1453a..c9f1b1d5f 100644 --- a/packages/server/src/services/Accounts/GetAccounts.ts +++ b/packages/server/src/services/Accounts/GetAccounts.ts @@ -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, diff --git a/packages/server/src/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsRepo.ts b/packages/server/src/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsRepo.ts index 55049836a..99456fa3a 100644 --- a/packages/server/src/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsRepo.ts +++ b/packages/server/src/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsRepo.ts @@ -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. diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 9fa8649f8..e1178a2f6 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -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, };