From e33733165000234e52a521ad55056367fc1ed691 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 20 Oct 2025 11:35:24 +0200 Subject: [PATCH] Clientes y Facturas de cliente --- modules/core/src/web/components/index.ts | 1 + .../core/src/web/components/page-header.tsx | 48 ++++ .../src/web/components/index.tsx | 1 - .../src/web/components/page-header.tsx | 50 ---- .../src/web/pages/list/invoices-list-grid.tsx | 2 +- .../src/web/pages/list/invoices-list-page.tsx | 5 +- .../web/pages/update/invoice-update-comp.tsx | 4 +- modules/customers/package.json | 1 + .../response/list-customers.response.dto.ts | 3 - .../web/components/customers-list-grid.tsx | 205 ---------------- modules/customers/src/web/components/index.ts | 1 - modules/customers/src/web/customer-routes.tsx | 7 +- .../src/web/hooks/use-customers-query.tsx | 44 ++-- .../customers/src/web/pages/customer-list.tsx | 35 --- modules/customers/src/web/pages/index.ts | 3 +- .../web/pages/list/customers-list-grid.tsx | 160 +++++++++++++ .../web/pages/list/customers-list-page.tsx | 120 ++++++++++ modules/customers/src/web/pages/list/index.ts | 1 + .../pages/list/use-customers-list-columns.tsx | 218 ++++++++++++++++++ .../web/pages/update/customer-update-page.tsx | 4 +- .../src/web/pages/view/customer-view-page.tsx | 6 +- .../schemas/customer-resume.form.schema.ts | 7 + .../src/web/schemas/customer.api.schema.ts | 10 +- modules/customers/src/web/schemas/index.ts | 1 + .../src/components/form/TextAreaField.tsx | 2 +- .../rdx-ui/src/components/form/TextField.tsx | 2 +- .../src/components/layout/app-content.tsx | 2 +- .../src/components/layout/app-header.tsx | 2 +- .../src/components/layout/app-layout.tsx | 2 +- pnpm-lock.yaml | 3 + 30 files changed, 618 insertions(+), 332 deletions(-) create mode 100644 modules/core/src/web/components/page-header.tsx delete mode 100644 modules/customer-invoices/src/web/components/page-header.tsx delete mode 100644 modules/customers/src/web/components/customers-list-grid.tsx delete mode 100644 modules/customers/src/web/pages/customer-list.tsx create mode 100644 modules/customers/src/web/pages/list/customers-list-grid.tsx create mode 100644 modules/customers/src/web/pages/list/customers-list-page.tsx create mode 100644 modules/customers/src/web/pages/list/index.ts create mode 100644 modules/customers/src/web/pages/list/use-customers-list-columns.tsx create mode 100644 modules/customers/src/web/schemas/customer-resume.form.schema.ts diff --git a/modules/core/src/web/components/index.ts b/modules/core/src/web/components/index.ts index f25110c6..5fc6d843 100644 --- a/modules/core/src/web/components/index.ts +++ b/modules/core/src/web/components/index.ts @@ -3,3 +3,4 @@ import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; ModuleRegistry.registerModules([AllCommunityModule]); export * from "./form"; +export * from "./page-header"; diff --git a/modules/core/src/web/components/page-header.tsx b/modules/core/src/web/components/page-header.tsx new file mode 100644 index 00000000..c5f848c3 --- /dev/null +++ b/modules/core/src/web/components/page-header.tsx @@ -0,0 +1,48 @@ +import { Button } from '@repo/shadcn-ui/components'; +import { cn } from '@repo/shadcn-ui/lib/utils'; +import { ChevronLeftIcon } from 'lucide-react'; +// features/common/components/page-header.tsx +import type { ReactNode } from "react"; + + +interface PageHeaderProps { + /** Icono que aparece a la izquierda del título */ + icon?: ReactNode; + /** Contenido del título (texto plano o nodo complejo) */ + title: ReactNode; + /** Descripción secundaria debajo del título */ + description?: ReactNode; + /** Estado opcional (ej. "draft", "paid") */ + status?: string; + /** Contenido del lado derecho (botones, menús, etc.) */ + rightSlot?: ReactNode; + + className?: string; +} + +export function PageHeader({ icon, title, description, status, rightSlot, className }: PageHeaderProps) { + return ( +
+
+ {/* Lado izquierdo */} +
+ + {icon &&
{icon}
} + +
+
+

