1
0
Files
bigcapital/packages/webapp/src/hooks/query/invoices/queries.ts
T
Ahmed Bouhuolia d53271325c wip
2026-03-19 05:52:04 +02:00

499 lines
14 KiB
TypeScript

import {
useQueryClient,
useMutation,
useQuery,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query';
import type {
SaleInvoicesListResponse,
SaleInvoice,
CreateSaleInvoiceBody,
EditSaleInvoiceBody,
GetSaleInvoicesQuery,
ValidateBulkDeleteSaleInvoicesResponse,
SaleInvoiceStateResponse,
} from '@bigcapital/sdk-ts';
import {
fetchSaleInvoices,
fetchSaleInvoice,
createSaleInvoice,
editSaleInvoice,
deleteSaleInvoice,
bulkDeleteSaleInvoices,
validateBulkDeleteSaleInvoices,
deliverSaleInvoice,
writeOffSaleInvoice,
cancelWrittenOffSaleInvoice,
fetchReceivableSaleInvoices,
fetchSaleInvoiceMailState,
sendSaleInvoiceMail,
fetchSaleInvoiceState,
fetchInvoicePayments,
fetchSaleInvoiceHtml,
} from '@bigcapital/sdk-ts';
import { useApiFetcher } from '../../useRequest';
import { useRequestQuery } from '../../useQueryRequest';
import { transformToCamelCase } from '@/utils';
import useApiRequest from '../../useRequest';
import { useRequestPdf } from '../../useRequestPdf';
import { invoicesKeys, InvoicesQueryKeys } from './query-keys';
import { customersKeys } from '../customers/query-keys';
import { itemsKeys } from '../items/query-keys';
import { accountsKeys } from '../accounts/query-keys';
import { estimatesKeys } from '../estimates/query-keys';
import { organizationKeys } from '../organization/query-keys';
// Keys that don't have factory methods yet - keeping inline
const FINANCIAL_REPORT = 'FINANCIAL-REPORT';
const TRANSACTIONS_BY_REFERENCE = 'TRANSACTIONS_BY_REFERENCE';
const RECONCILE_CREDIT_NOTE = 'RECONCILE_CREDIT_NOTE';
const RECONCILE_CREDIT_NOTES = 'RECONCILE_CREDIT_NOTES';
const SETTING = 'SETTING';
const SETTING_INVOICES = 'SETTING_INVOICES';
function commonInvalidateQueries(queryClient: ReturnType<typeof useQueryClient>) {
queryClient.invalidateQueries({ queryKey: invoicesKeys.all() });
queryClient.invalidateQueries({ queryKey: customersKeys.all() });
queryClient.invalidateQueries({ queryKey: itemsKeys.all() });
queryClient.invalidateQueries({ queryKey: [SETTING, SETTING_INVOICES] });
queryClient.invalidateQueries({ queryKey: [FINANCIAL_REPORT] });
queryClient.invalidateQueries({ queryKey: [TRANSACTIONS_BY_REFERENCE] });
queryClient.invalidateQueries({ queryKey: accountsKeys.all() });
queryClient.invalidateQueries({ queryKey: [RECONCILE_CREDIT_NOTE] });
queryClient.invalidateQueries({ queryKey: [RECONCILE_CREDIT_NOTES] });
queryClient.invalidateQueries({ queryKey: organizationKeys.mutateAbilities() });
}
export function useCreateInvoice(
props?: UseMutationOptions<void, Error, CreateSaleInvoiceBody>
) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: (values: CreateSaleInvoiceBody) => createSaleInvoice(fetcher, values),
onSuccess: (_data, values) => {
const customerId = values.customerId as unknown as number;
queryClient.invalidateQueries({ queryKey: customersKeys.detail(customerId) });
queryClient.invalidateQueries({ queryKey: estimatesKeys.all() });
commonInvalidateQueries(queryClient);
},
});
}
export function useEditInvoice(
props?: UseMutationOptions<void, Error, [number, EditSaleInvoiceBody]>
) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: ([id, values]: [number, EditSaleInvoiceBody]) =>
editSaleInvoice(fetcher, id, values),
onSuccess: (_data, [id, values]) => {
const customerId = values.customerId as unknown as number;
queryClient.invalidateQueries({ queryKey: invoicesKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: customersKeys.detail(customerId) });
commonInvalidateQueries(queryClient);
},
});
}
export function useDeleteInvoice(props?: UseMutationOptions<void, Error, number>) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: (id: number) => deleteSaleInvoice(fetcher, id),
onSuccess: (_data, id) => {
queryClient.invalidateQueries({ queryKey: invoicesKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: estimatesKeys.all() });
commonInvalidateQueries(queryClient);
},
});
}
export function useBulkDeleteInvoices(
props?: UseMutationOptions<
void,
Error,
{ ids: number[]; skipUndeletable?: boolean }
>
) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: ({
ids,
skipUndeletable = false,
}: {
ids: number[];
skipUndeletable?: boolean;
}) => bulkDeleteSaleInvoices(fetcher, { ids, skipUndeletable }),
onSuccess: () => commonInvalidateQueries(queryClient),
});
}
export function useValidateBulkDeleteInvoices(
props?: UseMutationOptions<ValidateBulkDeleteSaleInvoicesResponse, Error, number[]>
) {
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: (ids: number[]) => validateBulkDeleteSaleInvoices(fetcher, ids),
});
}
export function useInvoices(
query?: GetSaleInvoicesQuery,
props?: UseQueryOptions<SaleInvoicesListResponse, Error>
) {
const fetcher = useApiFetcher();
return useQuery({
...props,
queryKey: invoicesKeys.list(query),
queryFn: () => fetchSaleInvoices(fetcher, query),
});
}
export function useDeliverInvoice(props?: UseMutationOptions<void, Error, number>) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: (invoiceId: number) => deliverSaleInvoice(fetcher, invoiceId),
onSuccess: (_data, invoiceId) => {
queryClient.invalidateQueries({ queryKey: invoicesKeys.detail(invoiceId) });
commonInvalidateQueries(queryClient);
},
});
}
export function useInvoice(
invoiceId: number | null | undefined,
props?: UseQueryOptions<SaleInvoice, Error>,
) {
const fetcher = useApiFetcher();
return useQuery({
...props,
queryKey: invoicesKeys.detail(invoiceId),
queryFn: () => fetchSaleInvoice(fetcher, invoiceId as number),
enabled: invoiceId != null,
});
}
/**
* Retrieve the invoice pdf document data.
*/
export function usePdfInvoice(invoiceId: number) {
return useRequestPdf({
url: `sale-invoices/${invoiceId}`,
});
}
export function useInvoiceHtml(
invoiceId: number,
options?: UseQueryOptions<SaleInvoiceHtmlContentResponse, Error>
): UseQueryResult<GetInvoiceHtmlResponse, Error> {
const fetcher = useApiFetcher({ enableCamelCaseTransform: true });
return useQuery({
...options,
queryKey: ['SALE_INVOICE_HTML', invoiceId],
queryFn: () => fetchSaleInvoiceHtml(fetcher, invoiceId),
});
}
export function useDueInvoices(
customerId: number | null | undefined,
props?: UseQueryOptions<unknown, Error>
) {
const fetcher = useApiFetcher();
return useQuery({
...props,
queryKey: invoicesKeys.due(customerId),
queryFn: () => fetchReceivableSaleInvoices(fetcher, customerId ?? undefined),
enabled: customerId != null,
});
}
export function useRefreshInvoices() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries({ queryKey: invoicesKeys.all() });
},
};
}
export function useCreateBadDebt(
props?: UseMutationOptions<void, Error, [number, Record<string, unknown>]>
) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: ([id, values]: [number, Record<string, unknown>]) =>
writeOffSaleInvoice(fetcher, id, values),
onSuccess: (_data, [id]) => {
queryClient.invalidateQueries({ queryKey: [InvoicesQueryKeys.BAD_DEBT, id] });
commonInvalidateQueries(queryClient);
},
});
}
export function useCancelBadDebt(props?: UseMutationOptions<void, Error, number>) {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...props,
mutationFn: (id: number) => cancelWrittenOffSaleInvoice(fetcher, id),
onSuccess: (_data, id) => {
queryClient.invalidateQueries({ queryKey: [InvoicesQueryKeys.CANCEL_BAD_DEBT, id] });
commonInvalidateQueries(queryClient);
},
});
}
// Not in OpenAPI schema for sale-invoices; keep using apiRequest.
export function useCreateNotifyInvoiceBySMS(
props?: UseMutationOptions<unknown, Error, [number, Record<string, unknown>]>
) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation({
...props,
mutationFn: ([id, values]: [number, Record<string, unknown>]) =>
apiRequest.post(`sale-invoices/${id}/notify-by-sms`, values, {}),
onSuccess: (_data, [id]) => {
queryClient.invalidateQueries({ queryKey: [InvoicesQueryKeys.NOTIFY_SALE_INVOICE_BY_SMS, id] });
commonInvalidateQueries(queryClient);
},
});
}
// Not in OpenAPI schema for sale-invoices; keep using useRequestQuery.
export function useInvoiceSMSDetail(
invoiceId: number,
query?: Record<string, unknown>,
props?: Record<string, unknown>
) {
return useRequestQuery(
[...invoicesKeys.smsDetail(invoiceId), query],
{
method: 'get',
url: `sale-invoices/${invoiceId}/sms-details`,
params: query,
} as { method: string; url: string; params?: Record<string, unknown> },
{
select: (res: { data: unknown }) => res.data,
defaultData: {},
...props,
}
);
}
export function useInvoicePaymentTransactions(
invoiceId: number,
props?: UseQueryOptions<unknown, Error>
) {
const fetcher = useApiFetcher();
return useQuery({
...props,
queryKey: invoicesKeys.paymentTransactions(invoiceId),
queryFn: () => fetchInvoicePayments(fetcher, invoiceId),
});
}
// # Send sale invoice mail.
export interface SendSaleInvoiceMailValues {
id: number;
values: {
subject: string;
message: string;
to: Array<string>;
cc?: Array<string>;
bcc?: Array<string>;
attachInvoice?: boolean;
};
}
export type SendSaleInvoiceMailResponse = void;
export function useSendSaleInvoiceMail(
options?: UseMutationOptions<
SendSaleInvoiceMailResponse,
Error,
SendSaleInvoiceMailValues
>,
): UseMutationResult<
SendSaleInvoiceMailResponse,
Error,
SendSaleInvoiceMailValues
> {
const queryClient = useQueryClient();
const fetcher = useApiFetcher();
return useMutation({
...options,
mutationFn: (value: SendSaleInvoiceMailValues) =>
sendSaleInvoiceMail(fetcher, value.id, value.values),
onSuccess: () => commonInvalidateQueries(queryClient),
});
}
// # Get sale invoice mail state.
export interface GetSaleInvoiceDefaultOptionsResponse {
companyName: string;
companyLogoUri: string;
customerName: string;
dueDate: string;
dueDateFormatted: string;
dueAmount: number;
dueAmountFormatted: string;
entries: Array<{
quantity: number;
quantityFormatted: string;
rate: number;
rateFormatted: string;
total: number;
totalFormatted: string;
}>;
formatArgs: Record<string, string>;
from: string[];
to: string[];
invoiceDate: string;
invoiceDateFormatted: string;
invoiceNo: string;
message: string;
subject: string;
subtotal: number;
subtotalFormatted: string;
discountAmount: number;
discountAmountFormatted: string;
discountLabel: string;
discountPercentage: number;
discountPercentageFormatted: string;
adjustment: number;
adjustmentFormatted: string;
total: number;
totalFormatted: string;
attachInvoice: boolean;
primaryColor: string;
}
export function useSaleInvoiceMailState(
invoiceId: number,
options?: UseQueryOptions<GetSaleInvoiceDefaultOptionsResponse, Error>
): UseQueryResult<GetSaleInvoiceDefaultOptionsResponse, Error> {
const fetcher = useApiFetcher({ enableCamelCaseTransform: true });
return useQuery({
...options,
queryKey: [InvoicesQueryKeys.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
queryFn: () => fetchSaleInvoiceMailState(fetcher, invoiceId) as Promise<GetSaleInvoiceDefaultOptionsResponse>,
});
}
// # Get sale invoice state.
export type GetSaleInvoiceStateResponse = SaleInvoiceStateResponse;
export function useGetSaleInvoiceState(
options?: UseQueryOptions<GetSaleInvoiceStateResponse, Error>
): UseQueryResult<GetSaleInvoiceStateResponse, Error> {
const fetcher = useApiFetcher({ enableCamelCaseTransform: true });
return useQuery({
...options,
queryKey: ['SALE_INVOICE_STATE'],
queryFn: () =>
fetchSaleInvoiceState(fetcher).then((data: GetSaleInvoiceStateResponse & { data?: unknown }) =>
(data?.data ?? data) as GetSaleInvoiceStateResponse
),
});
}
// # Get sale invoice branding template — not in OpenAPI schema; keep apiRequest.
export interface GetSaleInvoiceBrandingTemplateResponse {
id: number;
default: number;
predefined: number;
resource: string;
resourceFormatted: string;
templateName: string;
updatedAt: string;
createdAt: string;
createdAtFormatted: string;
attributes: {
billedToLabel?: string;
companyLogoKey?: string | null;
companyLogoUri?: string;
dateIssueLabel?: string;
discountLabel?: string;
dueAmountLabel?: string;
dueDateLabel?: string;
invoiceNumberLabel?: string;
itemDescriptionLabel?: string;
itemNameLabel?: string;
itemRateLabel?: string;
itemTotalLabel?: string;
paymentMadeLabel?: string;
primaryColor?: string;
secondaryColor?: string;
showCompanyAddress?: boolean;
showCompanyLogo?: boolean;
showCustomerAddress?: boolean;
showDateIssue?: boolean;
showDiscount?: boolean;
showDueAmount?: boolean;
showDueDate?: boolean;
showInvoiceNumber?: boolean;
showPaymentMade?: boolean;
showStatement?: boolean;
showSubtotal?: boolean;
showTaxes?: boolean;
showTermsConditions?: boolean;
showTotal?: boolean;
statementLabel?: string;
subtotalLabel?: string;
termsConditionsLabel?: string;
totalLabel?: string;
};
}
export function useGetSaleInvoiceBrandingTemplate(
invoiceId: number,
options?: UseQueryOptions<GetSaleInvoiceBrandingTemplateResponse, Error>
): UseQueryResult<GetSaleInvoiceBrandingTemplateResponse, Error> {
const apiRequest = useApiRequest();
return useQuery({
...options,
queryKey: ['SALE_INVOICE_BRANDING_TEMPLATE', invoiceId],
queryFn: () =>
apiRequest
.get(`/sale-invoices/${invoiceId}/template`, {})
.then((res: { data?: { data?: unknown } }) =>
transformToCamelCase(res.data?.data) as GetSaleInvoiceBrandingTemplateResponse
),
});
}