diff --git a/modules/customers/src/web/_archived/pages/list/use-customers-list-columns.tsx b/modules/customers/src/web/_archived/pages/list/use-customers-list-columns.tsx deleted file mode 100644 index 988a32fc..00000000 --- a/modules/customers/src/web/_archived/pages/list/use-customers-list-columns.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { DataTableColumnHeader } from "@repo/rdx-ui/components"; -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, - EyeIcon, - MailIcon, - MoreHorizontalIcon, - PhoneIcon, - User2Icon, -} from "lucide-react"; -import * as React from "react"; - -import { useTranslation } from "../../../i18n"; -import { AddressCell } from "../../../list/ui/components/address-cell"; -import type { 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)}` : "-"; -} - -const KindBadge = ({ isCompany }: { isCompany: boolean }) => ( - - {isCompany ? : } - {isCompany ? "Company" : "Person"} - -); - -export const Soft = ({ children }: { children: React.ReactNode }) => ( - {children} -); - -const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => ( -
- {customer.email_primary && ( -
- - - {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}} - {false} -
- {false} -
-); - -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: "customer", - header: ({ column }) => ( - - ), - accessorFn: (row) => row.name, // para ordenar/buscar por nombre - enableHiding: false, - size: 140, - minSize: 120, - 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}} - -
-
-
- ); - }, - }, - - // Contacto (emails, teléfonos, web) - { - id: "contact", - header: ({ column }) => ( - - ), - accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`, - size: 140, - minSize: 120, - cell: ({ row }) => , - }, - - // Dirección (múltiples campos en bloque) - { - id: "address", - header: t("pages.list.grid_columns.address"), - accessorFn: (r) => - `${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`, - size: 140, - minSize: 120, - cell: ({ row }) => , - }, - - // Acciones - { - id: "actions", - header: ({ column }) => ( - - ), - size: 64, - minSize: 64, - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const customer = row.original; - const { website, email_primary } = customer; - return ( -
-
- - {0 === false && ( - - - - - - 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/list/controllers/use-customer-sheet.controller.ts b/modules/customers/src/web/list/controllers/use-customer-sheet.controller.ts deleted file mode 100644 index ccf0364f..00000000 --- a/modules/customers/src/web/list/controllers/use-customer-sheet.controller.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useSheetState } from "@repo/rdx-ui/hooks"; -import { useCallback, useState } from "react"; - -import { useCustomerGetQuery } from "../../shared/hooks"; - -export const useCustomerSheetController = (initialCustomerId = "") => { - const [customerId, setCustomerId] = useState(initialCustomerId); - const sheetState = useSheetState({ - defaultOpen: false, - defaultPinned: false, - defaultMode: "view", - }); - - const query = useCustomerGetQuery(customerId, { - enabled: Boolean(customerId), - }); - - const openCustomerSheet = useCallback( - (nextCustomerId: string) => { - setCustomerId(nextCustomerId); - sheetState.openInMode("view"); - }, - [sheetState] - ); - - const closeCustomerSheet = useCallback(() => { - sheetState.closeSheet(); - }, [sheetState]); - - return { - customer: query.data, - customerId, - setCustomerId, - - openCustomerSheet, - closeCustomerSheet, - - isLoading: query.isLoading, - isFetching: query.isFetching, - - isError: query.isError, - error: query.error, - - refetch: query.refetch, - - sheetState, - }; -}; diff --git a/modules/customers/src/web/list/controllers/use-customer-summary-panel.controller.ts b/modules/customers/src/web/list/controllers/use-customer-summary-panel.controller.ts new file mode 100644 index 00000000..399edc2f --- /dev/null +++ b/modules/customers/src/web/list/controllers/use-customer-summary-panel.controller.ts @@ -0,0 +1,53 @@ +import { type RightPanelMode, useRightPanelState } from "@repo/rdx-ui/hooks"; +import { useCallback, useState } from "react"; + +import { useCustomerGetQuery } from "../../shared"; + +interface Options { + initialCustomerId?: string; + initialMode?: RightPanelMode; + initialOpen?: boolean; +} + +export const useCustomerSummaryPanelController = ({ + initialCustomerId = "", + initialMode = "view", + initialOpen = false, +}: Options = {}) => { + const [customerId, setCustomerId] = useState(initialCustomerId); + + const panelState = useRightPanelState({ + defaultMode: initialMode, + defaultVisibility: initialOpen ? "temporary" : "hidden", + }); + + const query = useCustomerGetQuery(customerId, { + enabled: Boolean(customerId), + }); + + const openCustomerPanel = useCallback( + (nextCustomerId: string, mode: RightPanelMode = "view") => { + setCustomerId(nextCustomerId); + + if (panelState.isPinned) { + panelState.openPersistent(mode); + } else { + panelState.openTemporary(mode); + } + }, + [panelState.isPinned, panelState.openTemporary, panelState.openPersistent] + ); + + const closePanel = useCallback(() => { + panelState.close(); + setCustomerId(""); + }, [panelState.close]); + + return { + customer: query.data, + customerId, + openCustomerPanel, + closePanel, + panelState, + }; +}; diff --git a/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts b/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts index 7d115b21..cb0809b4 100644 --- a/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts +++ b/modules/customers/src/web/list/controllers/use-list-customers-page.controller.ts @@ -1,13 +1,48 @@ -import { useCustomerSheetController } from "./use-customer-sheet.controller"; +import type { RightPanelMode } from "@repo/rdx-ui/hooks"; +import { useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; + +import { useCustomerSummaryPanelController } from "./use-customer-summary-panel.controller"; import { useListCustomersController } from "./use-list-customers.controller"; -export function useListCustomersPageController() { +export const useListCustomersPageController = () => { const listCtrl = useListCustomersController(); - const sheetCtrl = useCustomerSheetController(); + const [searchParams] = useSearchParams(); + + // ----------------------------- + // URL → estado inicial (sync) + // ----------------------------- + const initialPanelState = useMemo(() => { + const customerId = searchParams.get("customerId"); + const panelMode = searchParams.get("panel") as RightPanelMode | null; + + if (!customerId) { + return { + customerId: "", + mode: "view" as RightPanelMode, + open: false, + }; + } + + return { + customerId, + mode: panelMode ?? "view", + open: true, + }; + }, [searchParams]); + + // ----------------------------- + // Controller con estado inicial + // ----------------------------- + const panelCtrl = useCustomerSummaryPanelController({ + initialCustomerId: initialPanelState.customerId, + initialMode: initialPanelState.mode, + initialOpen: initialPanelState.open, + }); return { listCtrl, - sheetCtrl, + panelCtrl, }; -} +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx b/modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx deleted file mode 100644 index 0547cee0..00000000 --- a/modules/customers/src/web/list/ui/blocks/customer-sheet/customer-sheet.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import { - Avatar, - AvatarFallback, - Badge, - Button, - Separator, - Sheet, - SheetContent, - SheetTitle, -} from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { - Building2Icon, - CopyIcon, - ExternalLinkIcon, - FileTextIcon, - GlobeIcon, - MailIcon, - MapPinIcon, - PencilIcon, - PhoneIcon, - PinIcon, - PinOffIcon, - TrendingUpIcon, - UserIcon, - XIcon, -} from "lucide-react"; - -import type { Customer } from "../../../../shared"; - -interface CustomerSheetProps { - customer?: Customer; - open: boolean; - onOpenChange: (open: boolean) => void; - pinned: boolean; - onPinnedChange: (pinned: boolean) => void; - onEdit?: (customer: Customer) => void; -} - -// Datos de ejemplo ERP para demostración -const mockERPData = { - totalPurchases: 45750.8, - purchasesThisYear: 12350.25, - lastInvoices: [ - { - id: "1", - number: "FAC-2024-0156", - date: "2024-01-15", - amount: 1250.0, - status: "paid" as const, - }, - { - id: "2", - number: "FAC-2024-0142", - date: "2024-01-08", - amount: 890.5, - status: "paid" as const, - }, - { - id: "3", - number: "FAC-2024-0128", - date: "2023-12-22", - amount: 2100.0, - status: "pending" as const, - }, - { - id: "4", - number: "FAC-2023-0098", - date: "2023-11-30", - amount: 750.25, - status: "overdue" as const, - }, - ], -}; - -export const CustomerSheet = ({ - customer, - open, - onOpenChange, - pinned, - onPinnedChange, - onEdit, -}: CustomerSheetProps) => { - const getInitials = (name: string) => { - return name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase() - .slice(0, 2); - }; - - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat("es-ES", { - style: "currency", - currency: "EUR", - }).format(amount); - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString("es-ES", { - day: "2-digit", - month: "short", - year: "numeric", - }); - }; - - const getGoogleMapsUrl = (customer: Customer) => { - const fullAddress = `${customer.street}, ${customer.postalCode} ${customer.city}, ${customer.province}, ${customer.country}`; - return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`; - }; - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - }; - - const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => { - switch (status) { - case "paid": - return ( - - Pagada - - ); - case "pending": - return ( - - Pendiente - - ); - case "overdue": - return ( - - Vencida - - ); - } - }; - - // Combinar datos del cliente con datos mock de ERP - const customerWithERP = { ...customer, ...mockERPData }; - - return ( - - -
- Ficha de cliente -
- -
-
- - {!pinned && ( - - )} -
-
- - {/* Contenido scrolleable */} - {customer ? ( -
- {/* Info principal del cliente */} -
-
- - - {getInitials(customer.name)} - - -
-
-

- {customer.name} -

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

{customer.tradeName}

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

Contacto

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

- - Volumen de compras -

-
-
-

Total histórico

-

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

-
-
-

Este año

-

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

-
-
-
- - - - {/* Últimas facturas */} -
-

- - Últimas facturas -

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

Selecciona un cliente

-
- )} - - {/* Footer con acciones */} -
-
- - -
-
-
-
- ); -}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts b/modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts deleted file mode 100644 index 1ca7fe4b..00000000 --- a/modules/customers/src/web/list/ui/blocks/customer-sheet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./customer-sheet"; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-address-section.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-address-section.tsx new file mode 100644 index 00000000..3e42afa0 --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-address-section.tsx @@ -0,0 +1,26 @@ +import { ExternalLinkIcon, MapPinIcon } from "lucide-react"; + +import type { Customer } from "../../../../shared"; + +export const CustomerAddressSection = ({ customer }: { customer: Customer }) => { + const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( + `${customer.street}, ${customer.city}` + )}`; + + return ( + + ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-contact-section.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-contact-section.tsx new file mode 100644 index 00000000..4046390f --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-contact-section.tsx @@ -0,0 +1,32 @@ +import { MailIcon, PhoneIcon } from "lucide-react"; + +import type { Customer } from "../../../../shared"; + +export const CustomerContactSection = ({ customer }: { customer: Customer }) => { + return ( +
+

Contacto

+ +
+ + + {customer.primaryEmail} + + + {customer.secondaryEmail && ( + + + {customer.secondaryEmail} + + )} + + {customer.primaryPhone && ( + + + {customer.primaryPhone} + + )} +
+
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-footer-actions.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-footer-actions.tsx new file mode 100644 index 00000000..e119070c --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-footer-actions.tsx @@ -0,0 +1,27 @@ +import { Button } from "@repo/shadcn-ui/components"; + +import type { Customer } from "../../../../shared"; + +export const CustomerFooterActions = ({ + customer, + onCreateInvoice, + onEdit, +}: { + customer: Customer; + onCreateInvoice?: (customer: Customer) => void; + onEdit?: (customer: Customer) => void; +}) => { + return ( +
+
+ + + +
+
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-header.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-header.tsx new file mode 100644 index 00000000..25a98955 --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-header.tsx @@ -0,0 +1,63 @@ +import { Avatar, AvatarFallback, Badge } from "@repo/shadcn-ui/components"; +import { Building2Icon, CopyIcon, UserIcon } from "lucide-react"; + +import type { Customer } from "../../../../shared"; +import { Initials } from "../../components"; + +export const CustomerHeader = ({ customer }: { customer: Customer }) => { + return ( +
+
+ + + + + + +
+
+

{customer.name}

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

{customer.tradeName}

+ )} + +
+ + {customer.isCompany ? ( + <> + + Empresa + + ) : ( + <> + + Particular + + )} + + + +
+
+
+
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-proformas-section.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-proformas-section.tsx new file mode 100644 index 00000000..cb4da1fb --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-proformas-section.tsx @@ -0,0 +1,83 @@ +import { Badge, Button } from "@repo/shadcn-ui/components"; +import { FileTextIcon } from "lucide-react"; + +const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => { + switch (status) { + case "paid": + return ( + + Pagada + + ); + case "pending": + return ( + + Pendiente + + ); + case "overdue": + return ( + + Vencida + + ); + } +}; + +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("es-ES", { + style: "currency", + currency: "EUR", + }).format(amount); +}; + +const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("es-ES", { + day: "2-digit", + month: "short", + year: "numeric", + }); +}; + +export const CustomerProformasSection = ({ + proformas, + onProformaClick, +}: { + proformas: { + id: string; + number: string; + date: string; + amount: number; + status: "paid" | "pending" | "overdue"; + }[]; + onProformaClick?: (id: string) => void; +}) => { + return ( +
+

+ + Últimas proformas +

+ +
+ {proformas.map((pro) => ( + + ))} +
+
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-stats-section.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-stats-section.tsx new file mode 100644 index 00000000..c3f6e143 --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-stats-section.tsx @@ -0,0 +1,33 @@ +import { TrendingUpIcon } from "lucide-react"; + +export const CustomerStatsSection = ({ + stats, +}: { + stats: { currency: string; totalPurchases: number; purchasesThisYear: number }; +}) => { + const formatCurrency = (value: number) => + new Intl.NumberFormat("es-ES", { style: "currency", currency: stats.currency }).format(value); + + return ( +
+

+ + Volumen de compras +

+
+
+

Total histórico

+

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

+
+
+

Este año

+

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

+
+
+
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-summary-content.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-summary-content.tsx new file mode 100644 index 00000000..e0f3ec8b --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-summary-content.tsx @@ -0,0 +1,68 @@ +import { Separator } from "@repo/shadcn-ui/components"; + +import type { Customer } from "../../../../shared"; + +import { CustomerAddressSection } from "./customer-address-section"; +import { CustomerContactSection } from "./customer-contact-section"; +import { CustomerFooterActions } from "./customer-footer-actions"; +import { CustomerHeader } from "./customer-header"; +import { CustomerProformasSection } from "./customer-proformas-section"; +import { CustomerStatsSection } from "./customer-stats-section"; + +interface CustomerSummaryContentProps { + customer: Customer; + + stats: { + totalPurchases: number; + purchasesThisYear: number; + }; + + proformas: { + id: string; + number: string; + date: string; + amount: number; + status: "paid" | "pending" | "overdue"; + }[]; + + onEdit?: (customer: Customer) => void; + onCreateInvoice?: (customer: Customer) => void; + onProformaClick?: (proformaId: string) => void; +} + +export const CustomerSummaryContent = ({ + customer, + stats, + proformas, + onEdit, + onCreateInvoice, + onProformaClick, +}: CustomerSummaryContentProps) => { + return ( +
+ + + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-summary-panel.tsx b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-summary-panel.tsx new file mode 100644 index 00000000..f78067ff --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/customer-summary-panel.tsx @@ -0,0 +1,123 @@ +import { RightPanel } from "@repo/rdx-ui/components"; +import type { RightPanelMode, RightPanelVisibility } from "@repo/rdx-ui/hooks"; +import { Button } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { PencilIcon, PinIcon, PinOffIcon } from "lucide-react"; + +import type { Customer } from "../../../../shared"; + +import { CustomerSummaryContent } from "./customer-summary-content"; + +const mockERPData = { + totalPurchases: 45750.8, + purchasesThisYear: 12350.25, + lastInvoices: [ + { + id: "1", + number: "FAC-2024-0156", + date: "2024-01-15", + amount: 1250.0, + status: "paid" as const, + }, + { + id: "2", + number: "FAC-2024-0142", + date: "2024-01-08", + amount: 890.5, + status: "paid" as const, + }, + { + id: "3", + number: "FAC-2024-0128", + date: "2023-12-22", + amount: 2100.0, + status: "pending" as const, + }, + { + id: "4", + number: "FAC-2023-0098", + date: "2023-11-30", + amount: 750.25, + status: "overdue" as const, + }, + ], +}; + +interface CustomerSummaryPanelProps { + customer?: Customer; + + open: boolean; + visibility: RightPanelVisibility; + mode: RightPanelMode; + + onOpenChange: (open: boolean) => void; + onTogglePinned: () => void; + + onEdit?: (customer: Customer) => void; + className?: string; +} + +export const CustomerSummaryPanel = ({ + customer, + open, + visibility, + mode, + onOpenChange, + onTogglePinned, + onEdit, + className, +}: CustomerSummaryPanelProps) => { + const isPinned = visibility === "persistent"; + + const titleMap: Record = { + view: "Ficha de cliente", + edit: "Editar cliente", + create: "Nuevo cliente", + }; + + return ( + + + + {customer ? ( + + ) : null} + + } + onOpenChange={onOpenChange} + open={open} + title={titleMap[mode]} + > + {customer ? ( + + ) : ( +
+

Selecciona un cliente

+
+ )} +
+ ); +}; diff --git a/modules/customers/src/web/list/ui/blocks/customer-summary-panel/index.ts b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/index.ts new file mode 100644 index 00000000..715afa4c --- /dev/null +++ b/modules/customers/src/web/list/ui/blocks/customer-summary-panel/index.ts @@ -0,0 +1 @@ +export * from "./customer-summary-panel"; diff --git a/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx b/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx index b03330f1..bc8a853b 100644 --- a/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx +++ b/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx @@ -132,7 +132,7 @@ export function useCustomersGridColumns( enableSorting: false, size: 140, minSize: 120, - cell: ({ row }) => , + cell: ({ row }) => , }, { id: "actions", diff --git a/modules/customers/src/web/list/ui/blocks/index.ts b/modules/customers/src/web/list/ui/blocks/index.ts index 5ed67835..e8cf1fa7 100644 --- a/modules/customers/src/web/list/ui/blocks/index.ts +++ b/modules/customers/src/web/list/ui/blocks/index.ts @@ -1,2 +1,2 @@ -export * from "./customer-sheet"; +export * from "./customer-summary-panel"; export * from "./customers-grid"; diff --git a/modules/customers/src/web/list/ui/components/address-cell.tsx b/modules/customers/src/web/list/ui/components/address-cell.tsx index 948c6e1c..330b3020 100644 --- a/modules/customers/src/web/list/ui/components/address-cell.tsx +++ b/modules/customers/src/web/list/ui/components/address-cell.tsx @@ -1,35 +1,44 @@ -import { MapPinIcon } from "lucide-react"; +import { ExternalLinkIcon, MapPinIcon } from "lucide-react"; import { useTranslation } from "../../../i18n"; -import type { CustomerListRow } from "../../../shared"; -const getGoogleMapsUrl = (customer: CustomerListRow) => { - const fullAddress = `${customer.street}, ${customer.postalCode} ${customer.city}, ${customer.province}, ${customer.country}`; +export interface CustomerAddress { + street: string; + street2: string; + postalCode: string; + city: string; + province: string; + country: string; +} + +const getGoogleMapsUrl = (adress: CustomerAddress) => { + const fullAddress = `${adress.street}, ${adress.postalCode} ${adress.city}, ${adress.province}, ${adress.country}`; return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`; }; -export const AddressCell = ({ customer }: { customer: CustomerListRow }) => { +export const AddressCell = ({ address }: { address: CustomerAddress }) => { const { t } = useTranslation(); - const line1 = [customer.street, customer.street2].filter(Boolean).join(", "); - const line2 = [customer.postalCode, customer.city].filter(Boolean).join(" "); - const line3 = [customer.province, customer.country].filter(Boolean).join(", "); + const line1 = [address.street, address.street2].filter(Boolean).join(", "); + const line2 = [address.postalCode, address.city].filter(Boolean).join(" "); + const line3 = [address.province, address.country].filter(Boolean).join(", "); return (
- -
); diff --git a/modules/customers/src/web/list/ui/pages/list-customers-page.tsx b/modules/customers/src/web/list/ui/pages/list-customers-page.tsx index a6efebfe..437d2608 100644 --- a/modules/customers/src/web/list/ui/pages/list-customers-page.tsx +++ b/modules/customers/src/web/list/ui/pages/list-customers-page.tsx @@ -7,18 +7,18 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "../../../i18n"; import { ErrorAlert } from "../../../shared/ui"; import { useListCustomersPageController } from "../../controllers"; -import { CustomerSheet, CustomersGrid, useCustomersGridColumns } from "../blocks"; +import { CustomerSummaryPanel, CustomersGrid, useCustomersGridColumns } from "../blocks"; export const ListCustomersPage = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { listCtrl, sheetCtrl } = useListCustomersPageController(); + const { listCtrl, panelCtrl } = useListCustomersPageController(); const columns = useCustomersGridColumns({ onEditClick: (customer) => navigate(`/customers/${customer.id}/edit`), onViewClick: (customer) => navigate(`/customers/${customer.id}`), - onSummaryClick: (customer) => sheetCtrl.openCustomerSheet(customer.id), + onSummaryClick: (customer) => panelCtrl.openCustomerPanel(customer.id, "view"), //onDeleteClick: (customer) => null, //confirmDelete(inv.id), }); @@ -51,38 +51,43 @@ export const ListCustomersPage = () => { title={t("pages.list.title")} /> - -
- +
+
+
+ +
+ + null} + pageIndex={listCtrl.pageIndex} + pageSize={listCtrl.pageSize} + /> +
+ + navigate(`/customers/${customer.id}/edit`)} + onOpenChange={(open) => { + if (open) panelCtrl.panelState.onOpenChange(true); + else panelCtrl.closePanel(); + }} + onTogglePinned={panelCtrl.panelState.togglePinned} + open={panelCtrl.panelState.isOpen} + visibility={panelCtrl.panelState.visibility} />
- - null} - pageIndex={listCtrl.pageIndex} - pageSize={listCtrl.pageSize} - /> - - {/* Customer Sheet */} - navigate(`/customers/${customer.id}/edit`)} - onOpenChange={sheetCtrl.sheetState.onOpenChange} - onPinnedChange={sheetCtrl.sheetState.setPinned} - open={sheetCtrl.sheetState.sheetIsOpen} - pinned={sheetCtrl.sheetState.sheetIsPinned} - /> ); diff --git a/packages/rdx-ui/package.json b/packages/rdx-ui/package.json index 47e00620..269fcc3f 100644 --- a/packages/rdx-ui/package.json +++ b/packages/rdx-ui/package.json @@ -10,7 +10,7 @@ "./helpers": "./src/helpers/index.ts", "./globals.css": "./src/styles/globals.css", "./postcss.config": "./postcss.config.mjs", - "./components": "./src/components/index.tsx", + "./components": "./src/components/index.ts", "./components/*": "./src/components/*.tsx", "./locales/*": "./src/locales/*", "./hooks": [ diff --git a/packages/rdx-ui/src/components/buttons/index.tsx b/packages/rdx-ui/src/components/buttons/index.ts similarity index 100% rename from packages/rdx-ui/src/components/buttons/index.tsx rename to packages/rdx-ui/src/components/buttons/index.ts diff --git a/packages/rdx-ui/src/components/datatable/index.tsx b/packages/rdx-ui/src/components/datatable/index.ts similarity index 100% rename from packages/rdx-ui/src/components/datatable/index.tsx rename to packages/rdx-ui/src/components/datatable/index.ts diff --git a/packages/rdx-ui/src/components/entity-sheet/entity-sheet-shell.tsx b/packages/rdx-ui/src/components/entity-sheet/entity-sheet-shell.tsx new file mode 100644 index 00000000..7909663d --- /dev/null +++ b/packages/rdx-ui/src/components/entity-sheet/entity-sheet-shell.tsx @@ -0,0 +1,69 @@ +// entity-sheet-shell.tsx + +import { Button, Sheet, SheetContent, SheetTitle } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { PinIcon, PinOffIcon, XIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +export interface EntitySheetShellProps { + open: boolean; + pinned: boolean; + title: string; + onOpenChange: (open: boolean) => void; + onTogglePinned: () => void; + headerActions?: ReactNode; + children: ReactNode; +} + +export const EntitySheetShell = ({ + open, + pinned, + title, + onOpenChange, + onTogglePinned, + headerActions, + children, +}: EntitySheetShellProps) => { + return ( + + +
+ {title} + +
+ +
+ +
+ {headerActions} + {pinned ? null : ( + + )} +
+
+ +
{children}
+
+
+ ); +}; diff --git a/packages/rdx-ui/src/components/entity-sheet/index.ts b/packages/rdx-ui/src/components/entity-sheet/index.ts new file mode 100644 index 00000000..9c45adc4 --- /dev/null +++ b/packages/rdx-ui/src/components/entity-sheet/index.ts @@ -0,0 +1 @@ +export * from "./entity-sheet-shell.tsx"; diff --git a/packages/rdx-ui/src/components/form/index.tsx b/packages/rdx-ui/src/components/form/index.ts similarity index 99% rename from packages/rdx-ui/src/components/form/index.tsx rename to packages/rdx-ui/src/components/form/index.ts index 8440cea8..10483cea 100644 --- a/packages/rdx-ui/src/components/form/index.tsx +++ b/packages/rdx-ui/src/components/form/index.ts @@ -1,9 +1,7 @@ -export * from "./date-picker-input-field/index.ts"; export * from "./DatePickerField.tsx"; - +export * from "./date-picker-input-field/index.ts"; export * from "./multi-select-field.tsx"; export * from "./SelectField.tsx"; export * from "./TextAreaField.tsx"; export * from "./TextField.tsx"; export type * from "./types.d.ts"; - diff --git a/packages/rdx-ui/src/components/index.tsx b/packages/rdx-ui/src/components/index.ts similarity index 55% rename from packages/rdx-ui/src/components/index.tsx rename to packages/rdx-ui/src/components/index.ts index 13c81e76..241f5018 100644 --- a/packages/rdx-ui/src/components/index.tsx +++ b/packages/rdx-ui/src/components/index.ts @@ -1,16 +1,18 @@ -export * from "./buttons/index.tsx"; +export * from "./buttons/index.ts"; export * from "./custom-dialog.tsx"; -export * from "./datatable/index.tsx"; +export * from "./datatable/index.ts"; export * from "./dynamics-tabs.tsx"; +export * from "./entity-sheet/index.ts"; export * from "./error-overlay.tsx"; -export * from "./form/index.tsx"; +export * from "./form/index.ts"; export * from "./full-screen-modal.tsx"; export * from "./grid/index.ts"; -export * from "./layout/index.tsx"; -export * from "./loading-overlay/index.tsx"; +export * from "./layout/index.ts"; +export * from "./loading-overlay/index.ts"; export * from "./logo-verifactu.tsx"; -export * from "./lookup-dialog/index.tsx"; +export * from "./lookup-dialog/index.ts"; export * from "./multi-select.tsx"; export * from "./multiple-selector.tsx"; +export * from "./right-panel/index.ts"; export * from "./scroll-to-top.tsx"; export * from "./tailwind-indicator.tsx"; diff --git a/packages/rdx-ui/src/components/layout/app-breadcrumb.tsx b/packages/rdx-ui/src/components/layout/app-breadcrumb.tsx index 01e62e7f..88d8e3bb 100644 --- a/packages/rdx-ui/src/components/layout/app-breadcrumb.tsx +++ b/packages/rdx-ui/src/components/layout/app-breadcrumb.tsx @@ -11,16 +11,16 @@ import { export const AppBreadcrumb = () => { return ( -
-
- - +
+
+ + - - Building Your Application + + Building Your Application - + Data Fetching diff --git a/packages/rdx-ui/src/components/layout/index.tsx b/packages/rdx-ui/src/components/layout/index.ts similarity index 100% rename from packages/rdx-ui/src/components/layout/index.tsx rename to packages/rdx-ui/src/components/layout/index.ts diff --git a/packages/rdx-ui/src/components/loading-overlay/index.tsx b/packages/rdx-ui/src/components/loading-overlay/index.ts similarity index 100% rename from packages/rdx-ui/src/components/loading-overlay/index.tsx rename to packages/rdx-ui/src/components/loading-overlay/index.ts diff --git a/packages/rdx-ui/src/components/lookup-dialog/index.tsx b/packages/rdx-ui/src/components/lookup-dialog/index.ts similarity index 100% rename from packages/rdx-ui/src/components/lookup-dialog/index.tsx rename to packages/rdx-ui/src/components/lookup-dialog/index.ts diff --git a/packages/rdx-ui/src/components/right-panel/index.ts b/packages/rdx-ui/src/components/right-panel/index.ts new file mode 100644 index 00000000..aeb6aa2c --- /dev/null +++ b/packages/rdx-ui/src/components/right-panel/index.ts @@ -0,0 +1 @@ +export * from "./right-panel.tsx"; diff --git a/packages/rdx-ui/src/components/right-panel/right-panel-header.tsx b/packages/rdx-ui/src/components/right-panel/right-panel-header.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/rdx-ui/src/components/right-panel/right-panel-types.ts b/packages/rdx-ui/src/components/right-panel/right-panel-types.ts new file mode 100644 index 00000000..1227d928 --- /dev/null +++ b/packages/rdx-ui/src/components/right-panel/right-panel-types.ts @@ -0,0 +1,10 @@ +import type { ReactNode } from "react"; + +export interface RightPanelProps { + open: boolean; + title: string; + className?: string; + headerActions?: ReactNode; + children: ReactNode; + onOpenChange?: (open: boolean) => void; +} diff --git a/packages/rdx-ui/src/components/right-panel/right-panel.tsx b/packages/rdx-ui/src/components/right-panel/right-panel.tsx new file mode 100644 index 00000000..33deacd4 --- /dev/null +++ b/packages/rdx-ui/src/components/right-panel/right-panel.tsx @@ -0,0 +1,53 @@ +import { Button } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { XIcon } from "lucide-react"; + +import type { RightPanelProps } from "./right-panel-types.ts"; + +export const RightPanel = ({ + open, + title, + className, + headerActions, + children, + onOpenChange, +}: RightPanelProps) => { + if (!open) return null; + + return ( + + ); +}; diff --git a/packages/rdx-ui/src/hooks/index.ts b/packages/rdx-ui/src/hooks/index.ts index 6e0460dd..020db2ab 100644 --- a/packages/rdx-ui/src/hooks/index.ts +++ b/packages/rdx-ui/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from "./sheet/index.ts"; +export * from "./side-panel/index.ts"; export * from "./use-device-info.ts"; export * from "./use-row-selection.ts"; diff --git a/packages/rdx-ui/src/hooks/side-panel/index.ts b/packages/rdx-ui/src/hooks/side-panel/index.ts new file mode 100644 index 00000000..d0adcfab --- /dev/null +++ b/packages/rdx-ui/src/hooks/side-panel/index.ts @@ -0,0 +1 @@ +export * from "./use-side-panel-state.ts"; diff --git a/packages/rdx-ui/src/hooks/side-panel/use-side-panel-state.ts b/packages/rdx-ui/src/hooks/side-panel/use-side-panel-state.ts new file mode 100644 index 00000000..b83e2d16 --- /dev/null +++ b/packages/rdx-ui/src/hooks/side-panel/use-side-panel-state.ts @@ -0,0 +1,88 @@ +import { useCallback, useState } from "react"; + +export type RightPanelMode = "view" | "edit" | "create"; +export type RightPanelVisibility = "hidden" | "temporary" | "persistent"; + +export interface RightPanelStateOptions { + defaultMode?: RightPanelMode; + defaultVisibility?: RightPanelVisibility; +} + +export interface RightPanelState { + mode: RightPanelMode; + visibility: RightPanelVisibility; + isOpen: boolean; + isPinned: boolean; +} + +export interface RightPanelStateActions { + onOpenChange: (next: boolean) => void; + openTemporary: (mode?: RightPanelMode) => void; + openPersistent: (mode?: RightPanelMode) => void; + togglePinned: () => void; + close: () => void; + reset: () => void; +} + +export type RightPanelStateController = RightPanelState & RightPanelStateActions; + +const DEFAULT_MODE: RightPanelMode = "view"; +const DEFAULT_VISIBILITY: RightPanelVisibility = "hidden"; + +export const useRightPanelState = ( + options: RightPanelStateOptions = {} +): RightPanelStateController => { + const { defaultMode = DEFAULT_MODE, defaultVisibility = DEFAULT_VISIBILITY } = options; + + const [mode, setMode] = useState(defaultMode); + const [visibility, setVisibility] = useState(defaultVisibility); + + const isOpen = visibility !== "hidden"; + const isPinned = visibility === "persistent"; + + const close = useCallback(() => { + setVisibility("hidden"); + }, []); + + const onOpenChange = useCallback((next: boolean) => { + setVisibility((prev) => { + if (!next) return "hidden"; + return prev === "persistent" ? "persistent" : "temporary"; + }); + }, []); + + const openTemporary = useCallback((nextMode: RightPanelMode = DEFAULT_MODE) => { + setMode(nextMode); + setVisibility("temporary"); + }, []); + + const openPersistent = useCallback((nextMode: RightPanelMode = DEFAULT_MODE) => { + setMode(nextMode); + setVisibility("persistent"); + }, []); + + const togglePinned = useCallback(() => { + setVisibility((prev) => { + if (prev === "hidden") return prev; + return prev === "persistent" ? "temporary" : "persistent"; + }); + }, []); + + const reset = useCallback(() => { + setMode(defaultMode); + setVisibility(defaultVisibility); + }, [defaultMode, defaultVisibility]); + + return { + mode, + visibility, + isOpen, + isPinned, + onOpenChange, + openTemporary, + openPersistent, + togglePinned, + close, + reset, + }; +}; diff --git a/packages/rdx-ui/src/index.ts b/packages/rdx-ui/src/index.ts index 3bdf4f2b..56dbe579 100644 --- a/packages/rdx-ui/src/index.ts +++ b/packages/rdx-ui/src/index.ts @@ -1,5 +1,5 @@ export const PACKAGE_NAME = "rdx-ui"; -export * from "./components/index.tsx"; +export * from "./components/index.ts"; export * from "./helpers/index.ts"; export * from "./hooks/index.ts"; diff --git a/packages/shadcn-ui/src/components/separator.tsx b/packages/shadcn-ui/src/components/separator.tsx index aea5b66d..c9737d58 100644 --- a/packages/shadcn-ui/src/components/separator.tsx +++ b/packages/shadcn-ui/src/components/separator.tsx @@ -1,7 +1,6 @@ -import * as React from "react" -import { Separator as SeparatorPrimitive } from "radix-ui" - -import { cn } from "@repo/shadcn-ui/lib/utils" +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { Separator as SeparatorPrimitive } from "radix-ui"; +import type * as React from "react"; function Separator({ className, @@ -11,16 +10,16 @@ function Separator({ }: React.ComponentProps) { return ( - ) + ); } -export { Separator } +export { Separator };