{title}

+
+ {description &&

{description}

} +
+
+ + {/* Lado derecho parametrizable */} + {rightSlot &&
{rightSlot}
} +
+ +
+ ); +} diff --git a/modules/customer-invoices/src/web/components/index.tsx b/modules/customer-invoices/src/web/components/index.tsx index f443819c..897f578e 100644 --- a/modules/customer-invoices/src/web/components/index.tsx +++ b/modules/customer-invoices/src/web/components/index.tsx @@ -6,5 +6,4 @@ export * from "./customer-invoices-layout"; export * from "./editor"; export * from "./editor/invoice-tax-summary"; export * from "./editor/invoice-totals"; -export * from "./page-header"; diff --git a/modules/customer-invoices/src/web/components/page-header.tsx b/modules/customer-invoices/src/web/components/page-header.tsx deleted file mode 100644 index c71011e0..00000000 --- a/modules/customer-invoices/src/web/components/page-header.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Button } from '@repo/shadcn-ui/components'; -import { cn } from '@repo/shadcn-ui/lib/utils'; -import { ChevronLeftIcon } from 'lucide-react'; -// features/common/components/page-header.tsx -import type { ReactNode } from "react"; -import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge"; - -interface PageHeaderProps { - /** Icono que aparece a la izquierda del título */ - icon?: ReactNode; - /** Contenido del título (texto plano o nodo complejo) */ - title: ReactNode; - /** Descripción secundaria debajo del título */ - description?: ReactNode; - /** Estado opcional (ej. "draft", "paid") */ - status?: string; - /** Contenido del lado derecho (botones, menús, etc.) */ - rightSlot?: ReactNode; - - className?: string; -} - -export function PageHeader({ icon, title, description, status, rightSlot, className }: PageHeaderProps) { - return ( -
-
-
- {/* Lado izquierdo */} -
- - {icon &&
{icon}
} - -
-
-

{title}

