mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
fix: passing number format to reports
This commit is contained in:
@@ -7,53 +7,46 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { type Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { mapKeysDeep } from '@/utils/deepdash';
|
||||
|
||||
export function camelToSnake<T = any>(value: T) {
|
||||
export function camelToSnake<T = any>(value: T): T {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(camelToSnake);
|
||||
}
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [
|
||||
key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join('_')
|
||||
.toLowerCase(),
|
||||
camelToSnake(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
return mapKeysDeep(
|
||||
value,
|
||||
(_value: string, key: any, parent: any, context: any) => {
|
||||
if (Array.isArray(parent)) {
|
||||
// tell mapKeysDeep to skip mapping inside this branch
|
||||
context.skipChildren = true;
|
||||
return key;
|
||||
}
|
||||
return key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join('_')
|
||||
.toLowerCase();
|
||||
},
|
||||
) as T;
|
||||
}
|
||||
|
||||
export function snakeToCamel<T = any>(value: T) {
|
||||
export function snakeToCamel<T = any>(value: T): T {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(snakeToCamel);
|
||||
}
|
||||
|
||||
const impl = (str: string) => {
|
||||
const converted = str.replace(/([-_]\w)/g, (group) =>
|
||||
group[1].toUpperCase(),
|
||||
);
|
||||
return converted[0].toLowerCase() + converted.slice(1);
|
||||
};
|
||||
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [
|
||||
impl(key),
|
||||
snakeToCamel(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
return mapKeysDeep(
|
||||
value,
|
||||
(_value: string, key: any, parent: any, context: any) => {
|
||||
if (Array.isArray(parent)) {
|
||||
// tell mapKeysDeep to skip mapping inside this branch
|
||||
context.skipChildren = true;
|
||||
return key;
|
||||
}
|
||||
const converted = key.replace(/([-_]\w)/g, (group) =>
|
||||
group[1].toUpperCase(),
|
||||
);
|
||||
return converted[0].toLowerCase() + converted.slice(1);
|
||||
},
|
||||
) as T;
|
||||
}
|
||||
|
||||
export const DEFAULT_STRATEGY = {
|
||||
@@ -63,7 +56,7 @@ export const DEFAULT_STRATEGY = {
|
||||
|
||||
@Injectable()
|
||||
export class SerializeInterceptor implements NestInterceptor<any, any> {
|
||||
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) {}
|
||||
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) { }
|
||||
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ServiceErrorFilter } from './common/filters/service-error.filter';
|
||||
import { ModelHasRelationsFilter } from './common/filters/model-has-relations.filter';
|
||||
import { ValidationPipe } from './common/pipes/ClassValidation.pipe';
|
||||
import { ToJsonInterceptor } from './common/interceptors/to-json.interceptor';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
||||
global.__public_dirname = path.join(__dirname, '..', 'public');
|
||||
global.__static_dirname = path.join(__dirname, '../static');
|
||||
@@ -15,7 +16,10 @@ global.__views_dirname = path.join(global.__static_dirname, '/views');
|
||||
global.__images_dirname = path.join(global.__static_dirname, '/images');
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { rawBody: true });
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
rawBody: true,
|
||||
});
|
||||
app.set('query parser', 'extended');
|
||||
app.setGlobalPrefix('/api');
|
||||
|
||||
// create and mount the middleware manually here
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IsPositive,
|
||||
} from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class NumberFormatQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
@@ -24,6 +25,7 @@ export class NumberFormatQueryDto {
|
||||
example: false,
|
||||
})
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsOptional()
|
||||
readonly divideOn1000: boolean;
|
||||
|
||||
@@ -32,6 +34,7 @@ export class NumberFormatQueryDto {
|
||||
example: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsOptional()
|
||||
readonly showZero: boolean;
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => NumberFormatQueryDto)
|
||||
@IsOptional()
|
||||
numberFormat: NumberFormatQueryDto;
|
||||
|
||||
@ApiProperty({
|
||||
|
||||
@@ -32,7 +32,7 @@ export class InventoryItemDetailsQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Number format for the inventory item details',
|
||||
})
|
||||
numberFormat: INumberFormatQuery;
|
||||
numberFormat: NumberFormatQueryDto;
|
||||
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsBoolean()
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto';
|
||||
|
||||
export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
@IsString()
|
||||
@@ -30,8 +32,10 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
toDate: moment.MomentInput;
|
||||
|
||||
@ApiProperty({ description: 'Number format configuration' })
|
||||
@Type(() => Object)
|
||||
numberFormat: INumberFormatQuery;
|
||||
@ValidateNested()
|
||||
@Type(() => NumberFormatQueryDto)
|
||||
@IsOptional()
|
||||
numberFormat: NumberFormatQueryDto;
|
||||
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"plaid-threads": "^11.4.3",
|
||||
"polished": "^4.3.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "^7.1.1",
|
||||
"qs": "^6.14.0",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "^18.2.0",
|
||||
"react-body-classname": "^1.3.1",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const getDefaultAPAgingSummaryQuery = () => {
|
||||
filterByOption: 'without-zero-balance',
|
||||
vendorsIds: [],
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export const getDefaultARAgingSummaryQuery = () => {
|
||||
filterByOption: 'without-zero-balance',
|
||||
customersIds: [],
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export const getDefaultBalanceSheetQuery = () => ({
|
||||
percentageOfRow: false,
|
||||
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ export const getDefaultCashFlowSheetQuery = () => {
|
||||
displayColumnsType: 'total',
|
||||
filterByOption: 'with-transactions',
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export const getInventoryItemDetailsDefaultQuery = () => ({
|
||||
itemsIds: [],
|
||||
warehousesIds: [],
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,7 @@ export const getDefaultProfitLossQuery = () => ({
|
||||
percentageExpense: false,
|
||||
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -50,7 +51,6 @@ const parseProfitLossQuery = (locationQuery) => {
|
||||
|
||||
return {
|
||||
...transformed,
|
||||
|
||||
// Ensures the branches ids is always array.
|
||||
branchesIds: castArray(transformed.branchesIds),
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export function getDefaultTrialBalanceQuery() {
|
||||
basis: 'accrual',
|
||||
filterByOption: 'with-transactions',
|
||||
branchesIds: [],
|
||||
numberFormat: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ export const transformAccountsFilter = (form) => {
|
||||
*/
|
||||
export const transformFilterFormToQuery = (form) => {
|
||||
return R.compose(
|
||||
R.curry(flatten)({ safe: true }),
|
||||
transfromToSnakeCase,
|
||||
transformAccountsFilter,
|
||||
transformDisplayColumnsType,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ParseOptions,
|
||||
ParsedQuery,
|
||||
StringifyOptions,
|
||||
parse,
|
||||
stringify,
|
||||
} from 'query-string';
|
||||
import * as qs from 'qs';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
export interface QueryStringResult {
|
||||
[0]: ParsedQuery;
|
||||
[0]: Record<string, any>;
|
||||
[1]: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
}
|
||||
|
||||
@@ -20,6 +14,61 @@ type NavigateCallback = (
|
||||
stringifedParams: string,
|
||||
) => void;
|
||||
|
||||
type ParseOptions = {
|
||||
parseNumbers?: boolean;
|
||||
parseBooleans?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type StringifyOptions = qs.IStringifyOptions;
|
||||
|
||||
/**
|
||||
* Checks if a string represents a number (including negatives, decimals, scientific notation)
|
||||
*/
|
||||
const isNumber = (val: string): boolean => {
|
||||
return !isNaN(parseFloat(val)) && isFinite(Number(val)) && val !== '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a string represents a boolean
|
||||
*/
|
||||
const isBoolean = (val: string): boolean => {
|
||||
return val === 'false' || val === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom decoder for qs to parse numbers and booleans
|
||||
* Based on query-types library approach: https://github.com/xpepermint/query-types
|
||||
*/
|
||||
const createDecoder = (parseNumbers: boolean, parseBooleans: boolean) => {
|
||||
return (str: string, defaultDecoder?: any, charset?: string, type?: 'key' | 'value') => {
|
||||
// Only decode values, not keys
|
||||
if (type === 'key') {
|
||||
return defaultDecoder ? defaultDecoder(str, defaultDecoder, charset) : str;
|
||||
}
|
||||
|
||||
// First decode using default decoder
|
||||
const decoded = defaultDecoder ? defaultDecoder(str, defaultDecoder, charset) : decodeURIComponent(str);
|
||||
|
||||
// Handle empty strings and undefined
|
||||
if (typeof decoded === 'undefined' || decoded === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse booleans first (before numbers, as 'true'/'false' are strings)
|
||||
if (parseBooleans && isBoolean(decoded)) {
|
||||
return decoded === 'true';
|
||||
}
|
||||
|
||||
// Parse numbers if enabled (handles integers, decimals, negatives, scientific notation)
|
||||
if (parseNumbers && isNumber(decoded)) {
|
||||
return Number(decoded);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Query string.
|
||||
* @param {Location} location
|
||||
@@ -35,14 +84,28 @@ export function useQueryString(
|
||||
stringifyOptions?: StringifyOptions,
|
||||
): QueryStringResult {
|
||||
const isFirst = useRef(true);
|
||||
const [state, setState] = useState(parse(location.search, parseOptions));
|
||||
|
||||
// Extract parseNumbers and parseBooleans from parseOptions
|
||||
const { parseNumbers = false, parseBooleans = false, ...qsParseOptions } = parseOptions || {};
|
||||
|
||||
// Create decoder if needed
|
||||
const parseConfig = {
|
||||
...qsParseOptions,
|
||||
...(parseNumbers || parseBooleans ? {
|
||||
decoder: createDecoder(parseNumbers, parseBooleans),
|
||||
} : {}),
|
||||
};
|
||||
|
||||
const [state, setState] = useState(
|
||||
qs.parse(location.search.substring(1), parseConfig)
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
if (isFirst.current) {
|
||||
isFirst.current = false;
|
||||
} else {
|
||||
const pathname = location.pathname;
|
||||
const stringifedParams = stringify(state, stringifyOptions);
|
||||
const stringifedParams = qs.stringify(state, stringifyOptions);
|
||||
const pathnameWithParams = pathname + '?' + stringifedParams;
|
||||
|
||||
navigate(pathnameWithParams, pathname, stringifedParams);
|
||||
@@ -52,7 +115,7 @@ export function useQueryString(
|
||||
const setQuery: typeof setState = (values): void => {
|
||||
const nextState = typeof values === 'function' ? values(state) : values;
|
||||
setState(
|
||||
(state): ParsedQuery => ({
|
||||
(state): Record<string, any> => ({
|
||||
...state,
|
||||
...nextState,
|
||||
}),
|
||||
@@ -78,7 +141,6 @@ export const useAppQueryString = (
|
||||
window.location,
|
||||
(pathnameWithParams, pathname, stringifiedParams) => {
|
||||
history.push({ pathname, search: stringifiedParams });
|
||||
|
||||
navigate && navigate(pathnameWithParams, pathname, stringifiedParams);
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user