diff --git a/biome.json b/biome.json index 5f1af7a4..03735ab9 100644 --- a/biome.json +++ b/biome.json @@ -25,6 +25,7 @@ "complexity": { "noForEach": "off", "noBannedTypes": "info", + "noUselessFragments": "off", "useOptionalChain": "off" }, "suspicious": { diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 00dc1344..96afbba8 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -1,150 +1,5 @@ { - "common": { - "append_empty_row": "Añadir fila", - "append_empty_row_tooltip": "Añadir una fila vacía", - "duplicate_row": "Duplicar fila", - "insert_row_above": "Insertar fila encima", - "insert_row_below": "Insertar fila debajo", - "remove_row": "Eliminar" - }, - "pages": { - "title": "Facturas", - "description": "Gestiona tus facturas", - "list": { - "title": "Lista de facturas", - "description": "Lista todas las facturas", - "grid_columns": { - "invoice_number": "Num. factura", - "invoice_series": "Serie", - "invoice_status": "Estado", - "issue_date": "Fecha", - "total_price": "Imp. total" - } - }, - "create": { - "title": "Crear factura", - "description": "Crear una nueva factura", - "back_to_list": "Volver a la lista" - }, - "edit": { - "title": "Editar factura", - "description": "Editar la factura seleccionada" - }, - "delete": { - "title": "Eliminar factura", - "description": "Eliminar la factura seleccionada" - }, - "view": { - "title": "Ver factura", - "description": "Ver los detalles de la factura seleccionada" - } - }, - "status": { - "draft": "Borrador", - "emitted": "Emitida", - "sent": "Enviada", - "received": "Recibida", - "rejected": "Rechazada" - }, - "form_fields": { - "invoice_number": { - "label": "Num. factura", - "placeholder": "", - "description": "" - }, - "issue_date": { - "label": "Fecha", - "placeholder": "Seleccionar una fecha", - "description": "Fecha de emisión de la factura" - }, - "invoice_series": { - "label": "Serie", - "placeholder": "", - "description": "" - }, - "operation_date": { - "label": "Intervención", - "placeholder": "Seleccionar una fecha", - "description": "Fecha de intervención de los trabajos" - }, - "description": { - "label": "Descripción", - "placeholder": "Descripción de la factura", - "description": "Descripción general de la factura" - }, - "subtotal_price": { - "label": "Subtotal", - "placeholder": "", - "description": "" - }, - "discount": { - "label": "Dto (%)", - "placeholder": "", - "description": "Porcentaje de descuento" - }, - "discount_price": { - "label": "Imp. descuento", - "placeholder": "", - "desc": "Importe del descuento" - }, - "total_price": { - "label": "Imp. total", - "placeholder": "", - "description": "Importe total con el descuento ya aplicado" - }, - "notes": { - "label": "Notas", - "placeholder": "Notas adicionales sobre la factura", - "description": "Notas adicionales que se pueden incluir en la factura" - }, - "items": { - "quantity": { - "label": "Cantidad", - "placeholder": "", - "description": "" - }, - "description": { - "label": "Descripción", - "placeholder": "", - "description": "" - }, - "unit_price": { - "label": "Imp. unitario", - "placeholder": "", - "description": "Importe unitario del artículo" - }, - "subtotal_price": { - "label": "Subtotal", - "placeholder": "", - "description": "" - }, - "discount": { - "label": "Dto (%)", - "placeholder": "", - "description": "Porcentaje de descuento" - }, - "discount_price": { - "label": "Imp. descuento", - "placeholder": "", - "desc": "Importe del descuento" - }, - "taxes": { - "label": "Impuestos", - "placeholder": "", - "desc": "Lista de impuestos aplicables" - }, - "taxes_price": { - "label": "Imp. impuestos", - "placeholder": "", - "desc": "Importe de los impuestos" - }, - "total_price": { - "label": "Imp. total", - "placeholder": "", - "description": "Importe total con el descuento ya aplicado" - } - } - }, + "common": {}, "components": { "customer_invoice_taxes_multi_select": { "label": "Impuestos", diff --git a/modules/customer-invoices/src/web/manifest.ts b/modules/customer-invoices/src/web/manifest.ts index eadb365f..6b2844fe 100644 --- a/modules/customer-invoices/src/web/manifest.ts +++ b/modules/customer-invoices/src/web/manifest.ts @@ -1,7 +1,4 @@ import { IModuleClient, ModuleClientParams } from "@erp/core/client"; -import i18next from "i18next"; -import enResources from "../common/locales/en.json"; -import esResources from "../common/locales/es.json"; import { CustomerInvoiceRoutes } from "./customer-invoice-routes"; export const MODULE_NAME = "CustomerInvoices"; @@ -10,13 +7,13 @@ const MODULE_VERSION = "1.0.0"; export const CustomerInvoicesModuleManifiest: IModuleClient = { name: MODULE_NAME, version: MODULE_VERSION, - dependencies: ["auth"], + dependencies: ["auth", "Customers"], protected: true, layout: "app", routes: (params: ModuleClientParams) => { - i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true); - i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true); + // i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true); + // i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true); return CustomerInvoiceRoutes(params); }, }; diff --git a/modules/customers/package.json b/modules/customers/package.json index 3cf8c2aa..c523f579 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -35,6 +35,7 @@ "i18next": "^25.1.1", "lucide-react": "^0.503.0", "react": "^19.1.0", + "react-data-table-component": "^7.7.0", "react-dom": "^19.1.0", "react-hook-form": "^7.58.1", "react-i18next": "^15.5.1", @@ -43,6 +44,8 @@ "slugify": "^1.6.6", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.5", + "use-debounce": "^10.0.5", + "use-query": "^1.0.2", "zod": "^3.25.67" } } diff --git a/modules/customers/src/common/dto/index.ts b/modules/customers/src/common/dto/index.ts new file mode 100644 index 00000000..346dac3b --- /dev/null +++ b/modules/customers/src/common/dto/index.ts @@ -0,0 +1,2 @@ +export * from "./request"; +export * from "./response"; diff --git a/modules/customers/src/common/dto/request/index.ts b/modules/customers/src/common/dto/request/index.ts new file mode 100644 index 00000000..bf6798a6 --- /dev/null +++ b/modules/customers/src/common/dto/request/index.ts @@ -0,0 +1 @@ +export * from "./list-customers.query.dto"; diff --git a/modules/customers/src/common/dto/request/list-customers.query.dto.ts b/modules/customers/src/common/dto/request/list-customers.query.dto.ts new file mode 100644 index 00000000..36c3a191 --- /dev/null +++ b/modules/customers/src/common/dto/request/list-customers.query.dto.ts @@ -0,0 +1,39 @@ +import * as z from "zod/v4"; + +/** + * DTO que transporta los parámetros de la consulta (paginación, filtros, etc.) + * para la búsqueda de clientes. + * + * Este DTO es utilizado por el endpoint: + * `GET /customers` (listado / búsqueda de clientes). + * + */ + +const operatorEnum = z.enum([ + "CONTAINS", + "NOT_CONTAINS", + "NOT_EQUALS", + "GREATER_THAN", + "GREATER_THAN_OR_EQUAL", + "LOWER_THAN", + "LOWER_THAN_OR_EQUAL", + "EQUALS", +]); + +const filterSchema = z.object({ + field: z.string(), + operator: operatorEnum, + value: z.string(), +}); + +export const ListCustomersQuerySchema = z.object({ + filters: z.array(filterSchema).optional(), + + pageSize: z.coerce.number().int().positive().optional(), + pageNumber: z.coerce.number().int().nonnegative().optional(), + + orderBy: z.string().optional(), + order: z.enum(["asc", "desc"]).default("asc").optional(), +}); + +export type ListCustomersQueryDTO = z.infer; diff --git a/modules/customers/src/common/dto/response/index.ts b/modules/customers/src/common/dto/response/index.ts new file mode 100644 index 00000000..3c108292 --- /dev/null +++ b/modules/customers/src/common/dto/response/index.ts @@ -0,0 +1 @@ +export * from "./list-customers.result.dto"; diff --git a/modules/customers/src/common/dto/response/list-customers.result.dto.ts b/modules/customers/src/common/dto/response/list-customers.result.dto.ts new file mode 100644 index 00000000..26bd94d5 --- /dev/null +++ b/modules/customers/src/common/dto/response/list-customers.result.dto.ts @@ -0,0 +1,32 @@ +import { MetadataSchema, createListViewSchema } from "@erp/core"; +import * as z from "zod/v4"; + +export const ListCustomersResultSchema = createListViewSchema( + z.object({ + id: z.uuid(), + reference: z.string().optional(), + + is_freelancer: z.boolean(), + name: z.string(), + trade_name: z.string().optional(), + tin: z.string(), + + street: z.string(), + city: z.string(), + state: z.string(), + postal_code: z.string(), + country: z.string(), + + email: z.email(), + phone: z.string(), + + default_tax: z.number(), + status: z.string(), + lang_code: z.string(), + currency_code: z.string(), + + metadata: MetadataSchema.optional(), + }) +); + +export type ListCustomersResultDTO = z.infer; diff --git a/modules/customers/src/common/locales/en.json b/modules/customers/src/common/locales/en.json new file mode 100644 index 00000000..e13fe01d --- /dev/null +++ b/modules/customers/src/common/locales/en.json @@ -0,0 +1,14 @@ +{ + "common": {}, + "components": { + "entity_selector": { + "close": "Close", + "select_entity": "Select entity", + "create_new_entity": "Create new entity", + "search_entity": "Search entity", + "no_entities_found": "No results found for \"{{search}}\"", + "select_or_create": "Select an item from the list or create a new one.", + "create_label": "Create new item" + } + } +} diff --git a/modules/customers/src/common/locales/es.json b/modules/customers/src/common/locales/es.json new file mode 100644 index 00000000..907f5d3e --- /dev/null +++ b/modules/customers/src/common/locales/es.json @@ -0,0 +1,14 @@ +{ + "common": {}, + "components": { + "entity_selector": { + "close": "Cerrar", + "select_entity": "Seleccionar entidad", + "create_new_entity": "Crear nueva entidad", + "search_entity": "Buscar entidad", + "no_entities_found": "No se encontraron resultados para \"{{search}}\"", + "select_or_create": "Seleccione un elemento de la lista o cree uno nuevo.", + "create_label": "Crear nuevo elemento" + } + } +} diff --git a/modules/customers/src/web/components/client-selector.tsx b/modules/customers/src/web/components/client-selector.tsx index 0c6ffef2..d9d2a8ab 100644 --- a/modules/customers/src/web/components/client-selector.tsx +++ b/modules/customers/src/web/components/client-selector.tsx @@ -1,42 +1,58 @@ -"use client"; +import { LookupDialog } from "@repo/rdx-ui/components"; +import DataTable, { TableColumn } from "react-data-table-component"; +import { useDebounce } from "use-debounce"; -import { generateUUIDv4 } from "@repo/rdx-utils"; import { Badge, Button, Card, CardContent, - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, Dialog, DialogContent, - DialogDescription, DialogFooter, DialogHeader, DialogTitle, - Input, Label, - Separator, + TableCell, } from "@repo/shadcn-ui/components"; -import { - Building, - Calendar, - Edit, - Mail, - MapPin, - Phone, - Plus, - Search, - Trash2, - User, -} from "lucide-react"; +import { Building, Calendar, Mail, MapPin, Phone, Plus, User } from "lucide-react"; import { useState } from "react"; +import { useCustomersQuery } from "../hooks"; -const mockCustomers = [ +type Customer = { + id: string; + name: string; + email: string; + company: string; + status: string; +}; + +const columns: TableColumn[] = [ + { + name: "Nombre", + selector: (row) => row.name, + sortable: true, + }, + { + name: "Email", + selector: (row) => row.email, + }, + { + name: "Empresa", + selector: (row) => row.company, + }, + { + name: "Estado", + selector: (row) => row.status, + cell: (row) => ( + + {row.status} + + ), + }, +]; + +const mockCustomers: Customer[] = [ { id: "a1d2e3f4-5678-90ab-cdef-1234567890ab", name: "Juan Pérez", @@ -79,357 +95,154 @@ const mockCustomers = [ }, ]; +async function fetchClientes(search: string): Promise { + await new Promise((res) => setTimeout(res, 500)); + const mock: Customer[] = [ + { + id: "a1", + name: "Juan Pérez", + email: "juan@email.com", + phone: "+34 600 123 456", + company: "Tech Corp", + address: "Madrid", + createdAt: "2024-01-15", + status: "Activo", + }, + { + id: "b1", + name: "María García", + email: "maria@email.com", + phone: "+34 600 789 012", + company: "Design Studio", + address: "Barcelona", + createdAt: "2024-02-20", + status: "Activo", + }, + ]; + return mock.filter( + (c) => + c.name.toLowerCase().includes(search.toLowerCase()) || + c.email.toLowerCase().includes(search.toLowerCase()) || + c.company.toLowerCase().includes(search.toLowerCase()) + ); +} + export const ClientSelector = () => { const [open, setOpen] = useState(false); - const [selectedCustomer, setSelectedCustomer] = useState(null); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); - const [searchValue, setSearchValue] = useState(""); - const [newCustomer, setNewCustomer] = useState({ - name: "", - email: "", - phone: "", - company: "", - address: "", + const [search, setSearch] = useState(""); + const [pageNumber, setPageNumber] = useState(1); + const [pageSize] = useState(10); + const [selectedCustomer, setSelectedCustomer] = useState(undefined); + + const [debouncedSearch] = useDebounce(search, 400); + const paginated = filtered.slice((pageNumber - 1) * pageSize, pageNumber * pageSize); + + const { data, isLoading, isError, error, refetch } = useCustomersQuery({ + filters: [ + { + field: "name", + operator: "CONTAINS", + value: debouncedSearch, + }, + { + field: "trade_name", + operator: "CONTAINS", + value: debouncedSearch, + }, + ], + pageNumber, + pageSize, }); - const handleCreateCustomer = (e) => { - e.preventDefault(); - - const createdCustomer = { - id: generateUUIDv4(), - ...newCustomer, - createdAt: new Date().toISOString().split("T")[0], - status: "Activo", - }; - - console.log("Cliente creado:", createdCustomer); - setSelectedCustomer(createdCustomer); - setIsCreateModalOpen(false); - setNewCustomer({ name: "", email: "", phone: "", company: "", address: "" }); - }; - - const handleEditCustomer = () => { - console.log("Editar cliente:", selectedCustomer); - setIsDetailsModalOpen(false); - }; - - const handleDeleteCustomer = () => { - console.log("Eliminar cliente:", selectedCustomer); - setSelectedCustomer(null); - setIsDetailsModalOpen(false); - }; - - const handleSelectCustomer = (customer) => { - console.log("Seleccionar cliente:", customer); - setSelectedCustomer(customer); - setOpen(false); - }; - return ( -
-
- -
- - - - -
- -

- No se encontró ningún cliente con "{searchValue}" -

- -
-
- - {mockCustomers.map((customer) => ( - handleSelectCustomer(customer)} - className='flex items-center space-x-3 p-3' - > - -
-
-

{customer.name}

- - {customer.status} - -
-
- - - {customer.company} - - - - {customer.email} - -
-
-
- ))} -
- - - { - setIsCreateModalOpen(true); - setOpen(false); - }} - className='flex items-center space-x-3 p-3 text-primary' - > - - Crear nuevo cliente - - -
-
- {selectedCustomer && ( - - + +
-
- -
-
-

{selectedCustomer.name}

- - {selectedCustomer.status} - -
-

{selectedCustomer.company}

-
-
-
- - + {selectedCustomer.status} +
+ {selectedCustomer.company}
+

{selectedCustomer.email}

)} - + { + setSelectedCustomer(item); + setOpen(false); + }} + onCreate={() => { + setOpen(false); + console.log("Crear nuevo cliente"); + }} + page={pageNumber} + perPage={perPage} + totalItems={filtered.length} + onPageChange={setPage} + renderItem={() => null} // No se usa con DataTable + renderContainer={(items) => ( + { + setSelectedCustomer(item); + setOpen(false); + }} + pagination + paginationServer + paginationPerPage={perPage} + paginationTotalRows={filtered.length} + onChangePage={(p) => setPage(p)} + highlightOnHover + pointerOnHover + noDataComponent='No se encontraron resultados' + /> + )} + /> + + - + - Crear Nuevo Cliente + Nuevo Cliente - Completa la información del nuevo cliente -
-
-
- - setNewCustomer({ ...newCustomer, name: e.target.value })} - placeholder='Nombre completo' - /> -
-
- - setNewCustomer({ ...newCustomer, company: e.target.value })} - placeholder='Nombre de la empresa' - /> -
-
-
- - setNewCustomer({ ...newCustomer, email: e.target.value })} - placeholder='correo@ejemplo.com' - /> -
-
- - setNewCustomer({ ...newCustomer, phone: e.target.value })} - placeholder='+34 600 000 000' - /> -
-
- - setNewCustomer({ ...newCustomer, address: e.target.value })} - placeholder='Dirección completa' - /> -
-
+

