From 6e97e91173807be105c1cb5aece56646d115ca20 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 3 Apr 2026 22:03:48 +0200 Subject: [PATCH] Parte cliente de Suppliers --- modules/supplier/package.json | 25 ++- .../adapters/get-supplier-by-id.adapter.ts | 38 +++++ .../supplier/src/web/shared/adapters/index.ts | 3 + .../shared/adapters/list-suppliers.adapter.ts | 52 ++++++ .../supplier-to-list-row-patch.adapter.ts | 29 ++++ .../src/web/shared/api/create-supplier.api.ts | 24 +++ .../shared/api/delete-supplier-by-id.api.ts | 13 ++ .../web/shared/api/get-supplier-by-id.api.ts | 13 ++ modules/supplier/src/web/shared/api/index.ts | 5 + .../src/web/shared/api/list-suppliers.api.ts | 19 +++ .../shared/api/update-supplier-by-id.api.ts | 22 +++ .../src/web/shared/constants/index.ts | 0 .../shared/constants/supplier.constants.ts | 28 ++++ .../supplier/src/web/shared/entities/index.ts | 3 + .../entities/supplier-list-row.entity.ts | 31 ++++ .../shared/entities/supplier-list.entity.ts | 9 ++ .../web/shared/entities/supplier.entity.ts | 31 ++++ .../supplier/src/web/shared/hooks/index.ts | 8 + modules/supplier/src/web/shared/hooks/keys.ts | 29 ++++ .../shared/hooks/supplier-cache-strategy.ts | 149 ++++++++++++++++++ .../web/shared/hooks/to-validation-errors.ts | 10 ++ .../shared/hooks/use-list-suppliers-query.ts | 32 ++++ .../hooks/use-supplier-create-mutation.ts | 51 ++++++ .../hooks/use-supplier-delete-mutation.ts | 53 +++++++ .../shared/hooks/use-supplier-get-query.ts | 29 ++++ .../hooks/use-supplier-update-mutation.ts | 54 +++++++ modules/supplier/src/web/shared/index.ts | 4 + pnpm-lock.yaml | 45 ++++++ 28 files changed, 808 insertions(+), 1 deletion(-) create mode 100644 modules/supplier/src/web/shared/adapters/get-supplier-by-id.adapter.ts create mode 100644 modules/supplier/src/web/shared/adapters/index.ts create mode 100644 modules/supplier/src/web/shared/adapters/list-suppliers.adapter.ts create mode 100644 modules/supplier/src/web/shared/adapters/supplier-to-list-row-patch.adapter.ts create mode 100644 modules/supplier/src/web/shared/api/create-supplier.api.ts create mode 100644 modules/supplier/src/web/shared/api/delete-supplier-by-id.api.ts create mode 100644 modules/supplier/src/web/shared/api/get-supplier-by-id.api.ts create mode 100644 modules/supplier/src/web/shared/api/index.ts create mode 100644 modules/supplier/src/web/shared/api/list-suppliers.api.ts create mode 100644 modules/supplier/src/web/shared/api/update-supplier-by-id.api.ts create mode 100644 modules/supplier/src/web/shared/constants/index.ts create mode 100644 modules/supplier/src/web/shared/constants/supplier.constants.ts create mode 100644 modules/supplier/src/web/shared/entities/index.ts create mode 100644 modules/supplier/src/web/shared/entities/supplier-list-row.entity.ts create mode 100644 modules/supplier/src/web/shared/entities/supplier-list.entity.ts create mode 100644 modules/supplier/src/web/shared/entities/supplier.entity.ts create mode 100644 modules/supplier/src/web/shared/hooks/index.ts create mode 100644 modules/supplier/src/web/shared/hooks/keys.ts create mode 100644 modules/supplier/src/web/shared/hooks/supplier-cache-strategy.ts create mode 100644 modules/supplier/src/web/shared/hooks/to-validation-errors.ts create mode 100644 modules/supplier/src/web/shared/hooks/use-list-suppliers-query.ts create mode 100644 modules/supplier/src/web/shared/hooks/use-supplier-create-mutation.ts create mode 100644 modules/supplier/src/web/shared/hooks/use-supplier-delete-mutation.ts create mode 100644 modules/supplier/src/web/shared/hooks/use-supplier-get-query.ts create mode 100644 modules/supplier/src/web/shared/hooks/use-supplier-update-mutation.ts create mode 100644 modules/supplier/src/web/shared/index.ts diff --git a/modules/supplier/package.json b/modules/supplier/package.json index 6e67e2a8..0da9f495 100644 --- a/modules/supplier/package.json +++ b/modules/supplier/package.json @@ -12,22 +12,45 @@ "exports": { ".": "./src/common/index.ts", "./common": "./src/common/index.ts", - "./api": "./src/api/index.ts" + "./api": "./src/api/index.ts", + "./api/domain": "./src/api/domain/index.ts", + "./client": "./src/web/manifest.ts", + "./globals.css": "./src/web/globals.css", + "./components": "./src/web/components/index.ts" + }, + "peerDependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { "@types/express": "^4.17.21", + "@types/react": "^19.1.2", + "@types/react-router-dom": "^5.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", "typescript": "^5.9.3" }, "dependencies": { "@erp/auth": "workspace:*", "@erp/core": "workspace:*", + "@hookform/resolvers": "^5.0.1", "@repo/i18next": "workspace:*", "@repo/rdx-criteria": "workspace:*", "@repo/rdx-ddd": "workspace:*", "@repo/rdx-logger": "workspace:*", + "@repo/rdx-ui": "workspace:*", "@repo/rdx-utils": "workspace:*", + "@repo/shadcn-ui": "workspace:*", + "@tanstack/react-query": "^5.90.6", + "@tanstack/react-table": "^8.21.3", "express": "^4.18.2", + "lucide-react": "^0.577.0", + "react-data-table-component": "^7.7.0", + "react-hook-form": "^7.72.1", + "react-i18next": "^16.2.4", + "react-router-dom": "^6.26.0", "sequelize": "^6.37.8", + "use-debounce": "^10.0.5", "zod": "^4.1.11" } } \ No newline at end of file diff --git a/modules/supplier/src/web/shared/adapters/get-supplier-by-id.adapter.ts b/modules/supplier/src/web/shared/adapters/get-supplier-by-id.adapter.ts new file mode 100644 index 00000000..18b2a497 --- /dev/null +++ b/modules/supplier/src/web/shared/adapters/get-supplier-by-id.adapter.ts @@ -0,0 +1,38 @@ +import type { GetSupplierByIdResponseDTO } from "../../../common"; +import type { Supplier } from "../entities"; + +export const GetSupplierByIdAdapter = { + fromDTO(dto: GetSupplierByIdResponseDTO): Supplier { + return { + id: dto.id, + companyId: dto.company_id, + status: dto.status, + reference: dto.reference, + + isCompany: dto.is_company === "1", + name: dto.name, + tradeName: dto.trade_name, + tin: dto.tin, + + street: dto.street, + street2: dto.street2, + city: dto.city, + province: dto.province, + postalCode: dto.postal_code, + country: dto.country, + + primaryEmail: dto.email_primary, + secondaryEmail: dto.email_secondary, + primaryPhone: dto.phone_primary, + secondaryPhone: dto.phone_secondary, + primaryMobile: dto.mobile_primary, + secondaryMobile: dto.mobile_secondary, + + fax: dto.fax, + website: dto.website, + + languageCode: dto.language_code, + currencyCode: dto.currency_code, + }; + }, +}; diff --git a/modules/supplier/src/web/shared/adapters/index.ts b/modules/supplier/src/web/shared/adapters/index.ts new file mode 100644 index 00000000..68806d11 --- /dev/null +++ b/modules/supplier/src/web/shared/adapters/index.ts @@ -0,0 +1,3 @@ +export * from "./get-supplier-by-id.adapter"; +export * from "./list-suppliers.adapter"; +export * from "./supplier-to-list-row-patch.adapter"; diff --git a/modules/supplier/src/web/shared/adapters/list-suppliers.adapter.ts b/modules/supplier/src/web/shared/adapters/list-suppliers.adapter.ts new file mode 100644 index 00000000..ba15b5ec --- /dev/null +++ b/modules/supplier/src/web/shared/adapters/list-suppliers.adapter.ts @@ -0,0 +1,52 @@ +import type { ListSuppliersResponseDTO } from "../../../common"; +import type { SupplierList, SupplierListRow } from "../entities"; + +type ListSuppliersItemDTO = ListSuppliersResponseDTO["items"][number]; + +export const ListSuppliersAdapter = { + fromDTO(pageDto: ListSuppliersResponseDTO): SupplierList { + return { + page: pageDto.page, + per_page: pageDto.per_page, + total_pages: pageDto.total_pages, + total_items: pageDto.total_items, + items: pageDto.items.map(ListSuppliersRowAdapter.fromDTO), + }; + }, +}; + +const ListSuppliersRowAdapter = { + fromDTO(rowDto: ListSuppliersItemDTO): SupplierListRow { + return { + id: rowDto.id, + companyId: rowDto.company_id, + status: rowDto.status, + reference: rowDto.reference, + + isCompany: rowDto.is_company === "1", + name: rowDto.name, + tradeName: rowDto.trade_name, + tin: rowDto.tin, + + street: rowDto.street, + street2: rowDto.street2, + city: rowDto.city, + province: rowDto.province, + postalCode: rowDto.postal_code, + country: rowDto.country, + + primaryEmail: rowDto.email_primary, + secondaryEmail: rowDto.email_secondary, + primaryPhone: rowDto.phone_primary, + secondaryPhone: rowDto.phone_secondary, + primaryMobile: rowDto.mobile_primary, + secondaryMobile: rowDto.mobile_secondary, + + fax: rowDto.fax, + website: rowDto.website, + + languageCode: rowDto.language_code, + currencyCode: rowDto.currency_code, + }; + }, +}; diff --git a/modules/supplier/src/web/shared/adapters/supplier-to-list-row-patch.adapter.ts b/modules/supplier/src/web/shared/adapters/supplier-to-list-row-patch.adapter.ts new file mode 100644 index 00000000..6cc7e9c1 --- /dev/null +++ b/modules/supplier/src/web/shared/adapters/supplier-to-list-row-patch.adapter.ts @@ -0,0 +1,29 @@ +import type { Supplier, SupplierListRow } from "../entities"; + +export type SupplierListRowPatch = Pick & Partial; + +export const SupplierToListRowPatchAdapter = { + fromSupplier(supplier: Supplier): SupplierListRowPatch { + return { + id: supplier.id, + status: supplier.status, + reference: supplier.reference, + + isCompany: supplier.isCompany, + name: supplier.name, + tradeName: supplier.tradeName, + tin: supplier.tin, + + city: supplier.city, + province: supplier.province, + country: supplier.country, + + primaryEmail: supplier.primaryEmail, + primaryPhone: supplier.primaryPhone, + primaryMobile: supplier.primaryMobile, + + languageCode: supplier.languageCode, + currencyCode: supplier.currencyCode, + }; + }, +}; diff --git a/modules/supplier/src/web/shared/api/create-supplier.api.ts b/modules/supplier/src/web/shared/api/create-supplier.api.ts new file mode 100644 index 00000000..7cbebc59 --- /dev/null +++ b/modules/supplier/src/web/shared/api/create-supplier.api.ts @@ -0,0 +1,24 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { CreateSupplierRequestDTO, SupplierCreationResponseDTO } from "../../../common"; + +export type SupplierCreateInput = CreateSupplierRequestDTO; +export type SupplierCreateOutput = SupplierCreationResponseDTO; + +export async function createSupplier( + dataSource: IDataSource, + id: string, + data: SupplierCreateInput +) { + if (!id) throw new Error("supplierId is required"); + + const response = await dataSource.createOne( + "suppliers", + { + ...data, + id, + } + ); + + return response; +} diff --git a/modules/supplier/src/web/shared/api/delete-supplier-by-id.api.ts b/modules/supplier/src/web/shared/api/delete-supplier-by-id.api.ts new file mode 100644 index 00000000..47115009 --- /dev/null +++ b/modules/supplier/src/web/shared/api/delete-supplier-by-id.api.ts @@ -0,0 +1,13 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { DeleteSupplierByIdRequestDTO } from "../../../common"; + +export type SupplierDeleteInput = DeleteSupplierByIdRequestDTO; + +export async function deleteSupplierById(dataSource: IDataSource, id: string, signal: AbortSignal) { + const response = await dataSource.deleteOne("suppliers", id, { + signal, + }); + + return response; +} diff --git a/modules/supplier/src/web/shared/api/get-supplier-by-id.api.ts b/modules/supplier/src/web/shared/api/get-supplier-by-id.api.ts new file mode 100644 index 00000000..6214d3f1 --- /dev/null +++ b/modules/supplier/src/web/shared/api/get-supplier-by-id.api.ts @@ -0,0 +1,13 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { GetSupplierByIdResponseDTO } from "../../../common"; + +export type SupplierGetOutput = GetSupplierByIdResponseDTO; + +export async function getSupplierById(dataSource: IDataSource, id: string, signal: AbortSignal) { + const response = dataSource.getOne("suppliers", id, { + signal, + }); + + return response; +} diff --git a/modules/supplier/src/web/shared/api/index.ts b/modules/supplier/src/web/shared/api/index.ts new file mode 100644 index 00000000..6f778c2a --- /dev/null +++ b/modules/supplier/src/web/shared/api/index.ts @@ -0,0 +1,5 @@ +export * from "./create-supplier.api"; +export * from "./delete-supplier-by-id.api"; +export * from "./get-supplier-by-id.api"; +export * from "./list-suppliers.api"; +export * from "./update-supplier-by-id.api"; diff --git a/modules/supplier/src/web/shared/api/list-suppliers.api.ts b/modules/supplier/src/web/shared/api/list-suppliers.api.ts new file mode 100644 index 00000000..44a309b6 --- /dev/null +++ b/modules/supplier/src/web/shared/api/list-suppliers.api.ts @@ -0,0 +1,19 @@ +import type { CriteriaDTO } from "@erp/core"; +import type { IDataSource } from "@erp/core/client"; + +import type { ListSuppliersResponseDTO } from "../../../common"; + +export type SupplierListOutput = ListSuppliersResponseDTO; + +export async function getListSuppliers( + dataSource: IDataSource, + criteria: CriteriaDTO, + signal: AbortSignal +) { + const response = dataSource.getList("suppliers", { + signal, + ...criteria, + }); + + return response; +} diff --git a/modules/supplier/src/web/shared/api/update-supplier-by-id.api.ts b/modules/supplier/src/web/shared/api/update-supplier-by-id.api.ts new file mode 100644 index 00000000..d58a1c40 --- /dev/null +++ b/modules/supplier/src/web/shared/api/update-supplier-by-id.api.ts @@ -0,0 +1,22 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { UpdateSupplierByIdRequestDTO, UpdateSupplierByIdResponseDTO } from "../../../common"; + +export type SupplierUpdateInput = UpdateSupplierByIdRequestDTO; +export type SupplierUpdateOutput = UpdateSupplierByIdResponseDTO; + +export async function updateSupplierById( + dataSource: IDataSource, + id: string, + data: SupplierUpdateInput +) { + if (!id) throw new Error("supplierId is required"); + + const response = dataSource.updateOne( + "suppliers", + id, + data + ); + + return response; +} diff --git a/modules/supplier/src/web/shared/constants/index.ts b/modules/supplier/src/web/shared/constants/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/supplier/src/web/shared/constants/supplier.constants.ts b/modules/supplier/src/web/shared/constants/supplier.constants.ts new file mode 100644 index 00000000..180eaed4 --- /dev/null +++ b/modules/supplier/src/web/shared/constants/supplier.constants.ts @@ -0,0 +1,28 @@ +export const COUNTRY_OPTIONS = [ + { value: "es", label: "España" }, + { value: "fr", label: "Francia" }, + { value: "de", label: "Alemania" }, + { value: "it", label: "Italia" }, + { value: "pt", label: "Portugal" }, + { value: "us", label: "Estados Unidos" }, + { value: "mx", label: "México" }, + { value: "ar", label: "Argentina" }, +] as const; + +export const LANGUAGE_OPTIONS = [ + { value: "es", label: "Español" }, + { value: "en", label: "Inglés" }, + { value: "fr", label: "Francés" }, + { value: "de", label: "Alemán" }, + { value: "it", label: "Italiano" }, + { value: "pt", label: "Portugués" }, +] as const; + +export const CURRENCY_OPTIONS = [ + { value: "EUR", label: "Euro" }, + { value: "USD", label: "Dólar estadounidense" }, + { value: "GBP", label: "Libra esterlina" }, + { value: "ARS", label: "Peso argentino" }, + { value: "MXN", label: "Peso mexicano" }, + { value: "JPY", label: "Yen japonés" }, +] as const; diff --git a/modules/supplier/src/web/shared/entities/index.ts b/modules/supplier/src/web/shared/entities/index.ts new file mode 100644 index 00000000..b83e5840 --- /dev/null +++ b/modules/supplier/src/web/shared/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./supplier.entity"; +export * from "./supplier-list.entity"; +export * from "./supplier-list-row.entity"; diff --git a/modules/supplier/src/web/shared/entities/supplier-list-row.entity.ts b/modules/supplier/src/web/shared/entities/supplier-list-row.entity.ts new file mode 100644 index 00000000..d23e241c --- /dev/null +++ b/modules/supplier/src/web/shared/entities/supplier-list-row.entity.ts @@ -0,0 +1,31 @@ +export interface SupplierListRow { + id: string; + companyId: string; + status: string; + reference: string; + + isCompany: boolean; + name: string; + tradeName: string; + tin: string; + + street: string; + street2: string; + city: string; + province: string; + postalCode: string; + country: string; + + primaryEmail: string; + secondaryEmail: string; + primaryPhone: string; + secondaryPhone: string; + primaryMobile: string; + secondaryMobile: string; + + fax: string; + website: string; + + languageCode: string; + currencyCode: string; +} diff --git a/modules/supplier/src/web/shared/entities/supplier-list.entity.ts b/modules/supplier/src/web/shared/entities/supplier-list.entity.ts new file mode 100644 index 00000000..dc740a9d --- /dev/null +++ b/modules/supplier/src/web/shared/entities/supplier-list.entity.ts @@ -0,0 +1,9 @@ +import type { SupplierListRow } from "./supplier-list-row.entity"; + +export interface SupplierList { + items: SupplierListRow[]; + total_pages: number; + total_items: number; + page: number; + per_page: number; +} diff --git a/modules/supplier/src/web/shared/entities/supplier.entity.ts b/modules/supplier/src/web/shared/entities/supplier.entity.ts new file mode 100644 index 00000000..26fce74e --- /dev/null +++ b/modules/supplier/src/web/shared/entities/supplier.entity.ts @@ -0,0 +1,31 @@ +export interface Supplier { + id: string; + companyId: string; + status: string; + reference: string; + + isCompany: boolean; + name: string; + tradeName: string; + tin: string; + + street: string; + street2: string; + city: string; + province: string; + postalCode: string; + country: string; + + primaryEmail: string; + secondaryEmail: string; + primaryPhone: string; + secondaryPhone: string; + primaryMobile: string; + secondaryMobile: string; + + fax: string; + website: string; + + languageCode: string; + currencyCode: string; +} diff --git a/modules/supplier/src/web/shared/hooks/index.ts b/modules/supplier/src/web/shared/hooks/index.ts new file mode 100644 index 00000000..d8f6cf41 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/index.ts @@ -0,0 +1,8 @@ +export * from "./keys"; +export * from "./supplier-cache-strategy"; +export * from "./to-validation-errors"; +export * from "./use-list-suppliers-query"; +export * from "./use-supplier-create-mutation"; +export * from "./use-supplier-delete-mutation"; +export * from "./use-supplier-get-query"; +export * from "./use-supplier-update-mutation"; diff --git a/modules/supplier/src/web/shared/hooks/keys.ts b/modules/supplier/src/web/shared/hooks/keys.ts new file mode 100644 index 00000000..ff88f028 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/keys.ts @@ -0,0 +1,29 @@ +import type { CriteriaDTO } from "@erp/core"; +import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria"; +import type { QueryKey } from "@tanstack/react-query"; + +export const LIST_SUPPLIERS_QUERY_KEY_PREFIX = ["suppliers"] as const; + +export const LIST_SUPPLIERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => + [ + ...LIST_SUPPLIERS_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 ?? "", + }, + ] as const; + +export const SUPPLIER_QUERY_KEY = (supplierId?: string): QueryKey => [ + "suppliers:detail", + { + supplierId, + }, +]; + +export const SUPPLIER_CREATE_KEY = ["suppliers", "create"] as const; +export const SUPPLIER_UPDATE_KEY = ["suppliers", "update"] as const; +export const SUPPLIER_DELETE_KEY = ["suppliers", "delete"] as const; diff --git a/modules/supplier/src/web/shared/hooks/supplier-cache-strategy.ts b/modules/supplier/src/web/shared/hooks/supplier-cache-strategy.ts new file mode 100644 index 00000000..772a1e82 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/supplier-cache-strategy.ts @@ -0,0 +1,149 @@ +import type { QueryClient, QueryKey } from "@tanstack/react-query"; + +import { SupplierToListRowPatchAdapter } from "../adapters"; +import type { Supplier, SupplierList, SupplierListRow } from "../entities"; + +import { LIST_SUPPLIERS_QUERY_KEY_PREFIX, SUPPLIER_QUERY_KEY } from "./keys"; + +export interface SupplierListCacheSnapshot { + key: QueryKey; + page?: SupplierList; +} + +export interface DeleteSupplierCacheContext { + snapshots: SupplierListCacheSnapshot[]; +} + +export function cancelSupplierListQueries(queryClient: QueryClient) { + return queryClient.cancelQueries({ + queryKey: LIST_SUPPLIERS_QUERY_KEY_PREFIX, + }); +} + +export function invalidateSupplierListQueries(queryClient: QueryClient) { + return queryClient.invalidateQueries({ + queryKey: LIST_SUPPLIERS_QUERY_KEY_PREFIX, + }); +} + +export function invalidateSupplierDetailQuery(queryClient: QueryClient, supplierId: string) { + return queryClient.invalidateQueries({ + queryKey: SUPPLIER_QUERY_KEY(supplierId), + exact: true, + }); +} + +export function setSupplierDetailCache( + queryClient: QueryClient, + supplierId: string, + supplier: Supplier +) { + queryClient.setQueryData(SUPPLIER_QUERY_KEY(supplierId), supplier); +} + +export function removeSupplierDetailCache(queryClient: QueryClient, supplierId: string) { + queryClient.removeQueries({ + queryKey: SUPPLIER_QUERY_KEY(supplierId), + exact: true, + }); +} + +export function getAllSupplierListQueryKeys(queryClient: QueryClient): QueryKey[] { + const entries = queryClient.getQueriesData({ + queryKey: LIST_SUPPLIERS_QUERY_KEY_PREFIX, + }); + + return entries.map(([key]) => key); +} + +export function upsertSupplierInListCaches( + queryClient: QueryClient, + supplier: Pick & Partial +) { + const keys = getAllSupplierListQueryKeys(queryClient); + + for (const key of keys) { + const page = queryClient.getQueryData(key); + if (!page) continue; + + const index = page.items.findIndex((row) => row.id === supplier.id); + if (index === -1) continue; + + const nextItems = page.items.slice(); + nextItems[index] = { + ...page.items[index], + ...supplier, + }; + + queryClient.setQueryData(key, { + ...page, + items: nextItems, + }); + } +} + +export function removeSupplierFromListCaches( + queryClient: QueryClient, + supplierId: string +): SupplierListCacheSnapshot[] { + const snapshots = getAllSupplierListQueryKeys(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 !== supplierId), + total_items: Math.max(0, page.total_items - 1), + }); + } + + return snapshots; +} + +export function restoreSupplierListCaches( + queryClient: QueryClient, + snapshots: SupplierListCacheSnapshot[] +) { + for (const snapshot of snapshots) { + queryClient.setQueryData(snapshot.key, snapshot.page); + } +} + +export function syncCreatedSupplierCaches(queryClient: QueryClient, supplier: Supplier) { + setSupplierDetailCache(queryClient, supplier.id, supplier); + return invalidateSupplierListQueries(queryClient); +} + +export function syncUpdatedSupplierCaches(queryClient: QueryClient, supplier: Supplier) { + setSupplierDetailCache(queryClient, supplier.id, supplier); + upsertSupplierInListCaches(queryClient, SupplierToListRowPatchAdapter.fromSupplier(supplier)); + + return invalidateSupplierListQueries(queryClient); +} + +export async function prepareDeleteSupplierOptimisticUpdate( + queryClient: QueryClient, + supplierId: string +): Promise { + await cancelSupplierListQueries(queryClient); + const snapshots = removeSupplierFromListCaches(queryClient, supplierId); + + return { snapshots }; +} + +export function rollbackDeleteSupplierOptimisticUpdate( + queryClient: QueryClient, + context?: DeleteSupplierCacheContext +) { + if (!context) return; + restoreSupplierListCaches(queryClient, context.snapshots); +} + +export function finalizeDeletedSupplierCaches(queryClient: QueryClient, supplierId: string) { + removeSupplierDetailCache(queryClient, supplierId); + return invalidateSupplierListQueries(queryClient); +} diff --git a/modules/supplier/src/web/shared/hooks/to-validation-errors.ts b/modules/supplier/src/web/shared/hooks/to-validation-errors.ts new file mode 100644 index 00000000..4f04945e --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/to-validation-errors.ts @@ -0,0 +1,10 @@ +import type { ZodError } from "zod"; + +// Helpers de validación a errores de dominio + +export function toValidationErrors(error: ZodError) { + return error.issues.map((err) => ({ + field: err.path.join("."), + message: err.message, + })); +} diff --git a/modules/supplier/src/web/shared/hooks/use-list-suppliers-query.ts b/modules/supplier/src/web/shared/hooks/use-list-suppliers-query.ts new file mode 100644 index 00000000..5c91d391 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/use-list-suppliers-query.ts @@ -0,0 +1,32 @@ +import type { CriteriaDTO } from "@erp/core"; +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query"; + +import { ListSuppliersAdapter } from "../adapters"; +import { getListSuppliers } from "../api"; +import type { SupplierList } from "../entities"; + +import { LIST_SUPPLIERS_QUERY_KEY } from "./keys"; + +type ListSuppliersQueryOptions = { + enabled?: boolean; + criteria?: Partial; +}; + +export const useListSuppliersQuery = ( + options?: ListSuppliersQueryOptions +): UseQueryResult => { + const dataSource = useDataSource(); + const enabled = options?.enabled ?? true; + const criteria = options?.criteria ?? {}; + + return useQuery({ + queryKey: LIST_SUPPLIERS_QUERY_KEY(criteria as CriteriaDTO), + queryFn: async ({ signal }) => { + const dto = await getListSuppliers(dataSource, criteria as CriteriaDTO, signal); + return ListSuppliersAdapter.fromDTO(dto); + }, + enabled, + placeholderData: (previousData) => previousData, + }); +}; diff --git a/modules/supplier/src/web/shared/hooks/use-supplier-create-mutation.ts b/modules/supplier/src/web/shared/hooks/use-supplier-create-mutation.ts new file mode 100644 index 00000000..023bfdc9 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/use-supplier-create-mutation.ts @@ -0,0 +1,51 @@ +import { useDataSource } from "@erp/core/hooks"; +import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { CreateSupplierRequestSchema } from "../../../common"; +import { GetSupplierByIdAdapter } from "../adapters"; +import { type SupplierCreateInput, createSupplier } from "../api"; +import type { Supplier } from "../entities"; + +import { SUPPLIER_CREATE_KEY } from "./keys"; +import { + invalidateSupplierListQueries, + syncCreatedSupplierCaches, +} from "./supplier-cache-strategy"; +import { toValidationErrors } from "./to-validation-errors"; + +type CreateSupplierContext = {}; + +type CreateSupplierPayload = { + data: SupplierCreateInput; +}; + +export const useSupplierCreateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + const schema = CreateSupplierRequestSchema; + + return useMutation({ + mutationKey: SUPPLIER_CREATE_KEY, + + mutationFn: async ({ data }) => { + const id = UniqueID.generateNewID().toString(); + + const result = schema.safeParse(data); + if (!result.success) { + throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error)); + } + + const dto = await createSupplier(dataSource, id, data); + return GetSupplierByIdAdapter.fromDTO(dto); + }, + + onSuccess: (createdSupplier) => { + syncCreatedSupplierCaches(queryClient, createdSupplier); + }, + + onSettled: async () => { + await invalidateSupplierListQueries(queryClient); + }, + }); +}; diff --git a/modules/supplier/src/web/shared/hooks/use-supplier-delete-mutation.ts b/modules/supplier/src/web/shared/hooks/use-supplier-delete-mutation.ts new file mode 100644 index 00000000..331f38f5 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/use-supplier-delete-mutation.ts @@ -0,0 +1,53 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { deleteSupplierById } from "../api"; + +import { SUPPLIER_DELETE_KEY } from "./keys"; +import { + type DeleteSupplierCacheContext, + finalizeDeletedSupplierCaches, + invalidateSupplierListQueries, + prepareDeleteSupplierOptimisticUpdate, + rollbackDeleteSupplierOptimisticUpdate, +} from "./supplier-cache-strategy"; + +export interface DeleteSupplierPayload { + id: string; +} + +interface DeleteSupplierContext extends DeleteSupplierCacheContext {} + +export const useSupplierDeleteMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation<{ id: string }, DefaultError, DeleteSupplierPayload, DeleteSupplierContext>({ + mutationKey: SUPPLIER_DELETE_KEY, + + mutationFn: async ({ id }) => { + if (!id) { + throw new Error("supplierId is required"); + } + + await deleteSupplierById(dataSource, id, new AbortController().signal); + return { id }; + }, + + onMutate: async ({ id }) => { + return prepareDeleteSupplierOptimisticUpdate(queryClient, id); + }, + + onError: (_error, _variables, context) => { + rollbackDeleteSupplierOptimisticUpdate(queryClient, context); + }, + + onSuccess: ({ id }) => { + finalizeDeletedSupplierCaches(queryClient, id); + }, + + onSettled: async () => { + await invalidateSupplierListQueries(queryClient); + }, + }); +}; diff --git a/modules/supplier/src/web/shared/hooks/use-supplier-get-query.ts b/modules/supplier/src/web/shared/hooks/use-supplier-get-query.ts new file mode 100644 index 00000000..61e7728e --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/use-supplier-get-query.ts @@ -0,0 +1,29 @@ +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 { Supplier } from "../entities"; + +import { SUPPLIER_QUERY_KEY } from "./keys"; + +type SupplierGetQueryOptions = { + enabled?: boolean; +}; + +export const useSupplierGetQuery = ( + supplierId?: string, + options?: SupplierGetQueryOptions +): UseQueryResult => { + const dataSource = useDataSource(); + const enabled = options?.enabled ?? Boolean(supplierId); + + return useQuery({ + queryKey: SUPPLIER_QUERY_KEY(supplierId), + queryFn: async ({ signal }) => { + const dto = await getSupplierById(dataSource, String(supplierId), signal); + return GetSupplierByIdAdapter.fromDTO(dto); + }, + enabled, + }); +}; diff --git a/modules/supplier/src/web/shared/hooks/use-supplier-update-mutation.ts b/modules/supplier/src/web/shared/hooks/use-supplier-update-mutation.ts new file mode 100644 index 00000000..ce838621 --- /dev/null +++ b/modules/supplier/src/web/shared/hooks/use-supplier-update-mutation.ts @@ -0,0 +1,54 @@ +import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { UpdateSupplierByIdRequestSchema } from "../../../common"; +import { GetSupplierByIdAdapter } from "../adapters"; +import { type SupplierUpdateInput, updateSupplierById } from "../api"; +import type { Supplier } from "../entities"; + +import { SUPPLIER_UPDATE_KEY } from "./keys"; +import { + invalidateSupplierListQueries, + syncUpdatedSupplierCaches, +} from "./supplier-cache-strategy"; +import { toValidationErrors } from "./to-validation-errors"; + +type UpdateSupplierContext = {}; + +type UpdateSupplierPayload = { + id: string; + data: SupplierUpdateInput; +}; + +export const useSupplierUpdateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + const schema = UpdateSupplierByIdRequestSchema; + + return useMutation({ + mutationKey: SUPPLIER_UPDATE_KEY, + + mutationFn: async ({ id, data }) => { + if (!id) { + throw new Error("supplierId is required"); + } + + const result = schema.safeParse(data); + if (!result.success) { + throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error)); + } + + const dto = await updateSupplierById(dataSource, id, data); + return GetSupplierByIdAdapter.fromDTO(dto); + }, + + onSuccess: (updatedSupplier) => { + syncUpdatedSupplierCaches(queryClient, updatedSupplier); + }, + + onSettled: async () => { + await invalidateSupplierListQueries(queryClient); + }, + }); +}; diff --git a/modules/supplier/src/web/shared/index.ts b/modules/supplier/src/web/shared/index.ts new file mode 100644 index 00000000..6a71a351 --- /dev/null +++ b/modules/supplier/src/web/shared/index.ts @@ -0,0 +1,4 @@ +export * from "./api"; +export * from "./constants"; +export * from "./entities"; +export * from "./hooks"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b359895..1ba88971 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -702,6 +702,9 @@ importers: '@erp/core': specifier: workspace:* version: link:../core + '@hookform/resolvers': + specifier: ^5.0.1 + version: 5.2.2(react-hook-form@7.72.1(react@19.2.0)) '@repo/i18next': specifier: workspace:* version: link:../../packages/i18n @@ -714,15 +717,45 @@ importers: '@repo/rdx-logger': specifier: workspace:* version: link:../../packages/rdx-logger + '@repo/rdx-ui': + specifier: workspace:* + version: link:../../packages/rdx-ui '@repo/rdx-utils': specifier: workspace:* version: link:../../packages/rdx-utils + '@repo/shadcn-ui': + specifier: workspace:* + version: link:../../packages/shadcn-ui + '@tanstack/react-query': + specifier: ^5.90.6 + version: 5.90.6(react@19.2.0) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) express: specifier: ^4.18.2 version: 4.21.2 + lucide-react: + specifier: ^0.577.0 + version: 0.577.0(react@19.2.0) + react-data-table-component: + specifier: ^7.7.0 + version: 7.7.0(react@19.2.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + react-hook-form: + specifier: ^7.72.1 + version: 7.72.1(react@19.2.0) + react-i18next: + specifier: ^16.2.4 + version: 16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + react-router-dom: + specifier: ^6.26.0 + version: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) sequelize: specifier: ^6.37.8 version: 6.37.8(mysql2@3.15.3)(pg-hstore@2.3.4) + use-debounce: + specifier: ^10.0.5 + version: 10.0.6(react@19.2.0) zod: specifier: ^4.1.11 version: 4.1.12 @@ -730,6 +763,18 @@ importers: '@types/express': specifier: ^4.17.21 version: 4.17.25 + '@types/react': + specifier: ^19.1.2 + version: 19.2.2 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 + react: + specifier: ^19.1.0 + version: 19.2.0 + react-dom: + specifier: ^19.1.0 + version: 19.2.0(react@19.2.0) typescript: specifier: ^5.9.3 version: 5.9.3