This commit is contained in:
David Arranz 2026-04-05 17:52:38 +02:00
parent 7cc601ef18
commit cef7536f27
26 changed files with 123 additions and 384 deletions

View File

@ -1,55 +1,7 @@
import {
AmountSchema,
MetadataSchema,
MoneySchema,
PercentageSchema,
QuantitySchema,
} from "@erp/core";
import { z } from "zod/v4";
type GetProformaByIdResponseDTO,
GetProformaByIdResponseSchema,
} from "./get-proforma-by-id.response.dto";
export const CreateProformaResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
invoice_number: z.string(),
status: z.string(),
series: z.string(),
invoice_date: z.string(),
operation_date: z.string(),
notes: z.string(),
language_code: z.string(),
currency_code: z.string(),
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
items: z.array(
z.object({
id: z.uuid(),
position: z.string(),
description: z.string(),
quantity: QuantitySchema,
unit_amount: AmountSchema,
taxes: z.string(),
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
})
),
metadata: MetadataSchema.optional(),
});
export type CreateProformaResponseDTO = z.infer<typeof CreateProformaResponseSchema>;
export const CreateProformaResponseSchema = GetProformaByIdResponseSchema;
export type CreateProformaResponseDTO = GetProformaByIdResponseDTO;

View File

@ -38,8 +38,8 @@ import { useFieldArray, useForm } from "react-hook-form";
import * as z from "zod";
import { useTranslation } from "../../i18n";
import { CustomerInvoicePricesCard } from "../../proformas/shared2/ui/components";
import { CustomerInvoiceItemsCardEditor } from "../../proformas/shared2/ui/components/items";
import { CustomerInvoicePricesCard } from "../../proformas/shared/ui/components";
import { CustomerInvoiceItemsCardEditor } from "../../proformas/shared/ui/components/items";
import type { CustomerInvoiceData } from "./customer-invoice.schema";
import { formatCurrency } from "./utils";

View File

