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.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