wip
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsInt } from 'class-validator';
|
||||
|
||||
export class InventoryAdjustmentsFilterDto {
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
@IsInt()
|
||||
@ToNumber()
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 12 })
|
||||
@IsInt()
|
||||
@ToNumber()
|
||||
@IsOptional()
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ export class ItemCategoriesExportable extends Exportable {
|
||||
|
||||
return this.itemCategoryApp
|
||||
.getItemCategories(parsedQuery)
|
||||
.then((output) => output.data);
|
||||
.then((output) => output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,4 @@ export interface IItemCategoriesFilter extends IDynamicListFilter {
|
||||
filterQuery?: (trx: Knex.Transaction) => void;
|
||||
}
|
||||
|
||||
export interface GetItemCategoriesResponse {
|
||||
data: ItemCategory[];
|
||||
// filterMeta: IFilterMeta;
|
||||
}
|
||||
export type GetItemCategoriesResponse = ItemCategory[];
|
||||
|
||||
@@ -59,6 +59,6 @@ export class GetItemCategoriesService {
|
||||
);
|
||||
dynamicList.buildQuery()(query);
|
||||
});
|
||||
return { data };
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { GetSaleEstimatesQueryDto } from './dtos/GetSaleEstimatesQuery.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { SaleEstiamteStateResponseDto } from './dtos/SaleEstimateStateResponse.dto';
|
||||
import { SaleEstimateHtmlContentResponseDto } from './dtos/SaleEstimateHtmlResponse.dto';
|
||||
import { SaleEstimateMailStateResponseDto } from './dtos/SaleEstimateMailStateResponse.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
BulkDeleteDto,
|
||||
@@ -48,12 +49,14 @@ import { SaleEstimateAction } from './types/SaleEstimates.types';
|
||||
|
||||
@Controller('sale-estimates')
|
||||
@ApiTags('Sale Estimates')
|
||||
@ApiExtraModels(SaleEstimateResponseDto)
|
||||
@ApiExtraModels(PaginatedResponseDto)
|
||||
@ApiExtraModels(SaleEstiamteStateResponseDto)
|
||||
@ApiExtraModels(SaleEstimateHtmlContentResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||
@ApiExtraModels(
|
||||
SaleEstimateResponseDto,
|
||||
PaginatedResponseDto,
|
||||
SaleEstiamteStateResponseDto,
|
||||
SaleEstimateHtmlContentResponseDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
SaleEstimateMailStateResponseDto,
|
||||
)
|
||||
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||
export class SaleEstimatesController {
|
||||
@Post('validate-bulk-delete')
|
||||
@@ -302,6 +305,11 @@ export class SaleEstimatesController {
|
||||
@Get(':id/mail')
|
||||
@RequirePermission(SaleEstimateAction.View, AbilitySubject.SaleEstimate)
|
||||
@ApiOperation({ summary: 'Retrieves the sale estimate mail state.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Retrieves the sale estimate mail state.',
|
||||
schema: { $ref: getSchemaPath(SaleEstimateMailStateResponseDto) },
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
class AddressItemDto {
|
||||
@ApiProperty()
|
||||
label: string;
|
||||
|
||||
@ApiProperty()
|
||||
mail: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
primary?: boolean;
|
||||
}
|
||||
|
||||
class SaleEstimateEntryMailDto {
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
quantity: number;
|
||||
|
||||
@ApiProperty()
|
||||
unitPrice: number;
|
||||
|
||||
@ApiProperty()
|
||||
unitPriceFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
total: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalFormatted: string;
|
||||
}
|
||||
|
||||
export class SaleEstimateMailStateResponseDto {
|
||||
@ApiProperty({ type: [String] })
|
||||
from: string[];
|
||||
|
||||
@ApiProperty({ type: [String] })
|
||||
to: string[];
|
||||
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
cc?: string[];
|
||||
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
bcc?: string[];
|
||||
|
||||
@ApiProperty()
|
||||
subject: string;
|
||||
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
formatArgs?: { customerName: string; estimateAmount: string };
|
||||
|
||||
@ApiProperty({ type: [AddressItemDto] })
|
||||
toOptions: AddressItemDto[];
|
||||
|
||||
@ApiProperty({ type: [AddressItemDto] })
|
||||
fromOptions: AddressItemDto[];
|
||||
|
||||
@ApiPropertyOptional()
|
||||
attachEstimate?: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
estimateDate: string;
|
||||
|
||||
@ApiProperty()
|
||||
estimateDateFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
expirationDate: string;
|
||||
|
||||
@ApiProperty()
|
||||
expirationDateFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
total: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
subtotal: number;
|
||||
|
||||
@ApiProperty()
|
||||
subtotalFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
discountAmount: number;
|
||||
|
||||
@ApiProperty()
|
||||
discountAmountFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
discountPercentage: number | null;
|
||||
|
||||
@ApiProperty()
|
||||
discountPercentageFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
discountLabel: string;
|
||||
|
||||
@ApiProperty()
|
||||
adjustment: number;
|
||||
|
||||
@ApiProperty()
|
||||
adjustmentFormatted: string;
|
||||
|
||||
@ApiProperty()
|
||||
estimateNumber: string;
|
||||
|
||||
@ApiProperty({ type: [SaleEstimateEntryMailDto] })
|
||||
entries: SaleEstimateEntryMailDto[];
|
||||
|
||||
@ApiProperty()
|
||||
companyName: string;
|
||||
|
||||
@ApiProperty()
|
||||
companyLogoUri: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
primaryColor: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
customerName: string;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function ItemsCategoryTable({
|
||||
<DataTable
|
||||
noInitialFetch={true}
|
||||
columns={columns}
|
||||
data={itemsCategories}
|
||||
data={itemsCategories || []}
|
||||
loading={isCategoriesLoading}
|
||||
headerLoading={isCategoriesLoading}
|
||||
progressBarLoading={isCategoriesFetching}
|
||||
|
||||
@@ -22,9 +22,6 @@ const commonInvalidateQueries = (queryClient: ReturnType<typeof useQueryClient>)
|
||||
queryClient.invalidateQueries({ queryKey: contactsKeys.all() });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the contact by ID (for duplicate/form).
|
||||
*/
|
||||
export function useContact(
|
||||
id: number | string | undefined | null,
|
||||
props?: Omit<UseQueryOptions<ContactResponse>, 'queryKey' | 'queryFn'>
|
||||
@@ -41,9 +38,6 @@ export function useContact(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the auto-complete contacts.
|
||||
*/
|
||||
export function useAutoCompleteContacts(
|
||||
props?: Omit<UseQueryOptions<Awaited<ReturnType<typeof fetchContactsAutoComplete>>>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
@@ -57,9 +51,6 @@ export function useAutoCompleteContacts(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the given Contact.
|
||||
*/
|
||||
export function useActivateContact(
|
||||
props?: UseMutationOptions<void, Error, number>
|
||||
) {
|
||||
@@ -76,9 +67,6 @@ export function useActivateContact(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate the given contact.
|
||||
*/
|
||||
export function useInactivateContact(
|
||||
props?: UseMutationOptions<void, Error, number>
|
||||
) {
|
||||
|
||||
@@ -86,11 +86,7 @@ export function useBulkDeleteCustomers(
|
||||
|
||||
return useMutation({
|
||||
...props,
|
||||
mutationFn: ({
|
||||
ids,
|
||||
skipUndeletable = false,
|
||||
}: {
|
||||
ids: number[];
|
||||
mutationFn: ({ ids, skipUndeletable = false }: { ids: number[];
|
||||
skipUndeletable?: boolean;
|
||||
}) =>
|
||||
bulkDeleteCustomers(fetcher, {
|
||||
|
||||
@@ -12,6 +12,10 @@ import type {
|
||||
CreateSaleEstimateBody,
|
||||
EditSaleEstimateBody,
|
||||
SaleEstimateHtmlContentResponse,
|
||||
BulkDeleteEstimatesBody,
|
||||
ValidateBulkDeleteEstimatesResponse,
|
||||
SaleEstimatesStateResponse,
|
||||
SaleEstimateMailStateResponse,
|
||||
} from '@bigcapital/sdk-ts';
|
||||
import {
|
||||
fetchSaleEstimates,
|
||||
@@ -36,14 +40,6 @@ import { estimatesKeys, EstimatesQueryKeys } from './query-keys';
|
||||
import { itemsKeys } from '../items/query-keys';
|
||||
import { useRequestPdf } from '../../useRequestPdf';
|
||||
|
||||
export type BulkDeleteEstimatesBody = { ids: number[]; skipUndeletable?: boolean };
|
||||
export type ValidateBulkDeleteEstimatesResponse = {
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
};
|
||||
|
||||
// Keys that don't have factory methods yet - keeping inline
|
||||
const SETTING = 'SETTING';
|
||||
const SETTING_ESTIMATES = 'SETTING_ESTIMATES';
|
||||
@@ -264,41 +260,6 @@ export function useSendSaleEstimateMail(
|
||||
});
|
||||
}
|
||||
|
||||
export interface SaleEstimateMailStateResponse {
|
||||
attachEstimate: boolean;
|
||||
companyLogoUri: string;
|
||||
companyName: string;
|
||||
customerName: string;
|
||||
entries: Array<unknown>;
|
||||
estimateDate: string;
|
||||
estimateDateFormatted: string;
|
||||
expirationDate: string;
|
||||
expirationDateFormatted: string;
|
||||
primaryColor: string;
|
||||
total: number;
|
||||
totalFormatted: string;
|
||||
subtotal: number;
|
||||
subtotalFormatted: string;
|
||||
discountAmount: number;
|
||||
discountAmountFormatted: string;
|
||||
discountLabel: string;
|
||||
discountPercentage: number | null;
|
||||
discountPercentageFormatted: string;
|
||||
adjustment: number;
|
||||
adjustmentFormatted: string;
|
||||
estimateNumber: string;
|
||||
formatArgs: {
|
||||
customerName: string;
|
||||
estimateAmount: string;
|
||||
};
|
||||
from: Array<string>;
|
||||
fromOptions: Array<unknown>;
|
||||
message: string;
|
||||
subject: string;
|
||||
to: Array<string>;
|
||||
toOptions: Array<unknown>;
|
||||
}
|
||||
|
||||
export function useSaleEstimateMailState(
|
||||
estimateId: number,
|
||||
props?: UseQueryOptions<SaleEstimateMailStateResponse, Error>
|
||||
@@ -307,23 +268,19 @@ export function useSaleEstimateMailState(
|
||||
return useQuery({
|
||||
...props,
|
||||
queryKey: [EstimatesQueryKeys.SALE_ESTIMATE_MAIL_OPTIONS, estimateId],
|
||||
queryFn: () => fetchSaleEstimateMail(fetcher, estimateId) as Promise<SaleEstimateMailStateResponse>,
|
||||
queryFn: () => fetchSaleEstimateMail(fetcher, estimateId),
|
||||
});
|
||||
}
|
||||
|
||||
export interface ISaleEstimatesStateResponse {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
|
||||
export function useGetSaleEstimatesState(
|
||||
options?: UseQueryOptions<ISaleEstimatesStateResponse, Error>
|
||||
): UseQueryResult<ISaleEstimatesStateResponse, Error> {
|
||||
options?: UseQueryOptions<SaleEstimatesStateResponse, Error>
|
||||
): UseQueryResult<SaleEstimatesStateResponse, Error> {
|
||||
const fetcher = useApiFetcher({ enableCamelCaseTransform: true });
|
||||
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: ['SALE_ESTIMATE_STATE'],
|
||||
queryFn: () => fetchSaleEstimatesState(fetcher) as Promise<ISaleEstimatesStateResponse>,
|
||||
queryFn: () => fetchSaleEstimatesState(fetcher),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,6 @@ const commonInvalidateQueries = (queryClient) => {
|
||||
queryClient.invalidateQueries({ queryKey: inventoryAdjustmentsKeys.all() });
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the inventory adjustment to the given item.
|
||||
*/
|
||||
export function useCreateInventoryAdjustment(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const fetcher = useApiFetcher();
|
||||
@@ -31,9 +28,6 @@ export function useCreateInventoryAdjustment(props) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the inventory adjustment transaction.
|
||||
*/
|
||||
export function useDeleteInventoryAdjustment(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const fetcher = useApiFetcher();
|
||||
@@ -47,10 +41,6 @@ export function useDeleteInventoryAdjustment(props) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve inventory adjustment list with pagination meta.
|
||||
* Uses useRequestQuery because list endpoint query params may not be in OpenAPI.
|
||||
*/
|
||||
export function useInventoryAdjustments(query, props) {
|
||||
const fetcher = useApiFetcher();
|
||||
return useQuery({
|
||||
@@ -60,9 +50,6 @@ export function useInventoryAdjustments(query, props) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the given inventory adjustment.
|
||||
*/
|
||||
export function usePublishInventoryAdjustment(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const fetcher = useApiFetcher();
|
||||
@@ -77,10 +64,6 @@ export function usePublishInventoryAdjustment(props) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustment details.
|
||||
* @param {number} id - inventory adjustment id.
|
||||
*/
|
||||
export function useInventoryAdjustment(id, props) {
|
||||
const fetcher = useApiFetcher();
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@ import { acceptInvite, fetchInviteCheck, resendInvite } from '@bigcapital/sdk-ts
|
||||
import { useApiFetcher } from '../../useRequest';
|
||||
import { inviteKeys } from './query-keys';
|
||||
|
||||
/**
|
||||
* Authentication invite accept.
|
||||
*/
|
||||
export function useAuthInviteAccept(
|
||||
props?: UseMutationOptions<unknown, Error, [Record<string, unknown>, string]>
|
||||
) {
|
||||
@@ -18,9 +15,6 @@ export function useAuthInviteAccept(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invite meta by the given token.
|
||||
*/
|
||||
export function useInviteMetaByToken(
|
||||
token: string | null | undefined,
|
||||
props?: Omit<UseQueryOptions<unknown>, 'queryKey' | 'queryFn'>
|
||||
@@ -34,9 +28,6 @@ export function useInviteMetaByToken(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend invitation to user.
|
||||
*/
|
||||
export function useResendInvitation(
|
||||
props?: UseMutationOptions<void, Error, number>
|
||||
) {
|
||||
|
||||
@@ -24,11 +24,7 @@ import {
|
||||
editSettings,
|
||||
} from '@bigcapital/sdk-ts';
|
||||
import { useApiFetcher } from '../../useRequest';
|
||||
import {
|
||||
settingsKeys,
|
||||
SETTING,
|
||||
SETTING_SMS_NOTIFICATIONS,
|
||||
} from './query-keys';
|
||||
import { settingsKeys } from './query-keys';
|
||||
|
||||
export function useSaveSettings(
|
||||
props?: UseMutationOptions<void, Error, SaveSettingsBody>
|
||||
@@ -40,7 +36,7 @@ export function useSaveSettings(
|
||||
...props,
|
||||
mutationFn: (values: SaveSettingsBody) => editSettings(fetcher, values),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [SETTING] });
|
||||
queryClient.invalidateQueries({ queryKey: settingsKeys.all() });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -196,7 +192,7 @@ export function useSettingSMSNotification(
|
||||
|
||||
return useQuery({
|
||||
...props,
|
||||
queryKey: [SETTING_SMS_NOTIFICATIONS, key],
|
||||
queryKey: settingsKeys.smsNotification(key),
|
||||
queryFn: () => fetchSettingSMSNotification(fetcher, key),
|
||||
enabled: !!key,
|
||||
});
|
||||
@@ -213,7 +209,7 @@ export function useSettingEditSMSNotification(
|
||||
mutationFn: ({ key, values }: { key: string; values: Record<string, unknown> }) =>
|
||||
editSettingSMSNotification(fetcher, key, values),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [SETTING_SMS_NOTIFICATIONS] });
|
||||
queryClient.invalidateQueries({ queryKey: settingsKeys.smsNotifications() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Fetcher } from 'openapi-typescript-fetch';
|
||||
import type { paths } from './schema';
|
||||
import { createCamelCaseMiddleware } from './middleware/camel-case-middleware';
|
||||
import { createSnakeCaseRequestMiddleware } from './middleware/snake-case-request-middleware';
|
||||
|
||||
export type ApiFetcher = ReturnType<typeof Fetcher.for<paths>>;
|
||||
|
||||
export interface CreateApiFetcherConfig {
|
||||
baseUrl?: string;
|
||||
init?: RequestInit;
|
||||
/** Set to true to disable automatic snake_case to camelCase transformation */
|
||||
/** Set to true to disable automatic snake_case to camelCase transformation on responses */
|
||||
disableCamelCaseTransform?: boolean;
|
||||
/** Set to true to disable automatic camelCase to snake_case transformation on requests */
|
||||
disableSnakeCaseTransform?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,13 +25,17 @@ export function createApiFetcher(config?: CreateApiFetcherConfig): ApiFetcher {
|
||||
const parsedConfig = {
|
||||
baseUrl: '',
|
||||
disableCamelCaseTransform: true,
|
||||
disableSnakeCaseTransform: false,
|
||||
...config,
|
||||
};
|
||||
const fetcher = Fetcher.for<paths>();
|
||||
fetcher.configure({
|
||||
baseUrl: parsedConfig.baseUrl,
|
||||
init: parsedConfig?.init,
|
||||
use: parsedConfig.disableCamelCaseTransform ? [] : [createCamelCaseMiddleware()],
|
||||
use: [
|
||||
...(parsedConfig.disableSnakeCaseTransform ? [] : [createSnakeCaseRequestMiddleware()]),
|
||||
...(parsedConfig.disableCamelCaseTransform ? [] : [createCamelCaseMiddleware()]),
|
||||
],
|
||||
});
|
||||
return fetcher;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Middleware } from 'openapi-typescript-fetch';
|
||||
import { camelToSnakeCase, transformKeysToSnakeCase } from '../utils/case-transform';
|
||||
|
||||
export function createSnakeCaseRequestMiddleware(): Middleware {
|
||||
return async (url, init, next) => {
|
||||
// Transform query string keys
|
||||
const [base, search] = url.split('?');
|
||||
let transformedUrl = base;
|
||||
if (search) {
|
||||
const params = new URLSearchParams(search);
|
||||
const newParams = new URLSearchParams();
|
||||
for (const [key, value] of params.entries()) {
|
||||
newParams.append(camelToSnakeCase(key), value);
|
||||
}
|
||||
transformedUrl = `${base}?${newParams.toString()}`;
|
||||
}
|
||||
|
||||
// Transform JSON body keys
|
||||
let transformedInit = init;
|
||||
const contentType = (init.headers as Record<string, string>)?.['content-type'] ?? '';
|
||||
if (init.body && contentType.includes('application/json')) {
|
||||
const parsed = JSON.parse(init.body as string);
|
||||
transformedInit = {
|
||||
...init,
|
||||
body: JSON.stringify(transformKeysToSnakeCase(parsed)),
|
||||
};
|
||||
}
|
||||
|
||||
return next(transformedUrl, transformedInit);
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ApiFetcher } from './fetch-utils';
|
||||
import { rawRequest } from './fetch-utils';
|
||||
import { paths } from './schema';
|
||||
import { paths, components } from './schema';
|
||||
import { OpForPath, OpQueryParams, OpRequestBody, OpResponseBody } from './utils';
|
||||
|
||||
export const SALE_ESTIMATES_ROUTES = {
|
||||
@@ -23,6 +23,14 @@ export type CreateSaleEstimateBody = OpRequestBody<OpForPath<typeof SALE_ESTIMAT
|
||||
export type EditSaleEstimateBody = OpRequestBody<OpForPath<typeof SALE_ESTIMATES_ROUTES.BY_ID, 'put'>>;
|
||||
export type GetSaleEstimatesQuery = OpQueryParams<OpForPath<typeof SALE_ESTIMATES_ROUTES.LIST, 'get'>>;
|
||||
export type SaleEstimateHtmlContentResponse = { htmlContent: string };
|
||||
export type SaleEstimatesStateResponse = components['schemas']['SaleEstiamteStateResponseDto'];
|
||||
export type BulkDeleteEstimatesBody = { ids: number[]; skipUndeletable?: boolean };
|
||||
export type ValidateBulkDeleteEstimatesResponse = {
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
};
|
||||
|
||||
export async function fetchSaleEstimates(
|
||||
fetcher: ApiFetcher,
|
||||
@@ -61,13 +69,39 @@ export async function deleteSaleEstimate(fetcher: ApiFetcher, id: number): Promi
|
||||
await del({ id });
|
||||
}
|
||||
|
||||
export type BulkDeleteEstimatesBody = { ids: number[]; skipUndeletable?: boolean };
|
||||
export type ValidateBulkDeleteEstimatesResponse = {
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
};
|
||||
export interface SaleEstimateMailStateResponse {
|
||||
from: string[];
|
||||
to: string[];
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
subject: string;
|
||||
message: string;
|
||||
formatArgs?: { customerName: string; estimateAmount: string };
|
||||
toOptions: Array<{ label: string; mail: string; primary?: boolean }>;
|
||||
fromOptions: Array<{ label: string; mail: string; primary?: boolean }>;
|
||||
attachEstimate?: boolean;
|
||||
estimateDate: string;
|
||||
estimateDateFormatted: string;
|
||||
expirationDate: string;
|
||||
expirationDateFormatted: string;
|
||||
total: number;
|
||||
totalFormatted: string;
|
||||
subtotal: number;
|
||||
subtotalFormatted: string;
|
||||
discountAmount: number;
|
||||
discountAmountFormatted: string;
|
||||
discountPercentage: number | null;
|
||||
discountPercentageFormatted: string;
|
||||
discountLabel: string;
|
||||
adjustment: number;
|
||||
adjustmentFormatted: string;
|
||||
estimateNumber: string;
|
||||
entries: Array<{ name: string; quantity: number; unitPrice: number; unitPriceFormatted: string; total: number; totalFormatted: string }>;
|
||||
companyName: string;
|
||||
companyLogoUri: string | null;
|
||||
primaryColor: string | null;
|
||||
customerName: string;
|
||||
}
|
||||
|
||||
export async function bulkDeleteSaleEstimates(
|
||||
fetcher: ApiFetcher,
|
||||
@@ -119,10 +153,10 @@ export async function fetchSaleEstimateSmsDetails(
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchSaleEstimateMail(fetcher: ApiFetcher, id: number): Promise<unknown> {
|
||||
export async function fetchSaleEstimateMail(fetcher: ApiFetcher, id: number): Promise<SaleEstimateMailStateResponse> {
|
||||
const get = fetcher.path(SALE_ESTIMATES_ROUTES.MAIL).method('get').create();
|
||||
const { data } = await get({ id });
|
||||
return data;
|
||||
return data as SaleEstimateMailStateResponse;
|
||||
}
|
||||
|
||||
export async function sendSaleEstimateMail(
|
||||
@@ -134,10 +168,10 @@ export async function sendSaleEstimateMail(
|
||||
await post({ id, ...(body ?? {}) } as never);
|
||||
}
|
||||
|
||||
export async function fetchSaleEstimatesState(fetcher: ApiFetcher): Promise<unknown> {
|
||||
export async function fetchSaleEstimatesState(fetcher: ApiFetcher): Promise<SaleEstimatesStateResponse> {
|
||||
const get = fetcher.path(SALE_ESTIMATES_ROUTES.STATE).method('get').create();
|
||||
const { data } = await get({});
|
||||
return data;
|
||||
return data as SaleEstimatesStateResponse;
|
||||
}
|
||||
|
||||
export async function fetchSaleEstimateHtmlContent(
|
||||
|
||||
@@ -74,3 +74,61 @@ export function transformKeysToCamelCase<T>(value: unknown, cache?: TransformCac
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a camelCase string to snake_case.
|
||||
*/
|
||||
export function camelToSnakeCase(str: string): string {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply transforms all keys in an object from camelCase to snake_case.
|
||||
* Handles nested objects, arrays, null, undefined, and primitive values.
|
||||
* Uses WeakMap caching to handle circular references safely.
|
||||
*/
|
||||
export function transformKeysToSnakeCase<T>(value: unknown, cache?: TransformCache): T {
|
||||
if (value === null || value === undefined) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
if (typeof value !== 'object') {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
if (value instanceof Blob) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
const localCache = cache ?? new WeakMap();
|
||||
|
||||
if (localCache.has(value as object)) {
|
||||
return localCache.get(value as object);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const result: unknown[] = [];
|
||||
localCache.set(value, result);
|
||||
for (const item of value) {
|
||||
result.push(transformKeysToSnakeCase(item, localCache));
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
localCache.set(value as object, result);
|
||||
|
||||
for (const key in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
const snakeKey = camelToSnakeCase(key);
|
||||
const itemValue = (value as Record<string, unknown>)[key];
|
||||
result[snakeKey] = transformKeysToSnakeCase(itemValue, localCache);
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user