Customers
This commit is contained in:
parent
51adb8d2b0
commit
8e547ff551
@ -226,7 +226,8 @@
|
||||
"useValidAnchor": "error",
|
||||
"useValidAriaProps": "error",
|
||||
"useValidAriaValues": "error",
|
||||
"useValidLang": "error"
|
||||
"useValidLang": "error",
|
||||
"useButtonType": "info"
|
||||
},
|
||||
"performance": {
|
||||
"noAccumulatingSpread": "warn",
|
||||
|
||||
@ -46,7 +46,7 @@ export class CustomerFullSnapshotBuilder implements ICustomerFullSnapshotBuilder
|
||||
|
||||
legal_record: maybeToEmptyString(customer.legalRecord, (value) => value.toPrimitive()),
|
||||
|
||||
default_taxes: customer.defaultTaxes.getAll().map((tax) => tax.toString()),
|
||||
default_taxes: customer.defaultTaxes.toKey(),
|
||||
|
||||
language_code: customer.languageCode.toPrimitive(),
|
||||
currency_code: customer.currencyCode.toPrimitive(),
|
||||
|
||||
@ -30,7 +30,7 @@ export interface ICustomerFullSnapshot {
|
||||
|
||||
legal_record: string;
|
||||
|
||||
default_taxes: string[];
|
||||
default_taxes: string;
|
||||
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
@ -30,7 +30,7 @@ export const GetCustomerByIdResponseSchema = z.object({
|
||||
|
||||
legal_record: z.string(),
|
||||
|
||||
default_taxes: z.array(z.string()),
|
||||
default_taxes: z.string(),
|
||||
status: z.string(),
|
||||
language_code: z.string(),
|
||||
currency_code: z.string(),
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import { useSheetState } from "@repo/rdx-ui/hooks";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { useCustomerGetQuery } from "../../shared/hooks";
|
||||
|
||||
export const useCustomerSheetController = (initialCustomerId = "") => {
|
||||
const [customerId, setCustomerId] = useState(initialCustomerId);
|
||||
const sheetState = useSheetState({
|
||||
defaultOpen: false,
|
||||
defaultPinned: false,
|
||||
defaultMode: "view",
|
||||
});
|
||||
|
||||
const query = useCustomerGetQuery(customerId, {
|
||||
enabled: Boolean(customerId),
|
||||
});
|
||||
|
||||
const openCustomerSheet = useCallback(
|
||||
(nextCustomerId: string) => {
|
||||
setCustomerId(nextCustomerId);
|
||||
sheetState.openInMode("view");
|
||||
},
|
||||
[sheetState]
|
||||
);
|
||||
|
||||
const closeCustomerSheet = useCallback(() => {
|
||||
sheetState.closeSheet();
|
||||
}, [sheetState]);
|
||||
|
||||
return {
|
||||
customer: query.data,
|
||||
customerId,
|
||||
setCustomerId,
|
||||
|
||||
openCustomerSheet,
|
||||
closeCustomerSheet,
|
||||
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
|
||||
refetch: query.refetch,
|
||||
|
||||
sheetState,
|
||||
};
|
||||
};
|
||||
@ -1,9 +1,13 @@
|
||||
import { useCustomerSheetController } from "./use-customer-sheet.controller";
|
||||
import { useListCustomersController } from "./use-list-customers.controller";
|
||||
|
||||
export function useListCustomersPageController() {
|
||||
const listCtrl = useListCustomersController();
|
||||
|
||||
const sheetCtrl = useCustomerSheetController();
|
||||
|
||||
return {
|
||||
listCtrl,
|
||||
sheetCtrl,
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,400 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
Badge,
|
||||
Button,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
Building2Icon,
|
||||
CopyIcon,
|
||||
ExternalLinkIcon,
|
||||
FileTextIcon,
|
||||
GlobeIcon,
|
||||
MailIcon,
|
||||
MapPinIcon,
|
||||
PencilIcon,
|
||||
PhoneIcon,
|
||||
PinIcon,
|
||||
PinOffIcon,
|
||||
TrendingUpIcon,
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import type { Customer } from "../../../../shared";
|
||||
|
||||
interface CustomerSheetProps {
|
||||
customer?: Customer;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
pinned: boolean;
|
||||
onPinnedChange: (pinned: boolean) => void;
|
||||
onEdit?: (customer: Customer) => void;
|
||||
}
|
||||
|
||||
// Datos de ejemplo ERP para demostración
|
||||
const mockERPData = {
|
||||
totalPurchases: 45750.8,
|
||||
purchasesThisYear: 12350.25,
|
||||
lastInvoices: [
|
||||
{
|
||||
id: "1",
|
||||
number: "FAC-2024-0156",
|
||||
date: "2024-01-15",
|
||||
amount: 1250.0,
|
||||
status: "paid" as const,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
number: "FAC-2024-0142",
|
||||
date: "2024-01-08",
|
||||
amount: 890.5,
|
||||
status: "paid" as const,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
number: "FAC-2024-0128",
|
||||
date: "2023-12-22",
|
||||
amount: 2100.0,
|
||||
status: "pending" as const,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
number: "FAC-2023-0098",
|
||||
date: "2023-11-30",
|
||||
amount: 750.25,
|
||||
status: "overdue" as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const CustomerSheet = ({
|
||||
customer,
|
||||
open,
|
||||
onOpenChange,
|
||||
pinned,
|
||||
onPinnedChange,
|
||||
onEdit,
|
||||
}: CustomerSheetProps) => {
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("es-ES", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("es-ES", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const getGoogleMapsUrl = (customer: Customer) => {
|
||||
const fullAddress = `${customer.street}, ${customer.postalCode} ${customer.city}, ${customer.province}, ${customer.country}`;
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => {
|
||||
switch (status) {
|
||||
case "paid":
|
||||
return (
|
||||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200" variant="outline">
|
||||
Pagada
|
||||
</Badge>
|
||||
);
|
||||
case "pending":
|
||||
return (
|
||||
<Badge className="bg-amber-50 text-amber-700 border-amber-200" variant="outline">
|
||||
Pendiente
|
||||
</Badge>
|
||||
);
|
||||
case "overdue":
|
||||
return (
|
||||
<Badge className="bg-red-50 text-red-700 border-red-200" variant="outline">
|
||||
Vencida
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Combinar datos del cliente con datos mock de ERP
|
||||
const customerWithERP = { ...customer, ...mockERPData };
|
||||
|
||||
return (
|
||||
<Sheet modal={!pinned} onOpenChange={onOpenChange} open={open}>
|
||||
<SheetContent
|
||||
className={cn("flex w-full flex-col p-0 sm:max-w-md", pinned && "shadow-none")}
|
||||
showCloseButton={false}
|
||||
side="right"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||
<SheetTitle className="sr-only">Ficha de cliente</SheetTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => onPinnedChange(!pinned)}
|
||||
size="icon-sm"
|
||||
title={pinned ? "Desfijar panel" : "Fijar panel"}
|
||||
variant="ghost"
|
||||
>
|
||||
{pinned ? <PinOffIcon className="size-4" /> : <PinIcon className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button onClick={() => null /*onEdit?.(customer?)*/} size="icon-sm" variant="ghost">
|
||||
<PencilIcon className="size-4" />
|
||||
<span className="sr-only">Editar</span>
|
||||
</Button>
|
||||
{!pinned && (
|
||||
<Button onClick={() => onOpenChange(false)} size="icon-sm" variant="ghost">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Cerrar</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenido scrolleable */}
|
||||
{customer ? (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Info principal del cliente */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar className="size-14 border-2 border-background shadow-md">
|
||||
<AvatarFallback
|
||||
className={
|
||||
customer.status === "active"
|
||||
? "bg-emerald-100 text-emerald-700 text-lg"
|
||||
: "bg-muted text-muted-foreground text-lg"
|
||||
}
|
||||
>
|
||||
{getInitials(customer.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold text-foreground truncate">
|
||||
{customer.name}
|
||||
</h2>
|
||||
<Badge
|
||||
className="shrink-0"
|
||||
variant={customer.status === "active" ? "default" : "secondary"}
|
||||
>
|
||||
{customer.status === "active" ? "Activo" : "Inactivo"}
|
||||
</Badge>
|
||||
</div>
|
||||
{customer.tradeName && (
|
||||
<p className="text-sm text-muted-foreground truncate">{customer.tradeName}</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Badge className="gap-1" variant="outline">
|
||||
{customer.isCompany ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
Empresa
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserIcon className="size-3" />
|
||||
Particular
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
<button
|
||||
className="flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={() => copyToClipboard(customer.tin)}
|
||||
title="Copiar NIF/CIF"
|
||||
type="button"
|
||||
>
|
||||
{customer.tin}
|
||||
<CopyIcon className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Datos de contacto */}
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-medium text-foreground mb-3">Contacto</h3>
|
||||
<div className="space-y-2.5">
|
||||
<a
|
||||
className="flex items-center gap-3 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href={`mailto:${customer.primaryEmail}`}
|
||||
>
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
<span className="truncate">{customer.primaryEmail}</span>
|
||||
</a>
|
||||
{customer.secondaryEmail && (
|
||||
<a
|
||||
className="flex items-center gap-3 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href={`mailto:${customer.secondaryEmail}`}
|
||||
>
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
<span className="truncate">{customer.secondaryEmail}</span>
|
||||
</a>
|
||||
)}
|
||||
{customer.primaryPhone && (
|
||||
<a
|
||||
className="flex items-center gap-3 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href={`tel:${customer.primaryPhone}`}
|
||||
>
|
||||
<PhoneIcon className="size-4 shrink-0" />
|
||||
<span>{customer.primaryPhone}</span>
|
||||
</a>
|
||||
)}
|
||||
{customerWithERP.primaryMobile && (
|
||||
<a
|
||||
className="flex items-center gap-3 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href={`tel:${customerWithERP.primaryMobile}`}
|
||||
>
|
||||
<PhoneIcon className="size-4 shrink-0" />
|
||||
<span>{customerWithERP.primaryMobile}</span>
|
||||
</a>
|
||||
)}
|
||||
{customerWithERP.website && (
|
||||
<a
|
||||
className="flex items-center gap-3 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href={customerWithERP.website}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<GlobeIcon className="size-4 shrink-0" />
|
||||
<span className="truncate">{customerWithERP.website}</span>
|
||||
<ExternalLinkIcon className="size-3 shrink-0" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Dirección */}
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-medium text-foreground mb-3">Dirección</h3>
|
||||
<a
|
||||
className="group flex items-start gap-3 rounded-lg p-2 -m-2 transition-colors hover:bg-muted"
|
||||
href={getGoogleMapsUrl(customer)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<MapPinIcon className="size-4 shrink-0 mt-0.5 text-muted-foreground group-hover:text-primary" />
|
||||
<div className="text-sm text-muted-foreground group-hover:text-foreground">
|
||||
<p>{customer.street}</p>
|
||||
<p>
|
||||
{customer.postalCode} {customer.city}
|
||||
</p>
|
||||
<p>
|
||||
{customer.province}, {customer.country}
|
||||
</p>
|
||||
</div>
|
||||
<ExternalLinkIcon className="size-3 shrink-0 mt-0.5 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Volumen de compras */}
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
|
||||
<TrendingUpIcon className="size-4" />
|
||||
Volumen de compras
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg border bg-muted/50 p-3">
|
||||
<p className="text-xs text-muted-foreground">Total histórico</p>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{formatCurrency(customerWithERP.totalPurchases || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-muted/50 p-3">
|
||||
<p className="text-xs text-muted-foreground">Este año</p>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{formatCurrency(customerWithERP.purchasesThisYear || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Últimas facturas */}
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
|
||||
<FileTextIcon className="size-4" />
|
||||
Últimas facturas
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{customerWithERP.lastInvoices?.map((invoice) => (
|
||||
<button
|
||||
className="w-full flex items-center justify-between rounded-lg border p-3 text-left transition-colors hover:bg-muted"
|
||||
key={invoice.id}
|
||||
onClick={() => console.log("Ver factura:", invoice.number)}
|
||||
type="button"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{invoice.number}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{formatDate(invoice.date)}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-sm font-medium">{formatCurrency(invoice.amount)}</span>
|
||||
{getInvoiceStatusBadge(invoice.status)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button className="w-full mt-3" size="sm" variant="outline">
|
||||
Ver todas las facturas
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<p className="text-sm text-muted-foreground">Selecciona un cliente</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer con acciones */}
|
||||
<div className="border-t p-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => console.log("Crear proforma para:", customer?.id)}
|
||||
variant="outline"
|
||||
>
|
||||
<FileTextIcon className="size-4 mr-2" />
|
||||
Nueva proforma
|
||||
</Button>
|
||||
<Button className="flex-1" onClick={() => null /*onEdit?.(customer?)*/}>
|
||||
<PencilIcon className="size-4 mr-2" />
|
||||
Editar cliente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer-sheet";
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from "../../../../i18n";
|
||||
import type { CustomerList, CustomerListRow } from "../../../../shared";
|
||||
|
||||
interface CustomersGridProps {
|
||||
data: CustomerList;
|
||||
data?: CustomerList;
|
||||
loading: boolean;
|
||||
fetching?: boolean;
|
||||
|
||||
@ -33,7 +33,7 @@ export const CustomersGrid = ({
|
||||
}: CustomersGridProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { items, total_items } = data;
|
||||
const { items, total_items } = data || { items: [], total_items: 0 };
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
|
||||
@ -23,6 +23,7 @@ import { AddressCell, ContactCell, Initials } from "../../components";
|
||||
type GridActionHandlers = {
|
||||
onEditClick?: (customer: CustomerListRow) => void;
|
||||
onViewClick?: (customer: CustomerListRow) => void;
|
||||
onSummaryClick?: (customer: CustomerListRow) => void;
|
||||
onDeleteClick?: (customer: CustomerListRow) => void;
|
||||
};
|
||||
|
||||
@ -30,7 +31,7 @@ export function useCustomersGridColumns(
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
): ColumnDef<CustomerListRow, unknown>[] {
|
||||
const { t } = useTranslation();
|
||||
const { onEditClick, onViewClick, onDeleteClick } = actionHandlers;
|
||||
const { onEditClick, onViewClick, onDeleteClick, onSummaryClick } = actionHandlers;
|
||||
|
||||
return React.useMemo<ColumnDef<CustomerListRow, unknown>[]>(
|
||||
() => [
|
||||
@ -53,7 +54,7 @@ export function useCustomersGridColumns(
|
||||
return (
|
||||
<button
|
||||
className="flex items-start gap-3 text-left transition-colors hover:opacity-80 cursor-pointer"
|
||||
onClick={onViewClick ? () => onViewClick(customer) : undefined}
|
||||
onClick={onSummaryClick ? () => onSummaryClick(customer) : undefined}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
@ -74,15 +75,15 @@ export function useCustomersGridColumns(
|
||||
<span className="font-medium text-foreground hover:underline">
|
||||
{customer.name}
|
||||
</span>
|
||||
{customer.trade_name && (
|
||||
<span className="text-muted-foreground">({customer.trade_name})</span>
|
||||
{customer.tradeName && (
|
||||
<span className="text-muted-foreground">({customer.tradeName})</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-mono">{customer.tin}</span>
|
||||
<Badge className="gap-1 px-1.5 py-0 text-xs font-normal" variant="outline">
|
||||
{customer.is_company ? (
|
||||
{customer.isCompany ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
{t("form_fields.customer_type.company")}
|
||||
@ -111,7 +112,7 @@ export function useCustomersGridColumns(
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) =>
|
||||
`${row.email_primary} ${row.phone_primary} ${row.mobile_primary} ${row.website}`,
|
||||
`${row.primaryEmail} ${row.primaryPhone} ${row.primaryMobile} ${row.website}`,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
@ -127,7 +128,7 @@ export function useCustomersGridColumns(
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) =>
|
||||
`${row.street} ${row.street2} ${row.city} ${row.postal_code} ${row.province} ${row.country}`,
|
||||
`${row.street} ${row.street2} ${row.city} ${row.postalCode} ${row.province} ${row.country}`,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
@ -148,7 +149,7 @@ export function useCustomersGridColumns(
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const customer = row.original;
|
||||
const { website, email_primary } = customer;
|
||||
const { website, primaryEmail: email_primary } = customer;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
@ -216,6 +217,6 @@ export function useCustomersGridColumns(
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, onDeleteClick, onEditClick, onViewClick]
|
||||
[t, onDeleteClick, onEditClick, onViewClick, onSummaryClick]
|
||||
);
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./customer-sheet";
|
||||
export * from "./customers-grid";
|
||||
|
||||
@ -4,14 +4,14 @@ import { useTranslation } from "../../../i18n";
|
||||
import type { CustomerListRow } from "../../../shared";
|
||||
|
||||
const getGoogleMapsUrl = (customer: CustomerListRow) => {
|
||||
const fullAddress = `${customer.street}, ${customer.postal_code} ${customer.city}, ${customer.province}, ${customer.country}`;
|
||||
const fullAddress = `${customer.street}, ${customer.postalCode} ${customer.city}, ${customer.province}, ${customer.country}`;
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`;
|
||||
};
|
||||
|
||||
export const AddressCell = ({ customer }: { customer: CustomerListRow }) => {
|
||||
const { t } = useTranslation();
|
||||
const line1 = [customer.street, customer.street2].filter(Boolean).join(", ");
|
||||
const line2 = [customer.postal_code, customer.city].filter(Boolean).join(" ");
|
||||
const line2 = [customer.postalCode, customer.city].filter(Boolean).join(" ");
|
||||
const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
|
||||
return (
|
||||
<address className="not-italic flex items-start gap-2 text-muted-foreground hover:text-primary transition-colors">
|
||||
|
||||
@ -3,32 +3,32 @@ import { MailIcon, PhoneIcon } from "lucide-react";
|
||||
|
||||
export const ContactCell = ({ customer }: { customer: CustomerListRow }) => (
|
||||
<div className="flex flex-col gap-1.5 text-sm text-muted-foreground transition-colors ">
|
||||
{customer.email_primary && (
|
||||
{customer.primaryEmail && (
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
href={`mailto:${customer.email_primary}`}
|
||||
href={`mailto:${customer.primaryEmail}`}
|
||||
>
|
||||
<MailIcon className="size-3.5" />
|
||||
{customer.email_primary}
|
||||
{customer.primaryEmail}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{customer.email_secondary && (
|
||||
{customer.secondaryEmail && (
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
href={`mailto:${customer.email_secondary}`}
|
||||
href={`mailto:${customer.secondaryEmail}`}
|
||||
>
|
||||
<MailIcon className="size-3.5" />
|
||||
{customer.email_secondary}
|
||||
{customer.secondaryEmail}
|
||||
</a>
|
||||
)}
|
||||
{customer.phone_primary && (
|
||||
{customer.primaryPhone && (
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
href={`tel:${customer.phone_primary}`}
|
||||
href={`tel:${customer.primaryPhone}`}
|
||||
>
|
||||
<PhoneIcon className="size-3.5" />
|
||||
{customer.phone_primary}
|
||||
{customer.primaryPhone}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -7,21 +7,22 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { ErrorAlert } from "../../../shared/ui";
|
||||
import { useListCustomersPageController } from "../../controllers";
|
||||
import { CustomersGrid, useCustomersGridColumns } from "../blocks";
|
||||
import { CustomerSheet, CustomersGrid, useCustomersGridColumns } from "../blocks";
|
||||
|
||||
export const ListCustomersPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { listCtrl } = useListCustomersPageController();
|
||||
const { listCtrl, sheetCtrl } = useListCustomersPageController();
|
||||
|
||||
const columns = useCustomersGridColumns({
|
||||
onEditClick: (customer) => navigate(`/customers/${customer.id}/edit`),
|
||||
onViewClick: (customer) => navigate(`/customers/${customer.id}`),
|
||||
onSummaryClick: (customer) => sheetCtrl.openCustomerSheet(customer.id),
|
||||
//onDeleteClick: (customer) => null, //confirmDelete(inv.id),
|
||||
});
|
||||
|
||||
if (listCtrl.isError || !listCtrl.data) {
|
||||
if (listCtrl.isError) {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
@ -67,10 +68,21 @@ export const ListCustomersPage = () => {
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
// acciones rápidas del grid → page controller
|
||||
//onRowClick={(id) => navigate(`/proformas/${id}`)}
|
||||
onRowClick={(id) => null}
|
||||
pageIndex={listCtrl.pageIndex}
|
||||
pageSize={listCtrl.pageSize}
|
||||
/>
|
||||
|
||||
{/* Customer Sheet */}
|
||||
<CustomerSheet
|
||||
customer={sheetCtrl.customer}
|
||||
//mode={sheetCtrl.sheet.mode}
|
||||
onEdit={(customer) => navigate(`/customers/${customer.id}/edit`)}
|
||||
onOpenChange={sheetCtrl.sheetState.onOpenChange}
|
||||
onPinnedChange={sheetCtrl.sheetState.setPinned}
|
||||
open={sheetCtrl.sheetState.sheetIsOpen}
|
||||
pinned={sheetCtrl.sheetState.sheetIsPinned}
|
||||
/>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,39 +1,46 @@
|
||||
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
||||
import type { Customer } from "../entities";
|
||||
|
||||
export const mapGetCustomerByIdResponseDtoToCustomerAdapter = (
|
||||
dto: GetCustomerByIdResponseDTO
|
||||
): Customer => ({
|
||||
id: dto.id,
|
||||
companyId: dto.company_id,
|
||||
reference: dto.reference,
|
||||
export const GetCustomerByIdAdapter = {
|
||||
fromDTO(dto: GetCustomerByIdResponseDTO, context?: unknown): Customer {
|
||||
const taxesAdapter = (taxes: string) => taxes.split(";").filter((item) => item !== "#") || [];
|
||||
|
||||
isCompany: dto.is_company,
|
||||
name: dto.name,
|
||||
tradeName: dto.trade_name,
|
||||
tin: dto.tin,
|
||||
const defaultTaxes = taxesAdapter(dto.default_taxes);
|
||||
console.log("defaultTaxes", defaultTaxes);
|
||||
|
||||
street: dto.street,
|
||||
street2: dto.street2,
|
||||
city: dto.city,
|
||||
province: dto.province,
|
||||
postalCode: dto.postal_code,
|
||||
country: dto.country,
|
||||
return {
|
||||
id: dto.id,
|
||||
companyId: dto.company_id,
|
||||
reference: dto.reference,
|
||||
|
||||
primaryEmail: dto.email_primary,
|
||||
secondaryEmail: dto.email_secondary,
|
||||
primaryPhone: dto.phone_primary,
|
||||
secondaryPhone: dto.phone_secondary,
|
||||
primaryMobile: dto.mobile_primary,
|
||||
secondaryMobile: dto.mobile_secondary,
|
||||
isCompany: dto.is_company,
|
||||
name: dto.name,
|
||||
tradeName: dto.trade_name,
|
||||
tin: dto.tin,
|
||||
|
||||
fax: dto.fax,
|
||||
website: dto.website,
|
||||
street: dto.street,
|
||||
street2: dto.street2,
|
||||
city: dto.city,
|
||||
province: dto.province,
|
||||
postalCode: dto.postal_code,
|
||||
country: dto.country,
|
||||
|
||||
legalRecord: dto.legal_record,
|
||||
primaryEmail: dto.email_primary,
|
||||
secondaryEmail: dto.email_secondary,
|
||||
primaryPhone: dto.phone_primary,
|
||||
secondaryPhone: dto.phone_secondary,
|
||||
primaryMobile: dto.mobile_primary,
|
||||
secondaryMobile: dto.mobile_secondary,
|
||||
|
||||
defaultTaxes: dto.default_taxes,
|
||||
status: dto.status,
|
||||
languageCode: dto.language_code,
|
||||
currencyCode: dto.currency_code,
|
||||
});
|
||||
fax: dto.fax,
|
||||
website: dto.website,
|
||||
|
||||
legalRecord: dto.legal_record,
|
||||
|
||||
defaultTaxes: defaultTaxes,
|
||||
status: dto.status,
|
||||
languageCode: dto.language_code,
|
||||
currencyCode: dto.currency_code,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -25,33 +25,33 @@ const ListCustomersRowAdapter = {
|
||||
|
||||
return {
|
||||
id: rowDto.id,
|
||||
company_id: rowDto.company_id,
|
||||
companyId: rowDto.company_id,
|
||||
status: rowDto.status,
|
||||
reference: rowDto.reference,
|
||||
|
||||
is_company: rowDto.is_company === "1",
|
||||
isCompany: rowDto.is_company === "1",
|
||||
name: rowDto.name,
|
||||
trade_name: rowDto.trade_name,
|
||||
tradeName: rowDto.trade_name,
|
||||
tin: rowDto.tin,
|
||||
|
||||
street: rowDto.street,
|
||||
street2: rowDto.street2,
|
||||
city: rowDto.city,
|
||||
province: rowDto.province,
|
||||
postal_code: rowDto.postal_code,
|
||||
postalCode: rowDto.postal_code,
|
||||
country: rowDto.country,
|
||||
|
||||
email_primary: rowDto.email_primary,
|
||||
email_secondary: rowDto.email_secondary,
|
||||
phone_primary: rowDto.phone_primary,
|
||||
phone_secondary: rowDto.phone_secondary,
|
||||
mobile_primary: rowDto.mobile_primary,
|
||||
mobile_secondary: rowDto.mobile_secondary,
|
||||
primaryEmail: rowDto.email_primary,
|
||||
secondaryEmail: rowDto.email_secondary,
|
||||
primaryPhone: rowDto.phone_primary,
|
||||
secondaryPhone: rowDto.phone_secondary,
|
||||
primaryMobile: rowDto.mobile_primary,
|
||||
secondaryMobile: rowDto.mobile_secondary,
|
||||
fax: rowDto.fax,
|
||||
website: rowDto.website,
|
||||
|
||||
language_code: rowDto.language_code,
|
||||
currency_code: rowDto.currency_code,
|
||||
languageCode: rowDto.language_code,
|
||||
currencyCode: rowDto.currency_code,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { IDataSource } from "@erp/core/client";
|
||||
|
||||
import type { Customer } from "../entities";
|
||||
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
||||
|
||||
export async function getCustomerById(dataSource: IDataSource, signal: AbortSignal, id?: string) {
|
||||
if (!id) throw new Error("customerId is required");
|
||||
const response = dataSource.getOne<Customer>("customers", id, { signal });
|
||||
const response = dataSource.getOne<GetCustomerByIdResponseDTO>("customers", id, { signal });
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -1,30 +1,31 @@
|
||||
export interface CustomerListRow {
|
||||
id: string;
|
||||
company_id: string;
|
||||
companyId: string;
|
||||
status: string;
|
||||
reference: string;
|
||||
|
||||
is_company: boolean;
|
||||
isCompany: boolean;
|
||||
name: string;
|
||||
trade_name: string;
|
||||
tradeName: string;
|
||||
tin: string;
|
||||
|
||||
street: string;
|
||||
street2: string;
|
||||
city: string;
|
||||
province: string;
|
||||
postal_code: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
|
||||
email_primary: string;
|
||||
email_secondary: string;
|
||||
phone_primary: string;
|
||||
phone_secondary: string;
|
||||
mobile_primary: string;
|
||||
mobile_secondary: string;
|
||||
primaryEmail: string;
|
||||
secondaryEmail: string;
|
||||
primaryPhone: string;
|
||||
secondaryPhone: string;
|
||||
primaryMobile: string;
|
||||
secondaryMobile: string;
|
||||
|
||||
fax: string;
|
||||
website: string;
|
||||
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ import {
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { type Customer, getCustomerById } from "../api";
|
||||
import { GetCustomerByIdAdapter } from "../adapters";
|
||||
import { getCustomerById } from "../api";
|
||||
import type { Customer } from "../entities";
|
||||
|
||||
export const CUSTOMER_QUERY_KEY = (customerId?: string): QueryKey => [
|
||||
"customers:detail",
|
||||
@ -28,9 +30,12 @@ export const useCustomerGetQuery = (
|
||||
|
||||
return useQuery<Customer, DefaultError>({
|
||||
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
||||
queryFn: async ({ signal }) => getCustomerById(dataSource, signal, customerId),
|
||||
queryFn: async ({ signal }) => {
|
||||
const dto = await getCustomerById(dataSource, signal, customerId);
|
||||
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||
},
|
||||
enabled,
|
||||
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||
placeholderData: (previousData) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import type { Customer } from "../../shared";
|
||||
import type { CustomerData } from "../types";
|
||||
|
||||
/**
|
||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||
*/
|
||||
export const CustomerDtoAdapter = {
|
||||
fromDto(customerDto: Customer, context?: unknown): CustomerData {
|
||||
return {
|
||||
...customerDto,
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from "./customer-dto.adapter";
|
||||
@ -1,20 +1,22 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useCustomerGetQuery } from "../../shared/hooks";
|
||||
import { CustomerDtoAdapter } from "../adapters";
|
||||
|
||||
export const useCustomerViewController = () => {
|
||||
const [customerId, setCustomerId] = useState("");
|
||||
export const useCustomerViewController = (initialCustomerId = "") => {
|
||||
const [customerId, setCustomerId] = useState(initialCustomerId);
|
||||
|
||||
const query = useCustomerGetQuery(customerId);
|
||||
const data = useMemo(
|
||||
() => (query.data ? CustomerDtoAdapter.fromDto(query.data) : undefined),
|
||||
[query.data]
|
||||
);
|
||||
|
||||
return {
|
||||
...query,
|
||||
data,
|
||||
data: query.data,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
|
||||
refetch: query.refetch,
|
||||
|
||||
customerId,
|
||||
setCustomerId,
|
||||
};
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./types";
|
||||
@ -1,3 +0,0 @@
|
||||
import type { Customer } from "../../shared/api";
|
||||
|
||||
export type CustomerData = Customer;
|
||||
@ -77,7 +77,7 @@ export const CustomerViewPage = () => {
|
||||
<Badge className="font-mono" variant="secondary">
|
||||
{customerData?.tin}
|
||||
</Badge>
|
||||
<Badge variant="outline">{customerData?.is_company ? "Empresa" : "Persona"}</Badge>
|
||||
<Badge variant="outline">{customerData?.isCompany ? "Empresa" : "Persona"}</Badge>
|
||||
</div>
|
||||
}
|
||||
rightSlot={
|
||||
@ -102,8 +102,8 @@ export const CustomerViewPage = () => {
|
||||
title={
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{customerData?.name}{" "}
|
||||
{customerData?.trade_name && (
|
||||
<span className="text-muted-foreground">({customerData.trade_name})</span>
|
||||
{customerData?.tradeName && (
|
||||
<span className="text-muted-foreground">({customerData.tradeName})</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
@ -133,12 +133,12 @@ export const CustomerViewPage = () => {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Registro Legal</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.legal_record}</dd>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.legalRecord}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Impuestos por Defecto</dt>
|
||||
<dd className="mt-1">
|
||||
{customerData?.default_taxes.map((tax) => (
|
||||
{customerData?.defaultTaxes.map((tax: string) => (
|
||||
<Badge key={tax} variant={"secondary"}>
|
||||
{tax}
|
||||
</Badge>
|
||||
@ -176,7 +176,7 @@ export const CustomerViewPage = () => {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Código Postal</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.postal_code}</dd>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.postalCode}</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@ -205,35 +205,35 @@ export const CustomerViewPage = () => {
|
||||
{/* Contacto Principal */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">Contacto Principal</h3>
|
||||
{customerData?.email_primary && (
|
||||
{customerData?.primaryEmail && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.email_primary}
|
||||
{customerData?.primaryEmail}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.mobile_primary && (
|
||||
{customerData?.primaryMobile && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.mobile_primary}
|
||||
{customerData?.primaryMobile}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.phone_primary && (
|
||||
{customerData?.primaryPhone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.phone_primary}
|
||||
{customerData?.primaryPhone}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
@ -243,35 +243,35 @@ export const CustomerViewPage = () => {
|
||||
{/* Contacto Secundario */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">Contacto Secundario</h3>
|
||||
{customerData?.email_secondary && (
|
||||
{customerData?.secondaryEmail && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.email_secondary}
|
||||
{customerData?.secondaryEmail}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.mobile_secondary && (
|
||||
{customerData?.secondaryMobile && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.mobile_secondary}
|
||||
{customerData?.secondaryMobile}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.phone_secondary && (
|
||||
{customerData?.secondaryPhone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.phone_secondary}
|
||||
{customerData?.secondaryPhone}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
@ -330,18 +330,14 @@ export const CustomerViewPage = () => {
|
||||
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Idioma Preferido</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.language_code}
|
||||
</dd>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.languageCode}</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Moneda Preferida</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.currency_code}
|
||||
</dd>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.currencyCode}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./sheet/index.ts";
|
||||
export * from "./use-device-info.ts";
|
||||
export * from "./use-row-selection.ts";
|
||||
|
||||
2
packages/rdx-ui/src/hooks/sheet/index.ts
Normal file
2
packages/rdx-ui/src/hooks/sheet/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./sheet-state-types.ts";
|
||||
export * from "./use-sheet-state.ts";
|
||||
28
packages/rdx-ui/src/hooks/sheet/sheet-state-types.ts
Normal file
28
packages/rdx-ui/src/hooks/sheet/sheet-state-types.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export type SheetMode = "view" | "edit" | "create";
|
||||
|
||||
export interface SheetStateOptions {
|
||||
defaultOpen?: boolean;
|
||||
defaultPinned?: boolean;
|
||||
defaultMode?: SheetMode;
|
||||
}
|
||||
|
||||
export interface SheetState {
|
||||
sheetIsOpen: boolean;
|
||||
sheetIsPinned: boolean;
|
||||
sheetIsModal: boolean;
|
||||
sheetMode: SheetMode;
|
||||
}
|
||||
|
||||
export interface SheetStateActions {
|
||||
openSheet: () => void;
|
||||
closeSheet: () => void;
|
||||
togglePinned: () => void;
|
||||
setOpen: (next: boolean) => void;
|
||||
setPinned: (next: boolean) => void;
|
||||
setMode: (next: SheetMode) => void;
|
||||
onOpenChange: (next: boolean) => void;
|
||||
openInMode: (nextMode: SheetMode) => void;
|
||||
resetSheet: () => void;
|
||||
}
|
||||
|
||||
export type SheetStateController = SheetState & SheetStateActions;
|
||||
56
packages/rdx-ui/src/hooks/sheet/use-sheet-state.ts
Normal file
56
packages/rdx-ui/src/hooks/sheet/use-sheet-state.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import type { SheetMode, SheetStateController, SheetStateOptions } from "./sheet-state-types.ts";
|
||||
|
||||
const DEFAULT_MODE: SheetMode = "view";
|
||||
|
||||
export const useSheetState = (options: SheetStateOptions = {}): SheetStateController => {
|
||||
const { defaultOpen = false, defaultPinned = false, defaultMode = DEFAULT_MODE } = options;
|
||||
|
||||
const [open, setOpen] = useState<boolean>(defaultOpen);
|
||||
const [pinned, setPinned] = useState<boolean>(defaultPinned);
|
||||
const [mode, setMode] = useState<SheetMode>(defaultMode);
|
||||
|
||||
const openSheet = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeSheet = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const togglePinned = useCallback(() => {
|
||||
setPinned((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const onOpenChange = useCallback((next: boolean) => {
|
||||
setOpen(next);
|
||||
}, []);
|
||||
|
||||
const openInMode = useCallback((nextMode: SheetMode) => {
|
||||
setMode(nextMode);
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const resetSheet = useCallback(() => {
|
||||
setOpen(defaultOpen);
|
||||
setPinned(defaultPinned);
|
||||
setMode(defaultMode);
|
||||
}, [defaultMode, defaultOpen, defaultPinned]);
|
||||
|
||||
return {
|
||||
sheetIsOpen: open,
|
||||
sheetIsPinned: pinned,
|
||||
sheetIsModal: !pinned,
|
||||
sheetMode: mode,
|
||||
openSheet,
|
||||
closeSheet,
|
||||
togglePinned,
|
||||
setOpen,
|
||||
setPinned,
|
||||
setMode,
|
||||
onOpenChange,
|
||||
openInMode,
|
||||
resetSheet,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user