- {status && } -
- {description &&

{description}

} -
-
- - {/* Lado derecho parametrizable */} - {rightSlot &&
{rightSlot}
} -
-
-
- ); -} diff --git a/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx b/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx index a8a94ae6..1b1d3357 100644 --- a/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx +++ b/modules/customer-invoices/src/web/pages/list/invoices-list-grid.tsx @@ -41,6 +41,7 @@ export const InvoicesListGrid = ({ }: InvoiceUpdateCompProps) => { const { t } = useTranslation(); const navigate = useNavigate(); + const { items, total_items } = invoicesPage; // Hook con Sheet de shadcn const preview = usePinnedPreviewSheet({ @@ -57,7 +58,6 @@ export const InvoicesListGrid = ({ onSendEmail: (invoice) => null, //sendInvoiceEmail(inv.id), onDelete: (invoice) => null, //confirmDelete(inv.id), }); - const { items, total_items } = invoicesPage; // Navegación accesible (click o teclado) const goToRow = useCallback( diff --git a/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx b/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx index 6cba46c8..84fef8a2 100644 --- a/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx +++ b/modules/customer-invoices/src/web/pages/list/invoices-list-page.tsx @@ -1,10 +1,10 @@ +import { PageHeader } from '@erp/core/components'; import { ErrorAlert } from '@erp/customers/components'; -import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components"; +import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components"; import { Button } from "@repo/shadcn-ui/components"; import { PlusIcon } from "lucide-react"; import { useMemo, useState } from 'react'; import { useNavigate } from "react-router-dom"; -import { PageHeader } from '../../components'; import { useInvoicesQuery } from '../../hooks'; import { useTranslation } from "../../i18n"; import { invoiceResumeDtoToFormAdapter } from '../../schemas/invoice-resume-dto.adapter'; @@ -79,7 +79,6 @@ export const InvoiceListPage = () => { return ( <> - { - const { t } = useTranslation(); - const navigate = useNavigate(); - - const { - data: customersData, - isLoading: isLoadingCustomers, - isError: isLoadError, - error: loadError, - } = useCustomersQuery({ - pagination: { - pageSize: 999, - }, - }); - - // Column Definitions: Defines & controls grid columns. - const [columnDefs] = useState([ - { field: "name", headerName: t("pages.list.grid_columns.name"), minWidth: 300 }, - { - field: "tin", - headerName: t("pages.list.grid_columns.tin"), - maxWidth: 120, - }, - { - field: "city", - headerName: t("pages.list.grid_columns.city"), - }, - { - field: "email_primary", - headerName: t("pages.list.grid_columns.email"), - }, - { - field: "phone_primary", - headerName: t("pages.list.grid_columns.phone"), - maxWidth: 120, - }, - - { - field: "mobile_primary", - headerName: t("pages.list.grid_columns.mobile"), - maxWidth: 120, - }, - { - field: "status", - headerName: t("pages.list.grid_columns.status"), - maxWidth: 135, - cellRenderer: (params: ValueFormatterParams) => { - return ; - }, - }, - { - colId: "actions", - headerName: t("pages.list.grid_columns.actions", "Actions"), - cellRenderer: (params: ValueFormatterParams) => { - const { data } = params; - return ( - - ); - }, - }, - ]); - - // Navegación centralizada (click/teclado) - const goToRow = useCallback( - (id: string, newTab = false) => { - const url = `/customers/${id}`; - if (newTab) { - window.open(url, "_blank", "noopener,noreferrer"); - } else { - navigate(url); - } - }, - [navigate] - ); - - const onRowClicked = useCallback( - (e: RowClickedEvent) => { - if (!e.data) return; - // Soporta Ctrl/Cmd click para nueva pestaña - const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey); - goToRow(e.data.id, newTab); - }, - [goToRow] - ); - - const onCellKeyDown = useCallback( - (e: CellKeyDownEvent) => { - if (!e.data) return; - const key = e.event.key; - // Enter o Space disparan navegación - if (key === "Enter" || key === " ") { - e.event.preventDefault(); - goToRow(e.data.id); - } - // Ctrl/Cmd+Enter abre en nueva pestaña - if ((e.event.ctrlKey || e.event.metaKey) && key === "Enter") { - e.event.preventDefault(); - goToRow(e.data.id, true); - } - }, - [goToRow] - ); - - const autoSizeStrategy = useMemo< - | SizeColumnsToFitGridStrategy - | SizeColumnsToFitProvidedWidthStrategy - | SizeColumnsToContentStrategy - >(() => { - return { - type: "fitGridWidth", - defaultMinWidth: 100, - columnLimits: [{ colId: "actions", minWidth: 75, maxWidth: 75 }], - }; - }, []); - - const gridOptions: GridOptions = useMemo( - () => ({ - columnDefs: columnDefs, - autoSizeStrategy: autoSizeStrategy, - defaultColDef: { - editable: false, - flex: 1, - filter: false, - sortable: false, - resizable: true, - }, - pagination: true, - paginationPageSize: 15, - paginationPageSizeSelector: [10, 15, 20, 30, 50], - localeText: AG_GRID_LOCALE_ES, - - // Evita conflictos con selección si la usas - suppressRowClickSelection: true, - // Clase visual de fila clickeable - getRowClass: () => "clickable-row", - // Accesibilidad con teclado - onCellKeyDown, - // Click en cualquier parte de la fila - onRowClicked, - // IDs estables (opcional pero recomendado) - getRowId: (params) => params.data.id, - }), - [autoSizeStrategy, columnDefs, onCellKeyDown, onRowClicked] - ); - - if (isLoadError) { - return ( - <> - - - ); - } - - // Container: Defines the grid's theme & dimensions. - return ( -
- -
- ); -}; diff --git a/modules/customers/src/web/components/index.ts b/modules/customers/src/web/components/index.ts index 7c55476e..7d0fe3aa 100644 --- a/modules/customers/src/web/components/index.ts +++ b/modules/customers/src/web/components/index.ts @@ -1,7 +1,6 @@ export * from "./client-selector-modal"; export * from "./customer-modal-selector"; export * from "./customers-layout"; -export * from "./customers-list-grid"; export * from "./editor"; export * from "./error-alert"; export * from "./not-found-card"; diff --git a/modules/customers/src/web/customer-routes.tsx b/modules/customers/src/web/customer-routes.tsx index fef329ad..968baada 100644 --- a/modules/customers/src/web/customer-routes.tsx +++ b/modules/customers/src/web/customer-routes.tsx @@ -7,9 +7,10 @@ const CustomersLayout = lazy(() => import("./components").then((m) => ({ default: m.CustomersLayout })) ); -const CustomersList = lazy(() => import("./pages").then((m) => ({ default: m.CustomersList }))); +const CustomersList = lazy(() => import("./pages").then((m) => ({ default: m.CustomersListPage }))); const CustomerView = lazy(() => import("./pages").then((m) => ({ default: m.CustomerViewPage }))); const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage }))); +const CustomerUpdate = lazy(() => import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))); export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => { return [ @@ -21,11 +22,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => { ), children: [ - /*{ path: "", index: true, element: }, // index + { path: "", index: true, element: }, // index { path: "list", element: }, { path: "create", element: }, { path: ":id", element: }, - { path: ":id/edit", element: },*/ + { path: ":id/edit", element: }, // /*{ path: "create", element: }, diff --git a/modules/customers/src/web/hooks/use-customers-query.tsx b/modules/customers/src/web/hooks/use-customers-query.tsx index 2482fe57..69f76974 100644 --- a/modules/customers/src/web/hooks/use-customers-query.tsx +++ b/modules/customers/src/web/hooks/use-customers-query.tsx @@ -1,22 +1,40 @@ -import { useDataSource, useQueryKey } from "@erp/core/hooks"; -import { useQuery } from "@tanstack/react-query"; -import { CustomersListData } from "../schemas"; +import { CriteriaDTO } from '@erp/core'; +import { useDataSource } from "@erp/core/hooks"; +import { DefaultError, QueryKey, useQuery } from "@tanstack/react-query"; +import { CustomersPage } from '../schemas'; + + +export const CUSTOMERS_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [ + "customer_invoices", { + pageNumber: criteria.pageNumber ?? 0, + pageSize: criteria.pageSize ?? 10, + q: criteria.q ?? "", + filters: criteria.filters ?? [], + orderBy: criteria.orderBy ?? "", + order: criteria.order ?? "", + }, +]; + +type CustomersQueryOptions = { + enabled?: boolean; + criteria?: CriteriaDTO +}; // Obtener todos los clientes -export const useCustomersQuery = (params?: any) => { +export const useCustomersQuery = (options?: CustomersQueryOptions) => { const dataSource = useDataSource(); - const keys = useQueryKey(); + const enabled = options?.enabled ?? true; + const criteria = options?.criteria ?? {}; - return useQuery({ - queryKey: keys().data().resource("customers").action("list").params(params).get(), - queryFn: async (context) => { - const { signal } = context; - const customers = await dataSource.getList("customers", { + return useQuery({ + queryKey: CUSTOMERS_QUERY_KEY(criteria), + queryFn: async ({ signal }) => { + return await dataSource.getList("customers", { signal, - ...params, + ...criteria, }); - - return customers as CustomersListData; }, + enabled, + placeholderData: (previousData, previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`) }); }; diff --git a/modules/customers/src/web/pages/customer-list.tsx b/modules/customers/src/web/pages/customer-list.tsx deleted file mode 100644 index 8c2095b7..00000000 --- a/modules/customers/src/web/pages/customer-list.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; -import { Button } from "@repo/shadcn-ui/components"; -import { PlusIcon } from "lucide-react"; -import { Outlet, useNavigate } from "react-router-dom"; -import { CustomersListGrid } from "../components"; -import { useTranslation } from "../i18n"; - -export const CustomersList = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); - - return ( - <> - - -
-
-

{t("pages.list.title")}

-

{t("pages.list.description")}

-
-
- -
-
-
- -
- -
- - ); -}; diff --git a/modules/customers/src/web/pages/index.ts b/modules/customers/src/web/pages/index.ts index 5402a730..764e8887 100644 --- a/modules/customers/src/web/pages/index.ts +++ b/modules/customers/src/web/pages/index.ts @@ -1,3 +1,4 @@ export * from "./create"; -export * from "./customer-list"; +export * from "./list"; +export * from "./update"; export * from "./view"; diff --git a/modules/customers/src/web/pages/list/customers-list-grid.tsx b/modules/customers/src/web/pages/list/customers-list-grid.tsx new file mode 100644 index 00000000..e6a2f802 --- /dev/null +++ b/modules/customers/src/web/pages/list/customers-list-grid.tsx @@ -0,0 +1,160 @@ + +import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; +import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, Spinner } from '@repo/shadcn-ui/components'; +import { SearchIcon, XIcon } from 'lucide-react'; +import { useCallback, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "../../i18n"; +import { CustomerSummaryFormData, CustomersPageFormData } from '../../schemas'; +import { useCustomersListColumns } from './use-customers-list-columns'; + + +export type CustomerUpdateCompProps = { + customersPage: CustomersPageFormData; + loading?: boolean; + + pageIndex: number; + pageSize: number; + onPageChange?: (pageNumber: number) => void; + onPageSizeChange?: (pageSize: number) => void; + + searchValue: string; + onSearchChange: (value: string) => void; + + onRowClick?: (row: CustomerSummaryFormData, index: number, event: React.MouseEvent) => void; +} + +export const CustomersListGrid = ({ + customersPage, + loading, + pageIndex, + pageSize, + onPageChange, + onPageSizeChange, + searchValue, onSearchChange, + onRowClick +}: CustomerUpdateCompProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { items, total_items } = customersPage; + + const [statusFilter, setStatusFilter] = useState("todas"); + + const columns = useCustomersListColumns({ + onEdit: (customer) => navigate(`/customers/${customer.id}/edit`), + onView: (customer) => null, //duplicateInvoice(inv.id), + onDelete: (customer) => null, //confirmDelete(inv.id), + }); + + + // Navegación centralizada (click/teclado) + const goToRow = useCallback( + (id: string, newTab = false) => { + const url = `/customers/${id}`; + if (newTab) { + window.open(url, "_blank", "noopener,noreferrer"); + } else { + navigate(url); + } + }, + [navigate] + ); + + // Handlers de búsqueda + const handleInputChange = (e: React.ChangeEvent) => onSearchChange(e.target.value); + const handleClear = () => onSearchChange(""); + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + // Envío inmediato: forzar “salto” del debounce + onSearchChange((e.target as HTMLInputElement).value); + } + }; + + /*const handleRowClick = useCallback( + (customer: CustomerSummaryFormData, _i: number, e: React.MouseEvent) => { + const url = `/customer-invoices/${customer.id}/edit`; + if (e.metaKey || e.ctrlKey) { window.open(url, "_blank", "noopener,noreferrer"); return; } + preview.open(customer); + }, + [preview] + );*/ + + if (loading) { + return ( +
+ +
+ ); + } + + // Render principal + return ( +
+ {/* Barra de filtros */} +
+
+ + + + + + + {loading && } + {!searchValue && !loading && + {t("common.search")} + } + {searchValue && !loading && + + {t("common.search")} + } + + + +
+
+
+
+ null /*handleRowClick*/} + /> +
+ + {/* + {({ item, isPinned, close, togglePin }) => ( + + )} + */} +
+
+ ); +}; \ No newline at end of file diff --git a/modules/customers/src/web/pages/list/customers-list-page.tsx b/modules/customers/src/web/pages/list/customers-list-page.tsx new file mode 100644 index 00000000..b5b47de1 --- /dev/null +++ b/modules/customers/src/web/pages/list/customers-list-page.tsx @@ -0,0 +1,120 @@ +import { PageHeader } from '@erp/core/components'; +import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components"; +import { Button } from "@repo/shadcn-ui/components"; +import { PlusIcon } from "lucide-react"; +import { useMemo, useState } from 'react'; +import { Outlet, useNavigate } from "react-router-dom"; +import { ErrorAlert } from '../../components'; +import { useCustomersQuery } from '../../hooks'; +import { useTranslation } from "../../i18n"; +import { CustomersListGrid } from './customers-list-grid'; + +export const CustomersListPage = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [search, setSearch] = useState(""); + const debouncedQ = useDebounce(search, 300); + + + const criteria = useMemo( + () => ({ + q: debouncedQ || "", + pageSize, + pageNumber: pageIndex, + }), + [pageSize, pageIndex, debouncedQ] + ); + + + const { + data: customersPageData, + isLoading, + isError, + error, + } = useCustomersQuery({ + criteria + }); + + const handlePageChange = (newPageIndex: number) => { + // TanStack usa pageIndex 0-based → API usa 0-based también + setPageIndex(newPageIndex); + }; + + const handlePageSizeChange = (newSize: number) => { + setPageSize(newSize); + setPageIndex(0); + }; + + const handleSearchChange = (value: string) => { + // Normalización ligera: recorta y colapsa espacios internos + const cleaned = value.trim().replace(/\s+/g, " "); + setSearch(cleaned); + setPageIndex(0); + }; + + + if (isError || !customersPageData) { + return ( + + + + + ); + } + + + return ( + <> + + + } + + + /> + + +
+
+

