mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +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 { Service, Inject } from 'typedi';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
|
import {
|
||||||
|
AbilitySubject,
|
||||||
|
AccountAction,
|
||||||
|
IAccountDTO,
|
||||||
|
IAccountsStructureType,
|
||||||
|
} from '@/interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||||
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
|
|||||||
|
|
||||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
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',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
inactiveMode: false,
|
inactiveMode: false,
|
||||||
|
structure: IAccountsStructureType.Tree,
|
||||||
...this.matchedQueryData(req),
|
...this.matchedQueryData(req),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
|
|||||||
try {
|
try {
|
||||||
const { data, query, meta } =
|
const { data, query, meta } =
|
||||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
meta: this.transfromToResponse(meta),
|
meta: this.transfromToResponse(meta),
|
||||||
data: this.transfromToResponse(data),
|
data: this.transfromToResponse(data),
|
||||||
|
|||||||
@@ -79,9 +79,15 @@ export interface IAccountTransaction {
|
|||||||
}
|
}
|
||||||
export interface IAccountResponse extends IAccount {}
|
export interface IAccountResponse extends IAccount {}
|
||||||
|
|
||||||
|
export enum IAccountsStructureType {
|
||||||
|
Tree = 'tree',
|
||||||
|
Flat = 'flat',
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAccountsFilter extends IDynamicListFilterDTO {
|
export interface IAccountsFilter extends IDynamicListFilterDTO {
|
||||||
stringifiedFilterRoles?: string;
|
stringifiedFilterRoles?: string;
|
||||||
onlyInactive: boolean;
|
onlyInactive: boolean;
|
||||||
|
structure?: IAccountsStructureType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountType {
|
export interface IAccountType {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||||
import { formatNumber } from 'utils';
|
import { formatNumber } from 'utils';
|
||||||
|
import { isArrayLikeObject } from 'lodash/fp';
|
||||||
|
|
||||||
export class Transformer {
|
export class Transformer {
|
||||||
public context: any;
|
public context: any;
|
||||||
@@ -39,12 +40,33 @@ export class Transformer {
|
|||||||
return object;
|
return object;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected preCollectionTransform = (object: any) => {
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected postCollectionTransform = (object: any) => {
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public work = (object: any) => {
|
public work = (object: any) => {
|
||||||
if (Array.isArray(object)) {
|
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)) {
|
} else if (isObject(object)) {
|
||||||
return this.getTransformation(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 { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
import { formatNumber } from 'utils';
|
import {
|
||||||
|
assocDepthLevelToObjectTree,
|
||||||
|
flatToNestedArray,
|
||||||
|
formatNumber,
|
||||||
|
nestedArrayToFlatten,
|
||||||
|
} from 'utils';
|
||||||
|
|
||||||
export class AccountTransformer extends Transformer {
|
export class AccountTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -8,7 +13,23 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
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}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedAmount = (account: IAccount): string => {
|
protected formattedAmount = (account: IAccount): string => {
|
||||||
return formatNumber(account.amount, {
|
return formatNumber(account.amount, { currencyCode: account.currencyCode });
|
||||||
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) => {
|
public getAccount = async (tenantId: number, accountId: number) => {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
// Find the given account or throw not found error.
|
// Find the given account or throw not found error.
|
||||||
const account = await Account.query().findById(accountId).throwIfNotFound();
|
const account = await Account.query().findById(accountId).throwIfNotFound();
|
||||||
|
|
||||||
|
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||||
|
|
||||||
// Transformes the account model to POJO.
|
// Transformes the account model to POJO.
|
||||||
const transformed = await this.transformer.transform(
|
const transformed = await this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
account,
|
account,
|
||||||
new AccountTransformer()
|
new AccountTransformer(),
|
||||||
|
{ accountsGraph }
|
||||||
);
|
);
|
||||||
return this.i18nService.i18nApply(
|
return this.i18nService.i18nApply(
|
||||||
[['accountTypeLabel'], ['accountNormalFormatted']],
|
[['accountTypeLabel'], ['accountNormalFormatted']],
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
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 TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||||
import { AccountTransformer } from './AccountTransform';
|
import { AccountTransformer } from './AccountTransform';
|
||||||
@@ -38,6 +43,7 @@ export class GetAccounts {
|
|||||||
filterDTO: IAccountsFilter
|
filterDTO: IAccountsFilter
|
||||||
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
|
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
// Parses the stringified filter roles.
|
// Parses the stringified filter roles.
|
||||||
const filter = this.parseListFilterDTO(filterDTO);
|
const filter = this.parseListFilterDTO(filterDTO);
|
||||||
@@ -53,17 +59,16 @@ export class GetAccounts {
|
|||||||
dynamicList.buildQuery()(builder);
|
dynamicList.buildQuery()(builder);
|
||||||
builder.modify('inactiveMode', filter.inactiveMode);
|
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,
|
tenantId,
|
||||||
accounts,
|
accounts,
|
||||||
new AccountTransformer()
|
new AccountTransformer(),
|
||||||
|
{ accountsGraph, structure: filterDTO.structure }
|
||||||
);
|
);
|
||||||
// Transform accounts to nested array.
|
|
||||||
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
|
|
||||||
id: 'id',
|
|
||||||
parentId: 'parentAccountId',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts: transformedAccounts,
|
accounts: transformedAccounts,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces
|
|||||||
@Service()
|
@Service()
|
||||||
export default class CashflowAccountTransactionsRepo {
|
export default class CashflowAccountTransactionsRepo {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the cashflow account transactions.
|
* Retrieve the cashflow account transactions.
|
||||||
|
|||||||
@@ -419,6 +419,54 @@ export const parseDate = (date: string) => {
|
|||||||
return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : '';
|
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 {
|
export {
|
||||||
templateRender,
|
templateRender,
|
||||||
accumSum,
|
accumSum,
|
||||||
@@ -449,4 +497,6 @@ export {
|
|||||||
dateRangeFromToCollection,
|
dateRangeFromToCollection,
|
||||||
transformToMapKeyValue,
|
transformToMapKeyValue,
|
||||||
mergeObjectsBykey,
|
mergeObjectsBykey,
|
||||||
|
nestedArrayToFlatten,
|
||||||
|
assocDepthLevelToObjectTree,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user