@ -2,7 +2,7 @@ import type { CriteriaDTO } from "@erp/core";
import { useDebounce } from "@repo/rdx-ui/components";
import { useMemo, useState } from "react";
import { useListProformasQuery } from "../../shared2";
import { useProformasListQuery } from "../../shared2";
export const useListProformasController = () => {
const [pageIndex, setPageIndex] = useState(0);
@ -26,7 +26,7 @@ export const useListProformasController = () => {
};
}, [pageSize, pageIndex, debouncedQ, status]);
const query = useListProformasQuery({ criteria });
const query = useProformasListQuery({ criteria });
const setSearchValue = (value: string) => {
setSearch(value.trim().replace(/\s+/g, " "));

View File

@ -1,6 +1,7 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { GetProformaByIdResponseDTO } from "../../../../common";
import type { CreateProformaResult, UpdateProformaByIdResult } from "../api";
import type {
Proforma,
ProformaItem,
@ -10,7 +11,9 @@ import type {
} from "../entities";
export const GetProformaByIdAdapter = {
fromDto(dto: GetProformaByIdResponseDTO): Proforma {
fromDto(
dto: GetProformaByIdResponseDTO | CreateProformaResult | UpdateProformaByIdResult
): Proforma {
return {
id: dto.id,
companyId: dto.company_id,

View File

@ -1,5 +1,8 @@
export * from "./keys";
export * from "./use-proforma-create-mutation";
export * from "./use-proforma-delete-mutation";
export * from "./use-proforma-get-query";
export * from "./use-proforma-update-mutation";
export * from "./use-proformas-list-query";
:

View File

@ -3,11 +3,14 @@ import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
import { ChangeStatusProformaByIdRequestSchema } from "../../../../common";
import { type ChangeProformaStatusByIdParams, changeProformaStatusById } from "../api";
import type { Proforma } from "../entities";
import {
type ChangeProformaStatusByIdParams,
type ChangeProformaStatusByIdResult,
changeProformaStatusById,
} from "../api";
import { CHANGE_STATUS_PROFORMA_MUTATION_KEY } from "./keys";
import { syncChangedProformaStatusCaches } from "./proforma-cache-strategy";
import { invalidateProformaListQueries } from "./proforma-cache-strategy";
import { toValidationErrors } from "./to-validation-errors";
type ChangeProformaStatusContext = {};
@ -17,7 +20,12 @@ export const useProformaChangeStatusMutation = () => {
const dataSource = useDataSource();
const schema = ChangeStatusProformaByIdRequestSchema;
useMutation<Proforma, DefaultError, ChangeProformaStatusByIdParams, ChangeProformaStatusContext>({
useMutation<
ChangeProformaStatusByIdResult,
DefaultError,
ChangeProformaStatusByIdParams,
ChangeProformaStatusContext
>({
mutationKey: CHANGE_STATUS_PROFORMA_MUTATION_KEY,
mutationFn: async (params) => {
@ -31,11 +39,10 @@ export const useProformaChangeStatusMutation = () => {
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
}
const dto = await changeProformaStatusById(dataSource, params);
return ChangeStatusProformaByIdAdapter.fromDto(dto);
return await changeProformaStatusById(dataSource, params);
},
onSuccess: async (proforma) => {
await syncChangedProformaStatusCaches(queryClient, proforma);
onSuccess: async () => {
await invalidateProformaListQueries(queryClient);
},
});
};

View File

@ -0,0 +1,42 @@
import { useDataSource } from "@erp/core/hooks";
import { CreateProformaRequestSchema } from "@erp/customer-invoices/common";
import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
import { GetProformaByIdAdapter } from "../adapters";
import { type CreateProformaParams, type CreateProformaResult, createProforma } from "../api";
import type { Proforma } from "../entities";
import { CREATE_PROFORMA_MUTATION_KEY } from "./keys";
import {
invalidateProformaListQueries,
syncCreatedProformaCaches,
} from "./proforma-cache-strategy";
import { toValidationErrors } from "./to-validation-errors";
type CreateProformaContext = {};
export const useProformaCreateMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const schema = CreateProformaRequestSchema;
return useMutation<Proforma, DefaultError, CreateProformaParams, CreateProformaContext>({
mutationKey: CREATE_PROFORMA_MUTATION_KEY,
mutationFn: async (params) => {
const result = schema.safeParse(params);
if (!result.success) {
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
}
const dto: CreateProformaResult = await createProforma(dataSource, params);
return GetProformaByIdAdapter.fromDto(dto);
},
onSuccess: async (proforma) => {
await syncCreatedProformaCaches(queryClient, proforma);
},
onSettled: async () => {
await invalidateProformaListQueries(queryClient);
},
});
};

View File

@ -1,5 +1,5 @@
import { useDataSource } from "@erp/core/hooks";
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { GetProformaByIdAdapter } from "../adapters";
import { getProformaById } from "../api";
@ -14,7 +14,7 @@ export interface UseProformaGetQueryOptions {
export const useProformaGetQuery = (
options: UseProformaGetQueryOptions
): UseQueryResult<Proforma, Error> => {
): UseQueryResult<Proforma, DefaultError> => {
const dataSource = useDataSource();
const id = options?.id;
const enabled = options?.enabled ?? Boolean(id);
@ -30,6 +30,6 @@ export const useProformaGetQuery = (
});
return GetProformaByIdAdapter.fromDto(dto);
},
enabled: enabled && Boolean(id),
enabled,
});
};

View File

@ -1,29 +1,39 @@
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
import { ChangeStatusProformaByIdRequestSchema } from "../../../../common";
import { type IssueProformaByIdParams, issueProformaById } from "../api";
import type { Proforma } from "../entities";
import {
type IssueProformaByIdParams,
type IssueProformaByIdResult,
issueProformaById,
} from "../api";
import { ISSUE_PROFORMA_MUTATION_KEY } from "./keys";
import { syncIssuedProformaCaches } from "./proforma-cache-strategy";
import { invalidateProformaListQueries } from "./proforma-cache-strategy";
type IssueProformaByIdContext = {};
export const useProformaChangeStatusMutation = () => {
export const useProformaIssueMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const schema = ChangeStatusProformaByIdRequestSchema;
useMutation<Proforma, Error, IssueProformaByIdParams, IssueProformaByIdContext>({
useMutation<
IssueProformaByIdResult,
DefaultError,
IssueProformaByIdParams,
IssueProformaByIdContext
>({
mutationKey: ISSUE_PROFORMA_MUTATION_KEY,
mutationFn: async (params) => {
const dto = await issueProformaById(dataSource, params);
return IssueProformaByIdAdapter.fromDto(dto);
const { id: proformaId } = params;
if (!proformaId) {
throw new Error("proformaId is required");
}
return issueProformaById(dataSource, params);
},
onSuccess: async (proforma) => {
await syncIssuedProformaCaches(queryClient, proforma);
onSuccess: async () => {
await invalidateProformaListQueries(queryClient);
},
});
};

View File

@ -9,7 +9,10 @@ import { updateProformaById } from "../api";
import type { Proforma } from "../entities";
import { UPDATE_PROFORMA_MUTATION_KEY } from "./keys";
import { syncUpdatedProformaCaches } from "./proforma-cache-strategy";
import {
invalidateProformaListQueries,
syncUpdatedProformaCaches,
} from "./proforma-cache-strategy";
import { toValidationErrors } from "./to-validation-errors";
type UpdateProformaContext = {};
@ -42,5 +45,8 @@ export const useProformaUpdateMutation = () => {
onSuccess: async (proforma) => {
await syncUpdatedProformaCaches(queryClient, proforma);
},
onSettled: async () => {
await invalidateProformaListQueries(queryClient);
},
});
};

View File

@ -3,14 +3,14 @@ import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { ListProformasAdapter } from "../adapters";
import { type ListProformasResult, getListProformasByCriteria } from "../api";
import { getListProformasByCriteria } from "../api";
import type { ProformaList } from "../entities";
import { LIST_PROFORMAS_QUERY_KEY } from "./keys";
export interface ProformasListQueryOptions {
criteria?: Partial<CriteriaDTO>;
enabled?: boolean;
criteria?: Partial<CriteriaDTO>;
}
export const useProformasListQuery = (
@ -23,10 +23,7 @@ export const useProformasListQuery = (
return useQuery<ProformaList, DefaultError>({
queryKey: LIST_PROFORMAS_QUERY_KEY(criteria),
queryFn: async ({ signal }) => {
const dto: ListProformasResult = await getListProformasByCriteria(dataSource, {
criteria,
signal,
});
const dto = await getListProformasByCriteria(dataSource, { signal, criteria });
return ListProformasAdapter.fromDto(dto);
},
enabled,

View File

@ -0,0 +1,6 @@
export * from "./adapters";
export * from "./api";
export * from "./constants";
export * from "./entities";
export * from "./hooks";
export * from "./ui";

View File

@ -1,3 +0,0 @@
export * from "./use-change-proforma-status-mutation";
export * from "./use-delete-proforma-mutation";
export * from "./use-list-proformas-query";

View File

@ -1,33 +0,0 @@
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { changeProformaStatusById } from "../../shared/api/change-proforma-status-by-id.api";
import type { PROFORMA_STATUS } from "../entities";
import { LIST_PROFORMAS_QUERY_KEY_PREFIX } from "./use-list-proformas-query";
interface ChangeProformaStatusPayload {
proformaId: string;
newStatus: PROFORMA_STATUS;
}
export function useChangeProformaStatusMutation() {
const dataSource = useDataSource();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ proformaId, newStatus }: ChangeProformaStatusPayload) =>
changeProformaStatusById(dataSource, proformaId, newStatus),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
},
});
return {
changeProformaStatus: mutation.mutateAsync,
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset,
};
}

View File

@ -1,31 +0,0 @@
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteProformaById } from "../api";
import { LIST_PROFORMAS_QUERY_KEY_PREFIX } from "./use-list-proformas-query";
interface DeleteProformaPayload {
proformaId: string;
}
export function useDeleteProformaMutation() {
const dataSource = useDataSource();
const queryClient = useQueryClient();
const mutation = useMutation<void, Error, DeleteProformaPayload>({
mutationFn: ({ proformaId }: DeleteProformaPayload) =>
deleteProformaById(dataSource, proformaId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
},
});
return {
deleteProforma: mutation.mutateAsync,
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset,
};
}

View File

@ -1,114 +0,0 @@
import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
import {
type DefaultError,
type QueryClient,
type QueryKey,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { ListProformasAdapter } from "../adapters";
import { getListProformas } from "../api";
import type { ProformaList, ProformaListRow } from "../entities";
export const LIST_PROFORMAS_QUERY_KEY_PREFIX = ["proformas"] as const;
export const LIST_PROFORMAS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
LIST_PROFORMAS_QUERY_KEY_PREFIX,
{
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
];
type useListProformasQueryOptions = {
enabled?: boolean;
criteria?: Partial<CriteriaDTO>;
};
export const useListProformasQuery = (
options?: useListProformasQueryOptions
): UseQueryResult<ProformaList, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {};
return useQuery<ProformaList, DefaultError>({
queryKey: LIST_PROFORMAS_QUERY_KEY(criteria),
queryFn: async ({ signal }) => {
const dto = await getListProformas(dataSource, signal, criteria);
return ListProformasAdapter.fromDTO(dto);
},
enabled,
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
});
};
export function cancelListProformasQueries(qc: QueryClient) {
return qc.cancelQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
}
export function invalidateListProformasQueries(qc: QueryClient) {
return qc.invalidateQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
}
export function getAllListProformasQueryKeys(qc: QueryClient): QueryKey[] {
const entries = qc.getQueriesData<ProformaList>({
queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX,
});
return entries.map(([key]) => key);
}
export function upsertProformaInListProformasCaches(
qc: QueryClient,
proforma: Pick<ProformaListRow, "id"> & Partial<ProformaListRow>
) {
const keys = getAllListProformasQueryKeys(qc);
for (const key of keys) {
const page = qc.getQueryData<ProformaList>(key);
if (!page) continue;
const index = page.items.findIndex((row) => row.id === proforma.id);
if (index === -1) continue;
const nextItems = page.items.slice();
nextItems[index] = {
...page.items[index],
...proforma,
};
qc.setQueryData<ProformaList>(key, {
...page,
items: nextItems,
});
}
}
export function removeProformaFromListProformasCaches(
qc: QueryClient,
proformaId: string
): Array<{ key: QueryKey; page?: ProformaList }> {
const snapshots = getAllListProformasQueryKeys(qc).map((key) => ({
key,
page: qc.getQueryData<ProformaList>(key),
}));
for (const { key, page } of snapshots) {
if (!page) continue;
qc.setQueryData<ProformaList>(key, {
...page,
items: page.items.filter((row) => row.id !== proformaId),
total_items: Math.max(0, page.total_items - 1),
});
}
return snapshots;
}

View File

@ -1,47 +0,0 @@
import { useDataSource } from "@erp/core/hooks";
import {
type DefaultError,
type QueryKey,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { type Proforma, getProformaById } from "../../api";
export const PROFORMA_QUERY_KEY = (proformaId?: string): QueryKey => [
"proformas:detail",
{
proformaId,
},
];
type ProformaQueryOptions = {
enabled?: boolean;
};
export const useProformaGetQuery = (
proformaId?: string,
options?: ProformaQueryOptions
): UseQueryResult<Proforma, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? Boolean(proformaId);
return useQuery<Proforma, DefaultError>({
queryKey: PROFORMA_QUERY_KEY(proformaId),
queryFn: async ({ signal }) => getProformaById(dataSource, signal, proformaId),
enabled,
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
});
};
/*export function invalidateProformaDetailCache(qc: QueryClient, id: string) {
return qc.invalidateQueries({
queryKey: getProformaQueryKey(id ?? "unknown"),
exact: Boolean(id),
});
}
export function setProformaDetailCache(qc: QueryClient, id: string, data: unknown) {
qc.setQueryData(getProformaQueryKey(id), data);
}
*/

View File

@ -1,58 +0,0 @@
import { useDataSource } from "@erp/core/hooks";
import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UpdateProformaByIdRequestSchema } from "../../../../common";
import type { ProformaFormData } from "../../update/types";
export const PROFORMA_UPDATE_KEY = ["proformas", "update"] as const;
type UpdateProformaContext = {};
type UpdateProformaPayload = {
id: string;
data: Partial<ProformaFormData>;
};
export const useProformaUpdateMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const schema = UpdateProformaByIdRequestSchema;
return useMutation<Proforma, DefaultError, UpdateProformaPayload, UpdateProformaContext>({
mutationKey: PROFORMA_UPDATE_KEY,
mutationFn: async (payload) => {
const { id: proformaId, data } = payload;
if (!proformaId) {
throw new Error("proformaId is required");
}
const result = schema.safeParse(data);
if (!result.success) {
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
}
const updated = await dataSource.updateOne("proformas", proformaId, data);
return updated as Proforma;
},
onSuccess: (updated: Proforma, variables) => {
const { id: proformaId } = updated;
// Invalida el listado para refrescar desde servidor
//invalidateProformaListCache(queryClient);
// Actualiza detalle
//setProformaDetailCache(queryClient, proformaId, updated);
// Actualiza todas las páginas donde aparezca
//upsertProformaIntoListCaches(queryClient, { ...updated });
},
onSettled: () => {
// Refresca todos los listados
//invalidateProformaListCache(queryClient);
},
});
};

View File

@ -1,4 +0,0 @@
export * from "./api";
export * from "./entities";
export * from "./hooks";
export * from "./ui";

View File

@ -8,10 +8,10 @@ import type { CustomerList } from "../entities";
import { LIST_CUSTOMERS_QUERY_KEY } from "./keys";
type CustomersListQueryOptions = {
export interface CustomersListQueryOptions {
criteria?: Partial<CriteriaDTO>;
enabled?: boolean;
};
}
export const useCustomersListQuery = (
options?: CustomersListQueryOptions

View File

@ -23,7 +23,7 @@ export const useSupplierDeleteMutation = () => {
mutationKey: SUPPLIER_DELETE_KEY,
mutationFn: async ({ id, signal }) => {
if (!id) {
throw new Error("customerId is required");
throw new Error("supplierId is required");
}
await deleteSupplierById(dataSource, { id, signal });

View File

@ -1,7 +1,8 @@
import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { GetSupplierByIdAdapter } from "../adapters";
import { getSupplierById } from "../api";
import { type GetSupplierByIdResult, getSupplierById } from "../api";
import type { Supplier } from "../entities";
import { SUPPLIER_QUERY_KEY } from "./keys";
@ -14,6 +15,7 @@ type SupplierGetQueryOptions = {
export const useSupplierGetQuery = (
options?: SupplierGetQueryOptions
): UseQueryResult<Supplier, DefaultError> => {
const dataSource = useDataSource();
const id = options?.id;
const enabled = options?.enabled ?? Boolean(id);

View File

@ -3,14 +3,15 @@ import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { ListSuppliersAdapter } from "../adapters";
import { type ListSuppliersResult, getListSuppliersByCriteria } from "../api";
import type { SupplierList } from "../entities";
import { LIST_SUPPLIERS_QUERY_KEY } from "./keys";
type SuppliersListQueryOptions = {
export interface SuppliersListQueryOptions {
criteria?: Partial<CriteriaDTO>;
enabled?: boolean;
};
}
export const useSuppliersListQuery = (
options?: SuppliersListQueryOptions