Formulario de creación pendiente…

- - - -
-
- - - - - - - Detalles del Cliente - - - {selectedCustomer && ( -
-
-
-
-

{selectedCustomer.name}

-

{selectedCustomer.company}

-
- - {selectedCustomer.status} - -
- - - -
-
- -
- -

{selectedCustomer.email}

-
-
- -
- -
- -

{selectedCustomer.phone}

-
-
- -
- -
- -

{selectedCustomer.address}

-
-
- -
- -
- -

- {new Date(selectedCustomer.createdAt).toLocaleDateString("es-ES")} -

-
-
-
-
- - - -
- - - -
-
- )} - -
@@ -437,3 +250,54 @@ export const ClientSelector = () => {
); }; + +// COMPONENTES VISUALES + +const CustomerCard = ({ customer }: { customer: Customer }) => ( + + +
+ + {customer.name} + + {customer.status} + +
+
+
+ + {customer.email} +
+
+ + {customer.company} +
+
+ + {customer.phone} +
+
+ + {customer.address} +
+
+ + {new Date(customer.createdAt).toLocaleDateString("es-ES")} +
+
+
+
+); + +const CustomerRow = ({ customer }: { customer: Customer }) => ( + <> + {customer.name} + {customer.email} + {customer.company} + + + {customer.status} + + + +); diff --git a/modules/customers/src/web/globals.css b/modules/customers/src/web/globals.css index c407c45a..8af59cfa 100644 --- a/modules/customers/src/web/globals.css +++ b/modules/customers/src/web/globals.css @@ -1,2 +1,19 @@ @source "./components"; @source "./pages"; + +.custom-dialog-lg { + max-width: 1024px !important; + width: 100% !important; +} +.custom-dialog-xl { + max-width: 1280px !important; + width: 100% !important; +} +.custom-dialog-2xl { + max-width: 1536px !important; + width: 100% !important; +} +.custom-dialog-3xl { + max-width: 1920px !important; + width: 100% !important; +} diff --git a/modules/customers/src/web/hooks/index.ts b/modules/customers/src/web/hooks/index.ts new file mode 100644 index 00000000..ad82e29e --- /dev/null +++ b/modules/customers/src/web/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-customers-query"; diff --git a/modules/customers/src/web/hooks/use-customers-query.tsx b/modules/customers/src/web/hooks/use-customers-query.tsx new file mode 100644 index 00000000..f2f98441 --- /dev/null +++ b/modules/customers/src/web/hooks/use-customers-query.tsx @@ -0,0 +1,24 @@ +import { useDataSource, useQueryKey } from "@erp/core/client"; +import { ListCustomersQueryDTO, ListCustomersResultDTO } from "@erp/customer-invoices/common/dto"; +import { UseQueryResult, useQuery } from "@tanstack/react-query"; + +type UseCustomersQueryParams = ListCustomersQueryDTO; + +// Obtener clientes +export const useCustomersQuery = ( + params: UseCustomersQueryParams +): UseQueryResult => { + const dataSource = useDataSource(); + const keys = useQueryKey(); + + return useQuery({ + queryKey: keys().data().resource("customers").action("list").params(params).get(), + queryFn: (context) => { + const { signal } = context; + return dataSource.getList("customers", { + signal, + ...params, + }); + }, + }); +}; diff --git a/modules/customers/src/web/i18n.ts b/modules/customers/src/web/i18n.ts new file mode 100644 index 00000000..01028045 --- /dev/null +++ b/modules/customers/src/web/i18n.ts @@ -0,0 +1,28 @@ +import { useEffect } from "react"; +import { useTranslation as useI18NextTranslation } from "react-i18next"; +import enResources from "../common/locales/en.json"; +import esResources from "../common/locales/es.json"; +import { MODULE_NAME } from "./manifest"; + +const addMissingBundles = (i18n: any) => { + const needsEn = !i18n.hasResourceBundle("en", MODULE_NAME); + const needsEs = !i18n.hasResourceBundle("es", MODULE_NAME); + + if (needsEn) { + i18n.addResourceBundle("en", MODULE_NAME, enResources, true, true); + } + + if (needsEs) { + i18n.addResourceBundle("es", MODULE_NAME, esResources, true, true); + } +}; + +export const useTranslation = () => { + const { i18n } = useI18NextTranslation(); + + useEffect(() => { + addMissingBundles(i18n); + }, [i18n]); + + return useI18NextTranslation(MODULE_NAME); +}; diff --git a/packages/rdx-ui/src/components/index.tsx b/packages/rdx-ui/src/components/index.tsx index a6e29323..a2cab425 100644 --- a/packages/rdx-ui/src/components/index.tsx +++ b/packages/rdx-ui/src/components/index.tsx @@ -5,6 +5,7 @@ export * from "./error-overlay.tsx"; export * from "./form/index.tsx"; export * from "./layout/index.tsx"; export * from "./loading-overlay/index.tsx"; +export * from "./lookup-dialog/index.tsx"; export * from "./multi-select.tsx"; export * from "./multiple-selector.tsx"; export * from "./scroll-to-top.tsx"; diff --git a/packages/rdx-ui/src/components/lookup-dialog/index.tsx b/packages/rdx-ui/src/components/lookup-dialog/index.tsx new file mode 100644 index 00000000..12637367 --- /dev/null +++ b/packages/rdx-ui/src/components/lookup-dialog/index.tsx @@ -0,0 +1 @@ +export * from "./lookup-dialog.tsx"; diff --git a/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx b/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx new file mode 100644 index 00000000..1cf83cec --- /dev/null +++ b/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx @@ -0,0 +1,143 @@ +import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts"; +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Separator, +} from "@repo/shadcn-ui/components"; +import { PlusIcon, RefreshCwIcon } from "lucide-react"; + +type LookupDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + items: T[]; + isLoading: boolean; + isError?: boolean; + refetch?: () => void; + search: string; + onSearchChange: (value: string) => void; + title: string; + description?: string; + searchPlaceholder?: string; + onSelect: (item: T) => void; + onCreate: () => void; + createLabel?: string; + maxWidth?: "lg" | "xl" | "2xl" | "3xl"; + renderItem: (item: T) => React.ReactNode; // no se usa si se usa renderContainer + renderContainer: (items: T[]) => React.ReactNode; + emptyStateComponent?: React.ReactNode; + page?: number; + perPage?: number; + totalItems?: number; + onPageChange?: (page: number) => void; +}; + +export const LookupDialog = ({ + open, + onOpenChange, + items, + isLoading, + isError, + refetch, + search, + onSearchChange, + title, + description, + searchPlaceholder, + onSelect, + onCreate, + createLabel, + maxWidth = "xl", + renderItem, + renderContainer, + emptyStateComponent, + page, + perPage = 10, + totalItems, + onPageChange, +}: LookupDialogProps) => { + const { t } = useTranslation(); + + const widthClass = { + lg: "custom-dialog-lg", + xl: "custom-dialog-xl", + "2xl": "custom-dialog-2xl", + "3xl": "custom-dialog-3xl", + }[maxWidth]; + + const showPagination = totalItems !== undefined && onPageChange !== undefined; + + return ( + + + + {title || t("components.entity_selector.select_entity")} + + {description || t("components.entity_selector.select_or_create")} + + + +
+ onSearchChange(e.target.value)} + className='flex-1' + /> +
+ + + + {isLoading && ( +

+ {t("components.entity_selector.loading")} +

+ )} + + {isError && ( +
+

Error al cargar los datos.

+ {refetch && ( + + )} +
+ )} + + {!isLoading && !isError && ( + <> + {items.length === 0 ? ( + emptyStateComponent || ( +

+ {t("components.entity_selector.no_entities_found")} +

+ ) + ) : ( +
{renderContainer(items)}
+ )} + + )} + +
+ +
+ + + + +
+
+ ); +}; diff --git a/packages/rdx-ui/src/locales/en.json b/packages/rdx-ui/src/locales/en.json index fd0bfa34..7daa52c2 100644 --- a/packages/rdx-ui/src/locales/en.json +++ b/packages/rdx-ui/src/locales/en.json @@ -16,9 +16,10 @@ "multi_select": { "clear_selection": "Clear", "close": "Close", + "loading": "Loading...", + "no_results": "No results found.", "select_options": "Select options", - "select_all": "Select all", - "no_results": "No results found." + "select_all": "Select all" } } } diff --git a/packages/rdx-ui/src/locales/es.json b/packages/rdx-ui/src/locales/es.json index 4edd7a28..f5a161b0 100644 --- a/packages/rdx-ui/src/locales/es.json +++ b/packages/rdx-ui/src/locales/es.json @@ -16,9 +16,10 @@ "multi_select": { "clear_selection": "Limpiar", "close": "Cerrar", + "loading": "Cargando...", + "no_results": "No se han encontrado resultados.", "select_options": "Seleccionar opciones", - "select_all": "Seleccionar todo", - "no_results": "No se han encontrado resultados." + "select_all": "Seleccionar todo" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 660c935a..dec9aef2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -604,6 +604,9 @@ importers: react: specifier: ^19.1.0 version: 19.1.0 + react-data-table-component: + specifier: ^7.7.0 + version: 7.7.0(react@19.1.0)(styled-components@6.1.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) @@ -628,6 +631,12 @@ importers: tw-animate-css: specifier: ^1.3.5 version: 1.3.5 + use-debounce: + specifier: ^10.0.5 + version: 10.0.5(react@19.1.0) + use-query: + specifier: ^1.0.2 + version: 1.0.2 zod: specifier: ^3.25.67 version: 3.25.67 @@ -1333,9 +1342,15 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + '@emotion/is-prop-valid@1.2.2': + resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} + '@emotion/is-prop-valid@1.3.1': resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + '@emotion/memoize@0.8.1': + resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} @@ -1367,6 +1382,9 @@ packages: '@emotion/unitless@0.10.0': resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + '@emotion/unitless@0.8.1': + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} peerDependencies: @@ -3058,6 +3076,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/stylis@4.2.5': + resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} + '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} @@ -3349,6 +3370,9 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001720: resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} @@ -3579,9 +3603,16 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -5349,6 +5380,10 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -5413,6 +5448,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-data-table-component@7.7.0: + resolution: {integrity: sha512-5knL6zMSKlbvzu9P04KM5Lx8/EyQujb4I9z3rWeoVX++IDJadQ7aR4X5J6EeS90wjK0Xoa6btaVeglnCAqD2ag==} + peerDependencies: + react: '>= 17.0.0' + styled-components: '>= 5.0.0' + react-day-picker@8.10.1: resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} peerDependencies: @@ -5753,6 +5794,9 @@ packages: shallow-equal-object@1.1.1: resolution: {integrity: sha512-9DDzYRlzCwF2CemeF0aOFk5T5KMrjG7HldcW7utwYhA/limuGHn3No8KhpDE8BrO7GLaSRJumNKReipZBybd7A==} + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5912,9 +5956,19 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + styled-components@6.1.19: + resolution: {integrity: sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==} + engines: {node: '>= 16'} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + stylis@4.3.2: + resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} + stylus@0.62.0: resolution: {integrity: sha512-v3YCf31atbwJQIMtPNX8hcQ+okD4NQaTuKGUWfII8eaqn+3otrbttGL1zSMZAAtiPsBztQnujVBugg/cXFUpyg==} hasBin: true @@ -6108,6 +6162,9 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6248,12 +6305,22 @@ packages: '@types/react': optional: true + use-debounce@10.0.5: + resolution: {integrity: sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '*' + use-deep-compare-effect@1.8.1: resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==} engines: {node: '>=10', npm: '>=6'} peerDependencies: react: '>=16.13' + use-query@1.0.2: + resolution: {integrity: sha512-Ypdv/LMbs4OnjCCZ4QtWVCu5XKUUiHZSf0X0dToZahX9BXs5LmVkBCgLN8PVEGcuulNI7fL1SOukFGesWhO2EA==} + engines: {node: '>=6.0.0'} + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -6834,10 +6901,16 @@ snapshots: '@emotion/hash@0.9.2': {} + '@emotion/is-prop-valid@1.2.2': + dependencies: + '@emotion/memoize': 0.8.1 + '@emotion/is-prop-valid@1.3.1': dependencies: '@emotion/memoize': 0.9.0 + '@emotion/memoize@0.8.1': {} + '@emotion/memoize@0.9.0': {} '@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0)': @@ -6883,6 +6956,8 @@ snapshots: '@emotion/unitless@0.10.0': {} + '@emotion/unitless@0.8.1': {} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': dependencies: react: 19.1.0 @@ -8639,6 +8714,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/stylis@4.2.5': {} + '@types/through@0.0.33': dependencies: '@types/node': 22.15.32 @@ -8969,6 +9046,8 @@ snapshots: camelcase@6.3.0: {} + camelize@1.0.1: {} + caniuse-lite@1.0.30001720: {} case@1.6.3: {} @@ -9208,6 +9287,8 @@ snapshots: crypto-js@4.2.0: {} + css-color-keywords@1.0.0: {} + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -9216,6 +9297,12 @@ snapshots: domutils: 2.8.0 nth-check: 2.1.1 + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + css-what@6.1.0: {} cssesc@3.0.0: {} @@ -11129,6 +11216,12 @@ snapshots: postcss-value-parser@4.2.0: {} + postcss@8.4.49: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -11205,6 +11298,12 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-data-table-component@7.7.0(react@19.1.0)(styled-components@6.1.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): + dependencies: + deepmerge: 4.3.1 + react: 19.1.0 + styled-components: 6.1.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-day-picker@8.10.1(date-fns@4.1.0)(react@19.1.0): dependencies: date-fns: 4.1.0 @@ -11536,6 +11635,8 @@ snapshots: shallow-equal-object@1.1.1: {} + shallowequal@1.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11685,8 +11786,24 @@ snapshots: strip-json-comments@3.1.1: {} + styled-components@6.1.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@emotion/is-prop-valid': 1.2.2 + '@emotion/unitless': 0.8.1 + '@types/stylis': 4.2.5 + css-to-react-native: 3.2.0 + csstype: 3.1.3 + postcss: 8.4.49 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + shallowequal: 1.1.0 + stylis: 4.3.2 + tslib: 2.6.2 + stylis@4.2.0: {} + stylis@4.3.2: {} + stylus@0.62.0: dependencies: '@adobe/css-tools': 4.3.3 @@ -11929,6 +12046,8 @@ snapshots: tslib@1.14.1: {} + tslib@2.6.2: {} + tslib@2.8.1: {} tsup@8.4.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3): @@ -12070,12 +12189,18 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + use-debounce@10.0.5(react@19.1.0): + dependencies: + react: 19.1.0 + use-deep-compare-effect@1.8.1(react@19.1.0): dependencies: '@babel/runtime': 7.27.6 dequal: 2.0.3 react: 19.1.0 + use-query@1.0.2: {} + use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0): dependencies: detect-node-es: 1.1.0