From 8e547ff551f74caefb328adcce1cf66339642338 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 31 Mar 2026 10:21:48 +0200 Subject: [PATCH] Customers --- biome.json | 3 +- .../domain/customer-snapshot-builder.ts | 2 +- .../domain/customer-snapshot.interface.ts | 2 +- .../get-customer-by-id.response.dto.ts | 2 +- .../use-customer-sheet.controller.ts | 48 +++ .../use-list-customers-page.controller.ts | 4 + .../blocks/customer-sheet/customer-sheet.tsx | 400 ++++++++++++++++++ .../list/ui/blocks/customer-sheet/index.ts | 1 + .../blocks/customers-grid/customers-grid.tsx | 4 +- .../use-customer-grid-columns.tsx | 19 +- .../customers/src/web/list/ui/blocks/index.ts | 1 + .../web/list/ui/components/address-cell.tsx | 4 +- .../web/list/ui/components/contact-cell.tsx | 18 +- .../web/list/ui/pages/list-customers-page.tsx | 20 +- .../adapters/get-customer-by-id.adapter.ts | 67 +-- .../shared/adapters/list-customers.adapter.ts | 24 +- .../web/shared/api/get-customer-by-id.api.ts | 4 +- .../entities/customer-list-row.entity.ts | 25 +- .../shared/hooks/use-customer-get-query.ts | 11 +- .../web/view/adapters/customer-dto.adapter.ts | 13 - .../customers/src/web/view/adapters/index.ts | 1 - .../use-customer-view.controller.ts | 22 +- modules/customers/src/web/view/types/index.ts | 1 - modules/customers/src/web/view/types/types.ts | 3 - .../web/view/ui/pages/customer-view-page.tsx | 44 +- packages/rdx-ui/src/hooks/index.ts | 1 + packages/rdx-ui/src/hooks/sheet/index.ts | 2 + .../src/hooks/sheet/sheet-state-types.ts | 28 ++ .../rdx-ui/src/hooks/sheet/use-sheet-state.ts | 56 +++ 29 files changed, 689 insertions(+), 141 deletions(-) create mode 100644 modules/customers/src/web/list/controllers/use-customer-sheet.controller.ts create mode 100644 modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx create mode 100644 modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts delete mode 100644 modules/customers/src/web/view/adapters/customer-dto.adapter.ts delete mode 100644 modules/customers/src/web/view/adapters/index.ts delete mode 100644 modules/customers/src/web/view/types/index.ts delete mode 100644 modules/customers/src/web/view/types/types.ts create mode 100644 packages/rdx-ui/src/hooks/sheet/index.ts create mode 100644 packages/rdx-ui/src/hooks/sheet/sheet-state-types.ts create mode 100644 packages/rdx-ui/src/hooks/sheet/use-sheet-state.ts diff --git a/biome.json b/biome.json index da4a4040..ac28b8ec 100644 --- a/biome.json +++ b/biome.json @@ -226,7 +226,8 @@ "useValidAnchor": "error", "useValidAriaProps": "error", "useValidAriaValues": "error", - "useValidLang": "error" + "useValidLang": "error", + "useButtonType": "info" }, "performance": { "noAccumulatingSpread": "warn", diff --git a/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot-builder.ts b/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot-builder.ts index b349aba4..b1dc7da2 100644 --- a/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot-builder.ts +++ b/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot-builder.ts @@ -46,7 +46,7 @@ export class CustomerFullSnapshotBuilder implements ICustomerFullSnapshotBuilder legal_record: maybeToEmptyString(customer.legalRecord, (value) => value.toPrimitive()), - default_taxes: customer.defaultTaxes.getAll().map((tax) => tax.toString()), + default_taxes: customer.defaultTaxes.toKey(), language_code: customer.languageCode.toPrimitive(), currency_code: customer.currencyCode.toPrimitive(), diff --git a/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot.interface.ts b/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot.interface.ts index 22508cbf..b7a3d2e3 100644 --- a/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot.interface.ts +++ b/modules/customers/src/api/application/snapshot-builders/domain/customer-snapshot.interface.ts @@ -30,7 +30,7 @@ export interface ICustomerFullSnapshot { legal_record: string; - default_taxes: string[]; + default_taxes: string; language_code: string; currency_code: string; diff --git a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts index 6ae75cd2..e07fd8cd 100644 --- a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts +++ b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts @@ -30,7 +30,7 @@ export const GetCustomerByIdResponseSchema = z.object({ legal_record: z.string(), - default_taxes: z.array(z.string()), + default_taxes: z.string(), status: z.string(), language_code: z.string(), currency_code: z.string(), diff --git a/modules/customers/src/web/list/controllers/use-customer-sheet.controller.ts b/modules/customers/src/web/list/controllers/use-customer-sheet.controller.ts new file mode 100644 index 00000000..ccf0364f --- /dev/null +++ b/modules/customers/src/web/list/controllers/use-customer-sheet.controller.ts @@ -0,0 +1,48 @@ +import { useSheetState } from "@repo/rdx-ui/hooks"; +import { useCallback, useState } from "react"; + +import { useCustomerGetQuery } from "../../shared/hooks"; + +export const useCustomerSheetController = (initialCustomerId = "") => { + const [customerId, setCustomerId] = useState(initialCustomerId); + const sheetState = useSheetState({ + defaultOpen: false, + defaultPinned: false, + defaultMode: "view", + }); + + const query = useCustomerGetQuery(customerId, { + enabled: Boolean(customerId), + }); + + const openCustomerSheet = useCallback( + (nextCustomerId: string) => { + setCustomerId(nextCustomerId); + sheetState.openInMode("view"); + }, + [sheetState] + ); + + const closeCustomerSheet = useCallback(() => { + sheetState.closeSheet(); + }, [sheetState]); + + return { + customer: query.data, + customerId, + setCustomerId, + + openCustomerSheet, + closeCustomerSheet, + + isLoading: query.isLoading, + isFetching: query.isFetching, + + isError: query.isError, + error: query.error, + + refetch: query.refetch, + + sheetState, + }; +}; diff --git a/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts b/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts index d29df55c..7d115b21 100644 --- a/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts +++ b/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts @@ -1,9 +1,13 @@ +import { useCustomerSheetController } from "./use-customer-sheet.controller"; import { useListCustomersController } from "./use-list-customers.controller"; export function useListCustomersPageController() { const listCtrl = useListCustomersController(); + const sheetCtrl = useCustomerSheetController(); + return { listCtrl, + sheetCtrl, }; } diff --git a/modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx b/modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx new file mode 100644 index 00000000..0547cee0 --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx @@ -0,0 +1,400 @@ +import { + Avatar, + AvatarFallback, + Badge, + Button, + Separator, + Sheet, + SheetContent, + SheetTitle, +} from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { + Building2Icon, + CopyIcon, + ExternalLinkIcon, + FileTextIcon, + GlobeIcon, + MailIcon, + MapPinIcon, + PencilIcon, + PhoneIcon, + PinIcon, + PinOffIcon, + TrendingUpIcon, + UserIcon, + XIcon, +} from "lucide-react"; + +import type { Customer } from "../../../../shared"; + +interface CustomerSheetProps { + customer?: Customer; + open: boolean; + onOpenChange: (open: boolean) => void; + pinned: boolean; + onPinnedChange: (pinned: boolean) => void; + onEdit?: (customer: Customer) => void; +} + +// Datos de ejemplo ERP para demostración +const mockERPData = { + totalPurchases: 45750.8, + purchasesThisYear: 12350.25, + lastInvoices: [ + { + id: "1", + number: "FAC-2024-0156", + date: "2024-01-15", + amount: 1250.0, + status: "paid" as const, + }, + { + id: "2", + number: "FAC-2024-0142", + date: "2024-01-08", + amount: 890.5, + status: "paid" as const, + }, + { + id: "3", + number: "FAC-2024-0128", + date: "2023-12-22", + amount: 2100.0, + status: "pending" as const, + }, + { + id: "4", + number: "FAC-2023-0098", + date: "2023-11-30", + amount: 750.25, + status: "overdue" as const, + }, + ], +}; + +export const CustomerSheet = ({ + customer, + open, + onOpenChange, + pinned, + onPinnedChange, + onEdit, +}: CustomerSheetProps) => { + const getInitials = (name: string) => { + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("es-ES", { + style: "currency", + currency: "EUR", + }).format(amount); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("es-ES", { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + const getGoogleMapsUrl = (customer: Customer) => { + const fullAddress = `${customer.street}, ${customer.postalCode} ${customer.city}, ${customer.province}, ${customer.country}`; + return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`; + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => { + switch (status) { + case "paid": + return ( + + Pagada + + ); + case "pending": + return ( + + Pendiente + + ); + case "overdue": + return ( + + Vencida + + ); + } + }; + + // Combinar datos del cliente con datos mock de ERP + const customerWithERP = { ...customer, ...mockERPData }; + + return ( + + +
+ Ficha de cliente +
+ +
+
+ + {!pinned && ( + + )} +
+
+ + {/* Contenido scrolleable */} + {customer ? ( +
+ {/* Info principal del cliente */} +
+
+ + + {getInitials(customer.name)} + + +
+
+

+ {customer.name} +

+ + {customer.status === "active" ? "Activo" : "Inactivo"} + +
+ {customer.tradeName && ( +

{customer.tradeName}

+ )} +
+ + {customer.isCompany ? ( + <> + + Empresa + + ) : ( + <> + + Particular + + )} + + +
+
+
+
+ + + + {/* Datos de contacto */} +
+

Contacto

+
+ + + {customer.primaryEmail} + + {customer.secondaryEmail && ( + + + {customer.secondaryEmail} + + )} + {customer.primaryPhone && ( + + + {customer.primaryPhone} + + )} + {customerWithERP.primaryMobile && ( + + + {customerWithERP.primaryMobile} + + )} + {customerWithERP.website && ( + + + {customerWithERP.website} + + + )} +
+
+ + + + {/* Dirección */} + + + + + {/* Volumen de compras */} +
+

+ + Volumen de compras +

+
+
+

Total histórico

+

+ {formatCurrency(customerWithERP.totalPurchases || 0)} +

+
+
+

Este año

+

+ {formatCurrency(customerWithERP.purchasesThisYear || 0)} +

+
+
+
+ + + + {/* Últimas facturas */} +
+

+ + Últimas facturas +

+
+ {customerWithERP.lastInvoices?.map((invoice) => ( + + ))} +
+ +
+
+ ) : ( +
+

Selecciona un cliente

+
+ )} + + {/* Footer con acciones */} +
+
+ + +
+
+
+
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts b/modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts new file mode 100644 index 00000000..1ca7fe4b --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts @@ -0,0 +1 @@ +export * from "./customer-sheet"; diff --git a/modules/customers/src/web/list/ui/blocks/customers-grid/customers-grid.tsx b/modules/customers/src/web/list/ui/blocks/customers-grid/customers-grid.tsx index b85b1bcf..e6db73c1 100644 --- a/modules/customers/src/web/list/ui/blocks/customers-grid/customers-grid.tsx +++ b/modules/customers/src/web/list/ui/blocks/customers-grid/customers-grid.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "../../../../i18n"; import type { CustomerList, CustomerListRow } from "../../../../shared"; interface CustomersGridProps { - data: CustomerList; + data?: CustomerList; loading: boolean; fetching?: boolean; @@ -33,7 +33,7 @@ export const CustomersGrid = ({ }: CustomersGridProps) => { const navigate = useNavigate(); const { t } = useTranslation(); - const { items, total_items } = data; + const { items, total_items } = data || { items: [], total_items: 0 }; if (loading) return ( diff --git a/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx b/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx index d9216d50..b03330f1 100644 --- a/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx +++ b/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx @@ -23,6 +23,7 @@ import { AddressCell, ContactCell, Initials } from "../../components"; type GridActionHandlers = { onEditClick?: (customer: CustomerListRow) => void; onViewClick?: (customer: CustomerListRow) => void; + onSummaryClick?: (customer: CustomerListRow) => void; onDeleteClick?: (customer: CustomerListRow) => void; }; @@ -30,7 +31,7 @@ export function useCustomersGridColumns( actionHandlers: GridActionHandlers = {} ): ColumnDef[] { const { t } = useTranslation(); - const { onEditClick, onViewClick, onDeleteClick } = actionHandlers; + const { onEditClick, onViewClick, onDeleteClick, onSummaryClick } = actionHandlers; return React.useMemo[]>( () => [ @@ -53,7 +54,7 @@ export function useCustomersGridColumns( return (