{t("pages.list.title")}

+

{t("pages.list.description")}

+
+
+ +
+
+
+
+ +
+
+ +
+ + ); +}; diff --git a/modules/customers/src/web/pages/list/index.ts b/modules/customers/src/web/pages/list/index.ts new file mode 100644 index 00000000..7cedd24d --- /dev/null +++ b/modules/customers/src/web/pages/list/index.ts @@ -0,0 +1 @@ +export * from "./customers-list-page"; diff --git a/modules/customers/src/web/pages/list/use-customers-list-columns.tsx b/modules/customers/src/web/pages/list/use-customers-list-columns.tsx new file mode 100644 index 00000000..598c8042 --- /dev/null +++ b/modules/customers/src/web/pages/list/use-customers-list-columns.tsx @@ -0,0 +1,218 @@ +import { + Avatar, + AvatarFallback, + Badge, + Button, + DropdownMenu, DropdownMenuContent, + DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger +} from '@repo/shadcn-ui/components'; +import type { ColumnDef } from "@tanstack/react-table"; +import { Building2Icon, GlobeIcon, MailIcon, MoreHorizontalIcon, PencilIcon, PhoneIcon, User2Icon } from 'lucide-react'; +import * as React from "react"; +import { useTranslation } from '../../i18n'; +import { CustomerSummaryFormData } from '../../schemas'; + +type CustomerActionHandlers = { + onEdit?: (customer: CustomerSummaryFormData) => void; + onView?: (customer: CustomerSummaryFormData) => void; + onDelete?: (customer: CustomerSummaryFormData) => void; +}; + +function shortId(id: string) { + return id ? `${id.slice(0, 4)}_${id.slice(-4)}` : "-"; +} + +// ---- Helpers UI ---- +const StatusBadge = ({ status }: { status: string }) => { + // Map visual simple; ajustar a tu catálogo real + const v = + status.toLowerCase() === "active" + ? "default" + : status.toLowerCase() === "inactive" + ? "outline" + : "secondary"; + return ( + + {status} + + ); +}; + +const KindBadge = ({ isCompany }: { isCompany: boolean }) => ( + + {isCompany ? : } + {isCompany ? "Company" : "Person"} + +); + +const Soft = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => ( +
+
+ + + {customer.email_primary || } + + {customer.email_secondary && • {customer.email_secondary}} +
+
+ + {customer.phone_primary || customer.mobile_primary || -} + {customer.phone_secondary && • {customer.phone_secondary}} + {customer.mobile_secondary && • {customer.mobile_secondary}} + {customer.fax && • fax {customer.fax}} +
+ {customer.website && ( + + )} +
+); + +const AddressCell: React.FC<{ c: CustomerSummaryFormData }> = ({ c }) => { + const line1 = [c.street, c.street2].filter(Boolean).join(", "); + const line2 = [c.postal_code, c.city].filter(Boolean).join(" "); + const line3 = [c.province, c.country].filter(Boolean).join(", "); + return ( +
+
{line1 || }
+
{[line2, line3].filter(Boolean).join(" • ")}
+
+ ); +}; + +function initials(name: string) { + const parts = name.trim().split(/\s+/).slice(0, 2); + return parts.map(p => p[0]?.toUpperCase() ?? "").join("") || "?"; +} + +function safeHttp(url: string) { + if (!url) return "#"; + if (/^https?:\/\//i.test(url)) return url; + return `https://${url}`; +} + +export function useCustomersListColumns( + handlers: CustomerActionHandlers = {} +): ColumnDef[] { + const { t } = useTranslation(); + const { + onEdit, onView, onDelete, + } = handlers; + + return React.useMemo[]>(() => [ + // Identidad + estado + metadatos (columna compuesta) + { + id: "identity", + header: "Customer", + accessorFn: (row) => row.name, // para ordenar/buscar por nombre + enableHiding: false, + size: 380, + cell: ({ row }) => { + const c = row.original; + const isCompany = String(c.is_company).toLowerCase() === "true"; + return ( +
+ + {initials(c.name)} + +
+
+ {c.name} + {c.trade_name && ({c.trade_name})} +
+
+ {c.tin && {c.tin}} +
+
+ + + {c.reference && Ref: {c.reference}} +
+
+
+ ); + }, + }, + + // Contacto (emails, teléfonos, web) + { + id: "contact", + header: "Contact", + accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`, + size: 420, + cell: ({ row }) => , + }, + + // Dirección (múltiples campos en bloque) + { + id: "address", + header: "Address", + accessorFn: (r) => + `${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`, + size: 360, + cell: ({ row }) => , + }, + + // Acciones + { + id: "actions", + header: () => Actions, + size: 72, + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const customer = row.original; + const { website, email_primary } = customer; + return ( +
+
+ + + + + + + Actions + + onView?.(customer)}>Open + onEdit?.(customer)}>Edit + + window.open(safeHttp(website), "_blank")}> + Visit website + + navigator.clipboard.writeText(email_primary)}> + Copy email + + + onDelete?.(customer)} + > + Delete + + + +
+
+ ); + }, + }, + ], [t, onEdit, onView, onDelete]); +} diff --git a/modules/customers/src/web/pages/update/customer-update-page.tsx b/modules/customers/src/web/pages/update/customer-update-page.tsx index 0adadfdb..0fcbc3d7 100644 --- a/modules/customers/src/web/pages/update/customer-update-page.tsx +++ b/modules/customers/src/web/pages/update/customer-update-page.tsx @@ -3,8 +3,8 @@ import { useNavigate } from "react-router-dom"; import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client"; import { - FormCommitButtonGroup, UnsavedChangesProvider, + UpdateCommitButtonGroup, useHookForm, useUrlParamId, } from "@erp/core/hooks"; @@ -137,7 +137,7 @@ export const CustomerUpdatePage = () => { {t("pages.update.description")}

