From 898c4a2958fa6abaa0b0a40e74074f1bcd4e3b12 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 5 Apr 2026 14:22:36 +0200 Subject: [PATCH] Repaso a proformas --- .vscode/extensions.json | 3 +- .../web/proformas/shared/adapters/index.ts | 3 + .../src/web/proformas/shared/hooks/index.ts | 5 + .../src/web/proformas/shared/hooks/keys.ts | 46 +++++ .../shared/hooks/proforma-cache-strategy.ts | 169 ++++++++++++++++++ .../shared/hooks/to-validation-errors.ts | 15 ++ .../shared/hooks/use-list-proformas-query.ts | 0 .../use-proforma-change-status-mutation.ts | 41 +++++ .../hooks/use-proforma-delete-mutation.ts | 46 +++++ .../shared/hooks/use-proforma-get-query.ts | 35 ++++ .../hooks/use-proforma-issue-mutation.ts | 29 +++ .../hooks/use-proforma-update-mutation.ts | 46 +++++ .../shared/hooks/use-proformas-list-query.ts | 35 ++++ 13 files changed, 472 insertions(+), 1 deletion(-) delete mode 100644 modules/customer-invoices/src/web/proformas/shared/hooks/use-list-proformas-query.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-change-status-mutation.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-issue-mutation.ts create mode 100644 modules/customer-invoices/src/web/proformas/shared/hooks/use-proformas-list-query.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9dc70574..842e23e8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "ms-vscode.vscode-json", "formulahendry.auto-rename-tag", "cweijan.dbclient-jdbc", - "pkief.material-icon-theme" + "pkief.material-icon-theme", + "fralle.copy-code-context" ] } diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts index e69de29b..58ff273b 100644 --- a/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts @@ -0,0 +1,3 @@ +export * from "./get-proforma-by-id.adapter"; +export * from "./list-proformas.adapter"; +export * from "./proforma-to-list-row-patch.adapter"; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts index e69de29b..4d50d9be 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts @@ -0,0 +1,5 @@ +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"; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts index e69de29b..53e31246 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts @@ -0,0 +1,46 @@ +import type { QueryKey } from "@tanstack/react-query"; + +import type { ProformasListRequestDTO } from "../../../../common"; + +/** + * Prefijo base para listados + */ +export const LIST_PROFORMAS_QUERY_KEY_PREFIX = ["proformas"] as const; + +/** + * Query key para listado de proformas + */ +export const LIST_PROFORMAS_QUERY_KEY = (criteria?: ProformasListRequestDTO): QueryKey => + [ + ...LIST_PROFORMAS_QUERY_KEY_PREFIX, + { + pageNumber: criteria?.pageNumber ?? 1, + pageSize: criteria?.pageSize ?? 10, + q: criteria?.q ?? "", + filters: criteria?.filters ?? [], + orderBy: criteria?.orderBy ?? "", + order: criteria?.order ?? "", + }, + ] as const; + +/** + * Query key para detalle de proforma + */ +export const PROFORMAS_DETAIL_QUERY_KEY_PREFIX = ["proformas:detail"] as const; +export const PROFORMA_QUERY_KEY = (proformaId?: string): QueryKey => [ + ...PROFORMAS_DETAIL_QUERY_KEY_PREFIX, + { proformaId }, +]; + +/** + * Keys para mutaciones + */ +export const CREATE_PROFORMA_MUTATION_KEY = ["proformas:create"] as const; +export const UPDATE_PROFORMA_MUTATION_KEY = ["proformas:update"] as const; +export const DELETE_PROFORMA_MUTATION_KEY = ["proformas:delete"] as const; + +/** + * Operaciones de dominio + */ +export const CHANGE_STATUS_PROFORMA_MUTATION_KEY = ["proformas:change-status"] as const; +export const ISSUE_PROFORMA_MUTATION_KEY = ["proformas:issue"] as const; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/proforma-cache-strategy.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/proforma-cache-strategy.ts index e69de29b..d9f19a4b 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/proforma-cache-strategy.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/proforma-cache-strategy.ts @@ -0,0 +1,169 @@ +import type { QueryClient, QueryKey } from "@tanstack/react-query"; + +import { ProformaToListRowPatchAdapter } from "../adapters"; +import type { Proforma, ProformaList, ProformaListRow } from "../entities"; + +import { LIST_PROFORMAS_QUERY_KEY_PREFIX, PROFORMA_QUERY_KEY } from "./keys"; + +export interface ProformaListCacheSnapshot { + key: QueryKey; + page?: ProformaList; +} + +export interface DeleteProformaCacheContext { + snapshots: ProformaListCacheSnapshot[]; +} + +// Primitivas + +export function cancelProformaListQueries(queryClient: QueryClient) { + return queryClient.cancelQueries({ + queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX, + }); +} + +export function invalidateProformaListQueries(queryClient: QueryClient) { + return queryClient.invalidateQueries({ + queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX, + }); +} + +export function invalidateProformaDetailQuery(queryClient: QueryClient, proformaId: string) { + return queryClient.invalidateQueries({ + queryKey: PROFORMA_QUERY_KEY(proformaId), + exact: true, + }); +} + +export function setProformaDetailCache( + queryClient: QueryClient, + proformaId: string, + proforma: Proforma +) { + queryClient.setQueryData(PROFORMA_QUERY_KEY(proformaId), proforma); +} + +export function removeProformaDetailCache(queryClient: QueryClient, proformaId: string) { + queryClient.removeQueries({ + queryKey: PROFORMA_QUERY_KEY(proformaId), + exact: true, + }); +} + +export function getAllProformaListQueryKeys(queryClient: QueryClient): QueryKey[] { + const entries = queryClient.getQueriesData({ + queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX, + }); + + return entries.map(([key]) => key); +} + +// Upsert de filas en cache + +export function upsertProformaInListCaches( + queryClient: QueryClient, + proforma: Pick & Partial +) { + const keys = getAllProformaListQueryKeys(queryClient); + + for (const key of keys) { + const page = queryClient.getQueryData(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, + }; + + queryClient.setQueryData(key, { + ...page, + items: nextItems, + }); + } +} + +// Remove optimista de listados + +export function removeProformaFromListCaches( + queryClient: QueryClient, + proformaId: string +): ProformaListCacheSnapshot[] { + const snapshots = getAllProformaListQueryKeys(queryClient).map((key) => ({ + key, + page: queryClient.getQueryData(key), + })); + + for (const { key, page } of snapshots) { + if (!page) continue; + + queryClient.setQueryData(key, { + ...page, + items: page.items.filter((row) => row.id !== proformaId), + total_items: Math.max(0, page.total_items - 1), + }); + } + + return snapshots; +} + +// Restore + +export function restoreProformaListCaches( + queryClient: QueryClient, + snapshots: ProformaListCacheSnapshot[] +) { + for (const snapshot of snapshots) { + queryClient.setQueryData(snapshot.key, snapshot.page); + } +} + +// Estrategias compuestas + +export function syncCreatedProformaCaches(queryClient: QueryClient, proforma: Proforma) { + setProformaDetailCache(queryClient, proforma.id, proforma); + return invalidateProformaListQueries(queryClient); +} + +export function syncUpdatedProformaCaches(queryClient: QueryClient, proforma: Proforma) { + setProformaDetailCache(queryClient, proforma.id, proforma); + upsertProformaInListCaches(queryClient, ProformaToListRowPatchAdapter.fromProforma(proforma)); + + return invalidateProformaListQueries(queryClient); +} + +export async function prepareDeleteProformaOptimisticUpdate( + queryClient: QueryClient, + proformaId: string +): Promise { + await cancelProformaListQueries(queryClient); + const snapshots = removeProformaFromListCaches(queryClient, proformaId); + + return { snapshots }; +} + +export function rollbackDeleteProformaOptimisticUpdate( + queryClient: QueryClient, + context?: DeleteProformaCacheContext +) { + if (!context) return; + restoreProformaListCaches(queryClient, context.snapshots); +} + +export function finalizeDeletedProformaCaches(queryClient: QueryClient, proformaId: string) { + removeProformaDetailCache(queryClient, proformaId); + return invalidateProformaListQueries(queryClient); +} + +// Cambio de estado e issue + +export function syncChangedProformaStatusCaches(queryClient: QueryClient, proforma: Proforma) { + return syncUpdatedProformaCaches(queryClient, proforma); +} + +export function syncIssuedProformaCaches(queryClient: QueryClient, proforma: Proforma) { + return syncUpdatedProformaCaches(queryClient, proforma); +} diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/to-validation-errors.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/to-validation-errors.ts index e69de29b..55b279a6 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/to-validation-errors.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/to-validation-errors.ts @@ -0,0 +1,15 @@ +import type { ZodError } from "zod"; + +/** + * Convierte un error de validación de Zod en una colección de errores de validación personalizada. + * + * @param error + * @returns array de objetos con el campo y el mensaje de error correspondiente. + */ + +export function toValidationErrors(error: ZodError) { + return error.issues.map((err) => ({ + field: err.path.join("."), + message: err.message, + })); +} diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-list-proformas-query.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-list-proformas-query.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-change-status-mutation.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-change-status-mutation.ts new file mode 100644 index 00000000..56f64081 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-change-status-mutation.ts @@ -0,0 +1,41 @@ +import { useDataSource } from "@erp/core/hooks"; +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 { CHANGE_STATUS_PROFORMA_MUTATION_KEY } from "./keys"; +import { syncChangedProformaStatusCaches } from "./proforma-cache-strategy"; +import { toValidationErrors } from "./to-validation-errors"; + +type ChangeProformaStatusContext = {}; + +export const useProformaChangeStatusMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + const schema = ChangeStatusProformaByIdRequestSchema; + + useMutation({ + mutationKey: CHANGE_STATUS_PROFORMA_MUTATION_KEY, + + mutationFn: async (params) => { + const { id: proformaId, data } = params; + 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 dto = await changeProformaStatusById(dataSource, params); + return ChangeStatusProformaByIdAdapter.fromDto(dto); + }, + onSuccess: async (proforma) => { + await syncChangedProformaStatusCaches(queryClient, proforma); + }, + }); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-delete-mutation.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-delete-mutation.ts index e69de29b..73cfc29a 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-delete-mutation.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-delete-mutation.ts @@ -0,0 +1,46 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import type { DeleteProformaByIdParams } from "../api"; +import { deleteProformaById } from "../api"; + +import { DELETE_PROFORMA_MUTATION_KEY } from "./keys"; +import { + type DeleteProformaCacheContext, + finalizeDeletedProformaCaches, + prepareDeleteProformaOptimisticUpdate, + rollbackDeleteProformaOptimisticUpdate, +} from "./proforma-cache-strategy"; + +interface DeleteProformaContext extends DeleteProformaCacheContext {} + +export const useProformaDeleteMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation<{ id: string }, DefaultError, DeleteProformaByIdParams, DeleteProformaContext>( + { + mutationKey: DELETE_PROFORMA_MUTATION_KEY, + mutationFn: async ({ id, signal }) => { + if (!id) { + throw new Error("customerId is required"); + } + + await deleteProformaById(dataSource, { id, signal }); + return { id }; + }, + onMutate: async ({ id }) => { + return prepareDeleteProformaOptimisticUpdate(queryClient, id); + }, + onError: (_error, _params, context) => { + rollbackDeleteProformaOptimisticUpdate(queryClient, context); + }, + onSuccess: ({ id }) => { + finalizeDeletedProformaCaches(queryClient, id); + }, + onSettled: async (_data, _error, { id }) => { + await finalizeDeletedProformaCaches(queryClient, id); + }, + } + ); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-get-query.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-get-query.ts index e69de29b..3a4e303d 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-get-query.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-get-query.ts @@ -0,0 +1,35 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type UseQueryResult, useQuery } from "@tanstack/react-query"; + +import { GetProformaByIdAdapter } from "../adapters"; +import { getProformaById } from "../api"; +import type { Proforma } from "../entities"; + +import { PROFORMA_QUERY_KEY } from "./keys"; + +export interface UseProformaGetQueryOptions { + id?: string; + enabled?: boolean; +} + +export const useProformaGetQuery = ( + options: UseProformaGetQueryOptions +): UseQueryResult => { + const dataSource = useDataSource(); + const id = options?.id; + const enabled = options?.enabled ?? Boolean(id); + + return useQuery({ + queryKey: PROFORMA_QUERY_KEY(id), + queryFn: async ({ signal }) => { + if (!id) throw new Error("proformaId is required"); + + const dto = await getProformaById(dataSource, { + id: id!, + signal, + }); + return GetProformaByIdAdapter.fromDto(dto); + }, + enabled: enabled && Boolean(id), + }); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-issue-mutation.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-issue-mutation.ts new file mode 100644 index 00000000..4d38c53b --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-issue-mutation.ts @@ -0,0 +1,29 @@ +import { useDataSource } from "@erp/core/hooks"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { ChangeStatusProformaByIdRequestSchema } from "../../../../common"; +import { type IssueProformaByIdParams, issueProformaById } from "../api"; +import type { Proforma } from "../entities"; + +import { ISSUE_PROFORMA_MUTATION_KEY } from "./keys"; +import { syncIssuedProformaCaches } from "./proforma-cache-strategy"; + +type IssueProformaByIdContext = {}; + +export const useProformaChangeStatusMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + const schema = ChangeStatusProformaByIdRequestSchema; + + useMutation({ + mutationKey: ISSUE_PROFORMA_MUTATION_KEY, + + mutationFn: async (params) => { + const dto = await issueProformaById(dataSource, params); + return IssueProformaByIdAdapter.fromDto(dto); + }, + onSuccess: async (proforma) => { + await syncIssuedProformaCaches(queryClient, proforma); + }, + }); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-update-mutation.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-update-mutation.ts index e69de29b..00c75d5c 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-update-mutation.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proforma-update-mutation.ts @@ -0,0 +1,46 @@ +import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { UpdateProformaByIdRequestSchema } from "../../../../common"; +import { GetProformaByIdAdapter } from "../adapters"; +import type { UpdateProformaByIdParams, UpdateProformaByIdResult } from "../api"; +import { updateProformaById } from "../api"; +import type { Proforma } from "../entities"; + +import { UPDATE_PROFORMA_MUTATION_KEY } from "./keys"; +import { syncUpdatedProformaCaches } from "./proforma-cache-strategy"; +import { toValidationErrors } from "./to-validation-errors"; + +type UpdateProformaContext = {}; + +export const useProformaUpdateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + const schema = UpdateProformaByIdRequestSchema; + + return useMutation({ + mutationKey: UPDATE_PROFORMA_MUTATION_KEY, + + mutationFn: async (params) => { + const { id: proformaId, data } = params; + 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 dto: UpdateProformaByIdResult = await updateProformaById( + dataSource, + params as UpdateProformaByIdParams + ); + return GetProformaByIdAdapter.fromDto(dto); + }, + onSuccess: async (proforma) => { + await syncUpdatedProformaCaches(queryClient, proforma); + }, + }); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-proformas-list-query.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proformas-list-query.ts new file mode 100644 index 00000000..3e6166f6 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-proformas-list-query.ts @@ -0,0 +1,35 @@ +import type { CriteriaDTO } from "@erp/core"; +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 type { ProformaList } from "../entities"; + +import { LIST_PROFORMAS_QUERY_KEY } from "./keys"; + +export interface ProformasListQueryOptions { + criteria?: Partial; + enabled?: boolean; +} + +export const useProformasListQuery = ( + options?: ProformasListQueryOptions +): UseQueryResult => { + const dataSource = useDataSource(); + const enabled = options?.enabled ?? true; + const criteria = options?.criteria ?? {}; + + return useQuery({ + queryKey: LIST_PROFORMAS_QUERY_KEY(criteria), + queryFn: async ({ signal }) => { + const dto: ListProformasResult = await getListProformasByCriteria(dataSource, { + criteria, + signal, + }); + return ListProformasAdapter.fromDto(dto); + }, + enabled, + placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria + }); +};