Facturas de cliente
This commit is contained in:
parent
94be2d066f
commit
c9d375f40c
@ -3,4 +3,5 @@ export * from "./use-create-customer-invoice-mutation";
|
||||
export * from "./use-customer-invoices-query";
|
||||
export * from "./use-invoice-query";
|
||||
export * from "./use-items-table-navigation";
|
||||
export * from "./use-pinned-preview-sheet";
|
||||
export * from "./use-update-customer-invoice-mutation";
|
||||
|
||||
@ -0,0 +1,140 @@
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@repo/shadcn-ui/components";
|
||||
// hooks/use-pinned-preview-sheet.ts
|
||||
import * as React from "react";
|
||||
|
||||
type UsePinnedPreviewSheetOptions<T> = {
|
||||
/** Persiste el pin en localStorage (clave). Si omites, no persiste. */
|
||||
persistKey?: string;
|
||||
/** Anchura del panel anclado (Tailwind). */
|
||||
pinnedWidthClass?: string; // p.ej. "w-[420px]"
|
||||
/** Título del Sheet (no anclado). */
|
||||
title?: string | ((item: T | null) => string);
|
||||
};
|
||||
|
||||
export type PinnedPreviewSheetAPI<T> = {
|
||||
/** Estado */
|
||||
isOpen: boolean;
|
||||
isPinned: boolean;
|
||||
item: T | null;
|
||||
|
||||
/** Acciones */
|
||||
open: (item: T) => void;
|
||||
close: () => void;
|
||||
togglePin: () => void;
|
||||
setItem: (item: T | null) => void;
|
||||
|
||||
/** Renderizado: coloca este nodo cerca del listado (al final de la página/feature) */
|
||||
PreviewContainer: React.FC<{
|
||||
/** Render del cuerpo del preview */
|
||||
children: (item: T) => React.ReactNode;
|
||||
/** Cabecera opcional (si quieres sustituir el SheetHeader) */
|
||||
header?: React.ReactNode;
|
||||
}>;
|
||||
};
|
||||
|
||||
export function usePinnedPreviewSheet<T = unknown>(
|
||||
opts?: UsePinnedPreviewSheetOptions<T>
|
||||
): PinnedPreviewSheetAPI<T> {
|
||||
const { persistKey, pinnedWidthClass = "w-[420px]", title } = opts ?? {};
|
||||
const [isOpen, setOpen] = React.useState(false);
|
||||
const [item, setItem] = React.useState<T | null>(null);
|
||||
const [isPinned, setPinned] = React.useState<boolean>(() => {
|
||||
if (!persistKey) return false;
|
||||
try {
|
||||
return localStorage.getItem(persistKey) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const open = React.useCallback((next: T) => {
|
||||
setItem(next);
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
if (isPinned) return; // Anclado: no cerrar
|
||||
setOpen(false);
|
||||
}, [isPinned]);
|
||||
|
||||
const togglePin = React.useCallback(() => {
|
||||
setPinned((p) => {
|
||||
const next = !p;
|
||||
if (persistKey) {
|
||||
try {
|
||||
localStorage.setItem(persistKey, next ? "1" : "0");
|
||||
} catch { }
|
||||
}
|
||||
// Si se fija el pin y hay item, abre “estático”; si se desancla, mostramos Sheet si había abierto
|
||||
if (!next && item) setOpen(true);
|
||||
return next;
|
||||
});
|
||||
}, [persistKey, item]);
|
||||
|
||||
const HeaderTitle = React.useMemo(() => {
|
||||
if (!item) return "";
|
||||
if (typeof title === "function") return title(item);
|
||||
return title ?? "";
|
||||
}, [item, title]);
|
||||
|
||||
const PreviewContainer: PinnedPreviewSheetAPI<T>["PreviewContainer"] = React.useCallback(
|
||||
({ children, header }) => {
|
||||
if (!item) return null;
|
||||
|
||||
// Modo anclado: aside fijo sin overlay, no bloquea scroll, accesible.
|
||||
if (isPinned) {
|
||||
return (
|
||||
<aside
|
||||
aria-label="Vista previa anclada"
|
||||
className={`fixed inset-y-0 right-0 ${pinnedWidthClass} bg-background border-l z-30 flex flex-col`}
|
||||
>
|
||||
<div className="flex items-center justify-between p-3 border-b">
|
||||
{header ?? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{HeaderTitle}</span>
|
||||
<span className="text-xs text-muted-foreground">(anclado)</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePin}
|
||||
className="text-xs underline"
|
||||
aria-label="Desanclar panel"
|
||||
>
|
||||
Desanclar
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-auto p-3">{children(item)}</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
// Modo Sheet (deslizable desde la derecha)
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
||||
<SheetContent side="right" className={pinnedWidthClass}>
|
||||
{header ?? (
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-base">{HeaderTitle}</SheetTitle>
|
||||
</SheetHeader>
|
||||
)}
|
||||
<div className="mt-3">{children(item)}</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePin}
|
||||
className="text-xs underline"
|
||||
aria-label="Anclar panel"
|
||||
>
|
||||
Anclar
|
||||
</button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
},
|
||||
[HeaderTitle, close, isOpen, isPinned, item, pinnedWidthClass, togglePin]
|
||||
);
|
||||
|
||||
return { isOpen, isPinned, item, open, close, togglePin, setItem, PreviewContainer };
|
||||
}
|
||||
@ -0,0 +1,349 @@
|
||||
import { Badge, Button, Card, CardContent, Separator } from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Calendar,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Download,
|
||||
Edit,
|
||||
FileText,
|
||||
Hash,
|
||||
Mail,
|
||||
MapPin,
|
||||
Pin,
|
||||
Receipt,
|
||||
Trash2,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { InvoiceSummaryFormData } from '../../schemas';
|
||||
|
||||
export type InvoicePreviewCardProps = {
|
||||
invoice: InvoiceSummaryFormData
|
||||
isOpen: boolean
|
||||
isPinned: boolean
|
||||
onClose: () => void
|
||||
onTogglePin: () => void
|
||||
}
|
||||
|
||||
|
||||
export const InvoicePreviewCard = ({
|
||||
invoice,
|
||||
isOpen,
|
||||
isPinned,
|
||||
onClose,
|
||||
onTogglePin
|
||||
}: InvoicePreviewCardProps) => {
|
||||
|
||||
return <>
|
||||
{/* Overlay - only show when not pinned */}
|
||||
{isOpen && !isPinned && (
|
||||
<div className='fixed inset-0 bg-black/20 z-40 transition-opacity' onClick={onClose} />
|
||||
)}
|
||||
|
||||
{/* Sheet */}
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-full bg-white shadow-2xl z-50 transition-transform duration-300 ease-in-out ${isOpen ? "translate-x-0" : "translate-x-full"
|
||||
} ${isPinned ? "w-[500px]" : "w-[600px]"}`}
|
||||
>
|
||||
{/* Header with gradient */}
|
||||
<div className='bg-gradient-to-r from-blue-600 to-violet-600 p-6 text-white'>
|
||||
<div className='flex items-start justify-between mb-4'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold mb-1'>
|
||||
{invoice.is_proforma ? "Proforma" : "Factura"} {invoice.invoice_number}
|
||||
</h2>
|
||||
<p className='text-blue-100 text-sm'>
|
||||
Serie: {invoice.series} • Ref: {invoice.reference}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onClick={onTogglePin}
|
||||
className={`text-white hover:bg-white/20 ${isPinned ? "bg-white/30" : ""}`}
|
||||
title={isPinned ? "Desanclar" : "Anclar"}
|
||||
>
|
||||
<Pin className={`h-4 w-4 ${isPinned ? "fill-current" : ""}`} />
|
||||
</Button>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onClick={onClose}
|
||||
className='text-white hover:bg-white/20'
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<Badge variant='outline' className='bg-white/20 text-white border-white/30'>
|
||||
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className='p-6 overflow-y-auto h-[calc(100%-180px)]'>
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center gap-2 mb-3'>
|
||||
<User className='h-5 w-5 text-blue-600' />
|
||||
<h3 className='font-semibold text-gray-900'>Cliente</h3>
|
||||
</div>
|
||||
<div className='bg-gradient-to-r from-blue-50 to-violet-50 p-4 rounded-lg space-y-2'>
|
||||
<p className='text-gray-700 font-semibold text-lg'>{invoice.recipient.name}</p>
|
||||
<div className='flex items-start gap-2 text-sm'>
|
||||
<Hash className='h-4 w-4 text-gray-500 mt-0.5' />
|
||||
<div>
|
||||
<span className='text-gray-500'>TIN:</span>
|
||||
<span className='ml-2 text-gray-700 font-medium'>{invoice.recipient.tin}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 text-sm'>
|
||||
<MapPin className='h-4 w-4 text-gray-500 mt-0.5' />
|
||||
<div className='text-gray-600'>
|
||||
<div>{invoice.recipient.street}</div>
|
||||
{invoice.recipient.street2 && <div>{invoice.recipient.street2}</div>}
|
||||
<div>
|
||||
{invoice.recipient.postal_code} {invoice.recipient.city}, {invoice.recipient.province}
|
||||
</div>
|
||||
<div>{invoice.recipient.country}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className='my-6' />
|
||||
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center gap-2 mb-3'>
|
||||
<Calendar className='h-5 w-5 text-blue-600' />
|
||||
<h3 className='font-semibold text-gray-900'>Fechas</h3>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='bg-blue-50 p-3 rounded-lg'>
|
||||
<span className='text-xs font-medium text-gray-600 block mb-1'>Fecha factura</span>
|
||||
<p className='text-gray-900 font-semibold'>{invoice.invoice_date}</p>
|
||||
</div>
|
||||
<div className='bg-violet-50 p-3 rounded-lg'>
|
||||
<span className='text-xs font-medium text-gray-600 block mb-1'>
|
||||
Fecha operación
|
||||
</span>
|
||||
<p className='text-gray-900 font-semibold'>{invoice.operation_date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className='my-6' />
|
||||
|
||||
{invoice.description && (
|
||||
<>
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center gap-2 mb-3'>
|
||||
<FileText className='h-5 w-5 text-blue-600' />
|
||||
<h3 className='font-semibold text-gray-900'>Descripción</h3>
|
||||
</div>
|
||||
<p className='text-gray-600 text-sm bg-gray-50 p-3 rounded-lg'>
|
||||
{invoice.description}
|
||||
</p>
|
||||
</div>
|
||||
<Separator className='my-6' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{invoice.items && invoice.items.length > 0 && (
|
||||
<>
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center gap-2 mb-3'>
|
||||
<Receipt className='h-5 w-5 text-blue-600' />
|
||||
<h3 className='font-semibold text-gray-900'>Conceptos</h3>
|
||||
</div>
|
||||
<div className='space-y-3'>
|
||||
{invoice.items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-gradient-to-r from-blue-50 to-violet-50 p-4 rounded-lg'
|
||||
>
|
||||
<div className='flex justify-between items-start mb-2'>
|
||||
<div className='flex-1'>
|
||||
<span className='font-semibold text-gray-900 block'>{item.concepto}</span>
|
||||
<span className='text-sm text-gray-600'>{item.descripcion}</span>
|
||||
</div>
|
||||
<span className='font-bold text-gray-900 ml-4'>
|
||||
{formatCurrency(item.total)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center justify-between text-sm text-gray-600 mt-2'>
|
||||
<div>
|
||||
{item.cantidad} × {formatCurrency(item.precio)}
|
||||
</div>
|
||||
{item.descuento > 0 && (
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='bg-amber-100 text-amber-700 border-amber-300 text-xs'
|
||||
>
|
||||
-{item.descuento}% dto.
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{item.impuestos && item.impuestos.length > 0 && (
|
||||
<div className='flex gap-1 mt-2'>
|
||||
{item.impuestos.map((tax, taxIndex) => (
|
||||
<Badge
|
||||
key={taxIndex}
|
||||
variant='outline'
|
||||
className='bg-blue-100 text-blue-700 border-blue-300 text-xs'
|
||||
>
|
||||
{tax}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator className='my-6' />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center gap-2 mb-3'>
|
||||
<CreditCard className='h-5 w-5 text-blue-600' />
|
||||
<h3 className='font-semibold text-gray-900'>Resumen Financiero</h3>
|
||||
</div>
|
||||
<div className='bg-gradient-to-br from-blue-50 to-violet-50 p-4 rounded-lg space-y-3'>
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Subtotal</span>
|
||||
<span className='text-gray-900 font-medium'>
|
||||
{invoice.subtotal_amount_fmt}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{invoice.discount_amount > 0 && (
|
||||
<>
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>
|
||||
Descuento ({invoice.discount_percentage}%)
|
||||
</span>
|
||||
<span className='text-red-600 font-medium'>
|
||||
-{invoice.discount_amount_fmt}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Base imponible</span>
|
||||
<span className='text-gray-900 font-medium'>
|
||||
{invoice.taxable_amount_fmt}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/*invoice.taxes && invoice.taxes.length > 0 && (
|
||||
<div className='space-y-2'>
|
||||
{invoice.taxes.map((tax, index) => (
|
||||
<div key={index} className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>
|
||||
{tax.name} {tax.rate}%
|
||||
</span>
|
||||
<span className='text-gray-900 font-medium'>
|
||||
{invoice.taxable_amount_fmt}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)*/}
|
||||
|
||||
<div className='flex justify-between text-sm'>
|
||||
<span className='text-gray-600'>Total impuestos</span>
|
||||
<span className='text-gray-900 font-medium'>
|
||||
{invoice.taxes_amount_fmt}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className='flex justify-between items-center pt-2'>
|
||||
<span className='font-bold text-gray-900 text-lg'>Total</span>
|
||||
<span className='text-3xl font-bold bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent'>
|
||||
{invoice.total_amount_fmt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Footer */}
|
||||
<div className='absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-r from-blue-50 to-violet-50 border-t border-gray-200'>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='border-blue-200 text-blue-600 hover:bg-blue-50 bg-transparent'
|
||||
>
|
||||
<Edit className='mr-2 h-4 w-4' />
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='border-violet-200 text-violet-600 hover:bg-violet-50 bg-transparent'
|
||||
>
|
||||
<Copy className='mr-2 h-4 w-4' />
|
||||
Duplicar
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='border-blue-200 text-blue-600 hover:bg-blue-50 bg-transparent'
|
||||
>
|
||||
<Download className='mr-2 h-4 w-4' />
|
||||
Descargar
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='border-violet-200 text-violet-600 hover:bg-violet-50 bg-transparent'
|
||||
>
|
||||
<Mail className='mr-2 h-4 w-4' />
|
||||
Enviar
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full mt-3 border-red-200 text-red-600 hover:bg-red-50 bg-transparent'
|
||||
>
|
||||
<Trash2 className='mr-2 h-4 w-4' />
|
||||
Eliminar factura
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<Card className="shadow-none border-0">
|
||||
<CardContent className="p-0 space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Nº</span>
|
||||
<span className="font-medium tabular-nums">{invoice.invoice_number}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Cliente</span>
|
||||
<span className="font-medium truncate max-w-[220px]" title={invoice.recipient?.name}>
|
||||
{invoice.recipient?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Fecha</span>
|
||||
<span className="tabular-nums">{invoice.invoice_date}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Importe</span>
|
||||
<span className="font-semibold tabular-nums">{invoice.total_amount?.formatted}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Estado</span>
|
||||
<span className="font-medium">{invoice.status}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -21,6 +21,8 @@ export type InvoiceUpdateCompProps = {
|
||||
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
|
||||
onRowClick?: (row: InvoiceSummaryFormData, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
|
||||
}
|
||||
|
||||
|
||||
@ -33,11 +35,11 @@ export const InvoicesListGrid = ({
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
searchValue, onSearchChange,
|
||||
onRowClick
|
||||
}: InvoiceUpdateCompProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("todas");
|
||||
|
||||
const columns = useInvoicesListColumns();
|
||||
@ -183,7 +185,7 @@ export const InvoicesListGrid = ({
|
||||
totalItems={total_items}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,12 +2,14 @@ import { ErrorAlert } from '@erp/customers/components';
|
||||
import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { FilePenIcon, PlusIcon } from "lucide-react";
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { InvoicesListGrid, PageHeader } from '../../components';
|
||||
import { useInvoicesQuery } from '../../hooks';
|
||||
import { useInvoicesQuery, usePinnedPreviewSheet } from '../../hooks';
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { InvoiceSummaryFormData } from '../../schemas';
|
||||
import { invoiceResumeDtoToFormAdapter } from '../../schemas/invoice-resume-dto.adapter';
|
||||
import { InvoicePreviewCard } from './invoice-preview-card';
|
||||
|
||||
export const InvoiceListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -45,6 +47,13 @@ export const InvoiceListPage = () => {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
||||
const invoicePreview = usePinnedPreviewSheet<InvoiceSummaryFormData>({
|
||||
persistKey: "invoice-preview-pin",
|
||||
pinnedWidthClass: "w-[440px]",
|
||||
title: (inv) => (inv ? `Factura ${inv.invoice_number}` : "Proforma"),
|
||||
});
|
||||
|
||||
const handlePageChange = (newPageIndex: number) => {
|
||||
// TanStack usa pageIndex 0-based → API usa 0-based también
|
||||
setPageIndex(newPageIndex);
|
||||
@ -62,6 +71,18 @@ export const InvoiceListPage = () => {
|
||||
setPageIndex(0);
|
||||
};
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(invoice: InvoiceSummaryFormData, _index: number, e: React.MouseEvent<HTMLTableRowElement>) => {
|
||||
const url = `/customer-invoices/${invoice.id}/edit`;
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
invoicePreview.open(invoice); // <-- abre o actualiza el preview
|
||||
},
|
||||
[invoicePreview]
|
||||
);
|
||||
|
||||
if (isError || !invoicesPageData) {
|
||||
return (
|
||||
<AppContent>
|
||||
@ -105,16 +126,30 @@ export const InvoiceListPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full h-full py-3'>
|
||||
<InvoicesListGrid
|
||||
invoicesPage={invoicesPageData}
|
||||
loading={isLoading}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
searchValue={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
/>
|
||||
<div className={invoicePreview.isPinned ? "flex-1 mr-[440px]" : "flex-1"}>
|
||||
<InvoicesListGrid
|
||||
invoicesPage={invoicesPageData}
|
||||
loading={isLoading}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
searchValue={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Contenedor del preview (Sheet o aside anclado) */}
|
||||
<invoicePreview.PreviewContainer>
|
||||
{(invoice) => <InvoicePreviewCard invoice={invoice}
|
||||
isOpen={invoicePreview.isOpen}
|
||||
isPinned={invoicePreview.isPinned}
|
||||
onClose={invoicePreview.close}
|
||||
onTogglePin={invoicePreview.togglePin}
|
||||
/>}
|
||||
</invoicePreview.PreviewContainer>
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
|
||||
@ -233,6 +233,7 @@ export function DataTable<TData, TValue>({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any);
|
||||
}}
|
||||
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
|
||||
onDoubleClick={
|
||||
!readOnly && !onRowClick ? () => setEditIndex(rowIndex) : undefined
|
||||
} >
|
||||
|
||||
Loading…
Reference in New Issue
Block a user