- { if (isLoadError) { return ( <> - { return ( <> -
{/* Header */}
-
+
{customer?.is_company ? ( ) : ( diff --git a/modules/customers/src/web/schemas/customer-resume.form.schema.ts b/modules/customers/src/web/schemas/customer-resume.form.schema.ts new file mode 100644 index 00000000..df4cac8c --- /dev/null +++ b/modules/customers/src/web/schemas/customer-resume.form.schema.ts @@ -0,0 +1,7 @@ +import { CustomerSummary, CustomersPage } from "./customer.api.schema"; + +export type CustomerSummaryFormData = CustomerSummary & {}; + +export type CustomersPageFormData = CustomersPage & { + items: CustomerSummaryFormData[]; +}; diff --git a/modules/customers/src/web/schemas/customer.api.schema.ts b/modules/customers/src/web/schemas/customer.api.schema.ts index cdc2dd6a..2bab9a9a 100644 --- a/modules/customers/src/web/schemas/customer.api.schema.ts +++ b/modules/customers/src/web/schemas/customer.api.schema.ts @@ -1,10 +1,11 @@ import { z } from "zod/v4"; +import { PaginationSchema } from "@erp/core"; import { ArrayElement } from "@repo/rdx-utils"; import { CreateCustomerRequestSchema, GetCustomerByIdResponseSchema, - ListCustomersResponseDTO, + ListCustomersResponseSchema, UpdateCustomerByIdRequestSchema, } from "../../common"; @@ -19,7 +20,12 @@ export type CustomerCreateInput = z.infer; // Cuerp export type CustomerUpdateInput = z.infer; // Cuerpo para actualizar // Resultado de consulta con criteria (paginado, etc.) -export type CustomersPage = ListCustomersResponseDTO; +export const CustomersPageSchema = ListCustomersResponseSchema.omit({ + metadata: true, +}); + +export type PaginatedResponse = z.infer; +export type CustomersPage = z.infer; // Ítem simplificado dentro del listado (no toda la entidad) export type CustomerSummary = Omit, "metadata">; diff --git a/modules/customers/src/web/schemas/index.ts b/modules/customers/src/web/schemas/index.ts index 6f1fb312..1918108f 100644 --- a/modules/customers/src/web/schemas/index.ts +++ b/modules/customers/src/web/schemas/index.ts @@ -1,2 +1,3 @@ +export * from "./customer-resume.form.schema"; export * from "./customer.api.schema"; export * from "./customer.form.schema"; diff --git a/packages/rdx-ui/src/components/form/TextAreaField.tsx b/packages/rdx-ui/src/components/form/TextAreaField.tsx index af775de7..a1ad0726 100644 --- a/packages/rdx-ui/src/components/form/TextAreaField.tsx +++ b/packages/rdx-ui/src/components/form/TextAreaField.tsx @@ -62,7 +62,7 @@ export function TextAreaField({ {...inputRest} disabled={disabled} aria-disabled={disabled} - className={cn(inputClassName)} + className={cn("bg-background", inputClassName)} /> diff --git a/packages/rdx-ui/src/components/form/TextField.tsx b/packages/rdx-ui/src/components/form/TextField.tsx index 7b49110c..7288a223 100644 --- a/packages/rdx-ui/src/components/form/TextField.tsx +++ b/packages/rdx-ui/src/components/form/TextField.tsx @@ -71,7 +71,7 @@ export function TextField({ {...inputRest} disabled={disabled} aria-disabled={disabled} - className={cn(inputClassName)} + className={cn("bg-background", inputClassName)} /> {false && {description || "\u00A0"}} diff --git a/packages/rdx-ui/src/components/layout/app-content.tsx b/packages/rdx-ui/src/components/layout/app-content.tsx index bdede8e3..cda1356c 100644 --- a/packages/rdx-ui/src/components/layout/app-content.tsx +++ b/packages/rdx-ui/src/components/layout/app-content.tsx @@ -9,7 +9,7 @@ export const AppContent = ({ return (
) => { return ( -
+
{children}
); diff --git a/packages/rdx-ui/src/components/layout/app-layout.tsx b/packages/rdx-ui/src/components/layout/app-layout.tsx index 5f36be74..17efa962 100644 --- a/packages/rdx-ui/src/components/layout/app-layout.tsx +++ b/packages/rdx-ui/src/components/layout/app-layout.tsx @@ -14,7 +14,7 @@ export const AppLayout = () => { > {/* Aquí está el MAIN */} - + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38e3054e..8e0e9b6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -613,6 +613,9 @@ importers: '@tanstack/react-query': specifier: ^5.74.11 version: 5.90.2(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) ag-grid-community: specifier: ^33.3.0 version: 33.3.2