Refactor Proforma Detail Page: Modularize Components and Improve Structure
- Simplified the ProformaDetailPage by extracting various components into separate files for better maintainability and readability. - Introduced ProformaDetailHeaderActions for managing header actions. - Created ProformaDetailPreviewCard for displaying the PDF viewer. - Added ProformaDetailSidebar to encapsulate billing and date information. - Implemented ProformaDetailSummaryCard to summarize billing, dates, and notes. - Enhanced ProformaDetailInspectorCard for consistent card layout across different sections. - Updated formatting functions for date and money to improve code reuse. - Removed redundant code and improved overall structure for better clarity.
This commit is contained in:
parent
1506a78582
commit
f0b100995a
@ -5,15 +5,6 @@ type EmptyCellValueProps = {
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export const EmptyCellValue = ({
|
||||
className,
|
||||
value = "\u2014",
|
||||
}: EmptyCellValueProps) => (
|
||||
<span
|
||||
aria-label="Sin valor"
|
||||
className={cn("inline-flex items-center text-muted-foreground", className)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
export const EmptyCellValue = ({ className, value = "\u2014" }: EmptyCellValueProps) => (
|
||||
<span className={cn("inline-flex items-center text-muted-foreground", className)}>{value}</span>
|
||||
);
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./proforma-detail-header-actions";
|
||||
export * from "./proforma-detail-preview-card";
|
||||
export * from "./proforma-detail-sidebar";
|
||||
export * from "./proforma-detail-summary-card";
|
||||
@ -0,0 +1,57 @@
|
||||
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@repo/shadcn-ui/components";
|
||||
import { DownloadIcon, MoreHorizontalIcon, PencilIcon, RefreshCwIcon } from "lucide-react";
|
||||
|
||||
type ProformaDetailHeaderActionsProps = {
|
||||
primaryAction: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
isPrimaryLoading: boolean;
|
||||
isArchived: boolean;
|
||||
onDownload: () => void;
|
||||
onDuplicate: () => void;
|
||||
onArchiveToggle: () => void;
|
||||
};
|
||||
|
||||
export const ProformaDetailHeaderActions = ({
|
||||
primaryAction,
|
||||
isPrimaryLoading,
|
||||
isArchived,
|
||||
onDownload,
|
||||
onDuplicate,
|
||||
onArchiveToggle,
|
||||
}: ProformaDetailHeaderActionsProps) => {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
<Button disabled={isPrimaryLoading} onClick={primaryAction.onClick} type="button">
|
||||
{isPrimaryLoading ? (
|
||||
<RefreshCwIcon className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
<PencilIcon className="mr-2 size-4" />
|
||||
)}
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onDownload} type="button" variant="outline">
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
Descargar PDF
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button size="icon" type="button" variant="outline">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onClick={onDuplicate}>Duplicar proforma</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onArchiveToggle}>
|
||||
{isArchived ? "Desarchivar" : "Archivar"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { PDFViewer } from "@repo/rdx-ui/components";
|
||||
import { Card, CardContent } from "@repo/shadcn-ui/components";
|
||||
|
||||
type ProformaDetailPreviewCardProps = {
|
||||
src: string | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
onOpen: () => void;
|
||||
onDownload: () => void;
|
||||
onPrint: () => void;
|
||||
};
|
||||
|
||||
export const ProformaDetailPreviewCard = ({
|
||||
src,
|
||||
isLoading,
|
||||
error,
|
||||
onOpen,
|
||||
onDownload,
|
||||
onPrint,
|
||||
}: ProformaDetailPreviewCardProps) => {
|
||||
return (
|
||||
<Card className="overflow-hidden border-border/80 shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<PDFViewer
|
||||
className="h-[calc(100vh-11.5rem)] min-h-[720px]"
|
||||
defaultZoom="fit"
|
||||
description="Vista previa del PDF real generado para esta proforma."
|
||||
error={error}
|
||||
loading={isLoading}
|
||||
onDownload={onDownload}
|
||||
onOpen={onOpen}
|
||||
onPrint={onPrint}
|
||||
src={src}
|
||||
title="Vista previa"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import type { Proforma } from "../../../shared";
|
||||
import { ProformaDetailStatusInspectorCard } from "../components";
|
||||
|
||||
import { ProformaDetailSummaryCard } from "./proforma-detail-summary-card";
|
||||
|
||||
type ProformaDetailSidebarProps = {
|
||||
proforma: Proforma;
|
||||
paymentMethodLabel: string | null;
|
||||
paymentTermLabel: string | null;
|
||||
onEdit: () => void;
|
||||
};
|
||||
|
||||
export const ProformaDetailSidebar = ({
|
||||
proforma,
|
||||
paymentMethodLabel,
|
||||
paymentTermLabel,
|
||||
}: ProformaDetailSidebarProps) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ProformaDetailStatusInspectorCard proforma={proforma} />
|
||||
<ProformaDetailSummaryCard
|
||||
paymentMethodLabel={paymentMethodLabel}
|
||||
paymentTermLabel={paymentTermLabel}
|
||||
proforma={proforma}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,159 @@
|
||||
import { FieldValueList, FormSectionCard, FormSectionGrid } from "@repo/rdx-ui/components";
|
||||
import { Separator } from "@repo/shadcn-ui/components";
|
||||
import { LandmarkIcon } from "lucide-react";
|
||||
|
||||
import { type Proforma } from "../../../shared";
|
||||
import { formatProformaDate, formatProformaMoney } from "../components/proforma-detail-formatters";
|
||||
|
||||
type ProformaDetailSummaryCardProps = {
|
||||
proforma: Proforma;
|
||||
paymentMethodLabel: string | null;
|
||||
paymentTermLabel: string | null;
|
||||
};
|
||||
|
||||
const SectionHeading = ({ title, description }: { title: string; description?: string }) => {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold tracking-tight text-foreground">{title}</h3>
|
||||
{description ? <p className="text-xs leading-5 text-muted-foreground">{description}</p> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProformaDetailSummaryCard = ({
|
||||
proforma,
|
||||
paymentMethodLabel,
|
||||
paymentTermLabel,
|
||||
}: ProformaDetailSummaryCardProps) => {
|
||||
const billingItems = [
|
||||
{
|
||||
label: "Método de pago",
|
||||
value: paymentMethodLabel ?? "\u2014",
|
||||
},
|
||||
{
|
||||
label: "Plazo",
|
||||
value: paymentTermLabel ?? "\u2014",
|
||||
},
|
||||
{
|
||||
label: "Moneda",
|
||||
value: proforma.currencyCode,
|
||||
},
|
||||
];
|
||||
|
||||
const datesItems = [
|
||||
{
|
||||
label: "Fecha proforma",
|
||||
value: formatProformaDate(proforma.proformaDate),
|
||||
},
|
||||
{
|
||||
label: "Fecha operación",
|
||||
value: formatProformaDate(proforma.operationDate),
|
||||
},
|
||||
{
|
||||
label: "Serie",
|
||||
value: proforma.series ?? "\u2014",
|
||||
},
|
||||
{
|
||||
label: "Referencia",
|
||||
value: proforma.reference ?? "\u2014",
|
||||
},
|
||||
];
|
||||
|
||||
const totalsItems = [
|
||||
{
|
||||
label: "Subtotal",
|
||||
value: formatProformaMoney(proforma.subtotalAmount, proforma.currencyCode, proforma.languageCode),
|
||||
},
|
||||
{
|
||||
label: "Descuentos",
|
||||
value: `-${formatProformaMoney(proforma.totalDiscountAmount, proforma.currencyCode, proforma.languageCode)}`,
|
||||
valueClassName: "text-red-600",
|
||||
},
|
||||
{
|
||||
label: "IVA",
|
||||
value: formatProformaMoney(proforma.ivaAmount, proforma.currencyCode, proforma.languageCode),
|
||||
},
|
||||
{
|
||||
label: "Total",
|
||||
value: formatProformaMoney(proforma.totalAmount, proforma.currencyCode, proforma.languageCode),
|
||||
valueClassName: "text-lg font-semibold text-foreground",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<FormSectionCard
|
||||
className="shadow-sm"
|
||||
contentClassName="space-y-5"
|
||||
description="Resumen operativo de la proforma con las condiciones clave y sus importes."
|
||||
icon={<LandmarkIcon className="size-5" />}
|
||||
title="Resumen de proforma"
|
||||
>
|
||||
<FormSectionGrid className="gap-y-6 md:grid-cols-1">
|
||||
<div className="space-y-3 md:col-span-12">
|
||||
<SectionHeading
|
||||
description="Condiciones de cobro configuradas actualmente para esta proforma."
|
||||
title="Cobros"
|
||||
/>
|
||||
<FieldValueList
|
||||
className="rounded-lg border bg-muted/20 p-4"
|
||||
items={billingItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-12">
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:col-span-12">
|
||||
<SectionHeading
|
||||
description="Fechas principales y referencias internas del documento."
|
||||
title="Fechas y referencias"
|
||||
/>
|
||||
<FieldValueList
|
||||
className="rounded-lg border bg-muted/20 p-4"
|
||||
items={datesItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-12">
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:col-span-12">
|
||||
<SectionHeading
|
||||
description="Texto libre que acompaña a la proforma y puede trasladarse al PDF."
|
||||
title="Notas"
|
||||
/>
|
||||
<div className="rounded-lg border bg-muted/20 p-4 text-sm leading-6 text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">Descripción</p>
|
||||
<p>{proforma.description ?? "Sin descripción."}</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">Observaciones</p>
|
||||
<p>{proforma.notes ?? "Sin notas adicionales."}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-12">
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:col-span-12">
|
||||
<SectionHeading
|
||||
description="Desglose económico principal del documento."
|
||||
title="Resumen económico"
|
||||
/>
|
||||
<FieldValueList
|
||||
className="rounded-lg border bg-muted/20 p-4"
|
||||
itemClassName="items-center"
|
||||
items={totalsItems}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
</div>
|
||||
</FormSectionGrid>
|
||||
</FormSectionCard>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
export * from "./proforma-detail-archived-badge";
|
||||
export * from "./proforma-detail-billing-inspector-card";
|
||||
export * from "./proforma-detail-dates-inspector-card";
|
||||
export * from "./proforma-detail-header-meta";
|
||||
export * from "./proforma-detail-inspector-card";
|
||||
export * from "./proforma-detail-notes-inspector-card";
|
||||
export * from "./proforma-detail-skeleton";
|
||||
export * from "./proforma-detail-status-inspector-card";
|
||||
export * from "./proforma-detail-totals-inspector-card";
|
||||
@ -0,0 +1,33 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ArchiveIcon } from "lucide-react";
|
||||
|
||||
type ProformaDetailArchivedBadgeProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ProformaDetailArchivedBadge = ({
|
||||
className,
|
||||
}: ProformaDetailArchivedBadgeProps) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full border border-slate-300 bg-slate-100/90 px-2.5 py-1 text-xs font-medium leading-none whitespace-nowrap text-slate-700 ring-1 ring-inset ring-slate-300/90",
|
||||
"[&>svg]:pointer-events-none [&>svg]:size-3.5 [&>svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ArchiveIcon aria-hidden="true" />
|
||||
Archivada
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>Esta proforma está archivada y fuera del flujo principal.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { FieldValueList } from "@repo/rdx-ui/components";
|
||||
|
||||
import type { Proforma } from "../../../shared";
|
||||
|
||||
import { ProformaDetailInspectorCard } from "./proforma-detail-inspector-card";
|
||||
|
||||
type ProformaDetailBillingInspectorCardProps = {
|
||||
proforma: Proforma;
|
||||
paymentMethodLabel: string | null;
|
||||
paymentTermLabel: string | null;
|
||||
onEdit: () => void;
|
||||
};
|
||||
|
||||
export const ProformaDetailBillingInspectorCard = ({
|
||||
proforma,
|
||||
paymentMethodLabel,
|
||||
paymentTermLabel,
|
||||
onEdit,
|
||||
}: ProformaDetailBillingInspectorCardProps) => {
|
||||
const items = [
|
||||
{
|
||||
label: "Método de pago",
|
||||
value: paymentMethodLabel ?? "\u2014",
|
||||
},
|
||||
{
|
||||
label: "Plazo",
|
||||
value: paymentTermLabel ?? "\u2014",
|
||||
},
|
||||
{
|
||||
label: "Moneda",
|
||||
value: proforma.currencyCode,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProformaDetailInspectorCard onEdit={onEdit} title="Cobro">
|
||||
<FieldValueList items={items} />
|
||||
</ProformaDetailInspectorCard>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import { FieldValueList } from "@repo/rdx-ui/components";
|
||||
|
||||
import type { Proforma } from "../../../shared";
|
||||
|
||||
import { formatProformaDate } from "./proforma-detail-formatters";
|
||||
import { ProformaDetailInspectorCard } from "./proforma-detail-inspector-card";
|
||||
|
||||
type ProformaDetailDatesInspectorCardProps = {
|
||||
proforma: Proforma;
|
||||
onEdit: () => void;
|
||||
};
|
||||
|
||||
export const ProformaDetailDatesInspectorCard = ({
|
||||
proforma,
|
||||
onEdit,
|
||||
}: ProformaDetailDatesInspectorCardProps) => {
|
||||
const items = [
|
||||
{
|
||||
label: "Fecha proforma",
|
||||
value: formatProformaDate(proforma.proformaDate),
|
||||
},
|
||||
{
|
||||
label: "Fecha operación",
|
||||
value: formatProformaDate(proforma.operationDate),
|
||||
},
|
||||
{
|
||||
label: "Serie de la factura",
|
||||
value: proforma.series ?? "\u2014",
|
||||
},
|
||||
{
|
||||
label: "Referencia interna",
|
||||
value: proforma.reference ?? "\u2014",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProformaDetailInspectorCard onEdit={onEdit} title="Fechas y referencia">
|
||||
<FieldValueList items={items} />
|
||||
</ProformaDetailInspectorCard>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { DateHelper, MoneyHelper } from "@repo/rdx-utils";
|
||||
|
||||
export const formatProformaDate = (value: string | null | undefined) => {
|
||||
if (!value) {
|
||||
return "\u2014";
|
||||
}
|
||||
|
||||
return DateHelper.format(value);
|
||||
};
|
||||
|
||||
export const formatProformaMoney = (
|
||||
amount: number,
|
||||
currencyCode: string,
|
||||
languageCode?: string | null
|
||||
) => {
|
||||
return MoneyHelper.formatCurrency(amount, 2, currencyCode, languageCode ?? undefined);
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { Proforma } from "../../../shared";
|
||||
|
||||
import { formatProformaDate, formatProformaMoney } from "./proforma-detail-formatters";
|
||||
|
||||
type ProformaDetailHeaderMetaProps = {
|
||||
proforma: Proforma;
|
||||
};
|
||||
|
||||
export const ProformaDetailHeaderMeta = ({ proforma }: ProformaDetailHeaderMetaProps) => {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
proforma.recipient.name ?? "Cliente sin nombre",
|
||||
formatProformaDate(proforma.proformaDate),
|
||||
`Total ${formatProformaMoney(proforma.totalAmount, proforma.currencyCode, proforma.languageCode)}`,
|
||||
],
|
||||
[proforma]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted-foreground">
|
||||
{items.map((item, index) => (
|
||||
<div className="flex items-center gap-3" key={`${item}-${index}`}>
|
||||
{index > 0 ? <span className="hidden text-border sm:inline">|</span> : null}
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||
import { SquarePenIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type ProformaDetailInspectorCardProps = {
|
||||
title: string;
|
||||
onEdit?: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const ProformaDetailInspectorCard = ({
|
||||
title,
|
||||
onEdit,
|
||||
children,
|
||||
}: ProformaDetailInspectorCardProps) => {
|
||||
return (
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-xl">{title}</CardTitle>
|
||||
{onEdit ? (
|
||||
<Button onClick={onEdit} size="icon-sm" type="button" variant="ghost">
|
||||
<SquarePenIcon className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { Separator } from "@repo/shadcn-ui/components";
|
||||
|
||||
import type { Proforma } from "../../../shared";
|
||||
|
||||
import { ProformaDetailInspectorCard } from "./proforma-detail-inspector-card";
|
||||
|
||||
type ProformaDetailNotesInspectorCardProps = {
|
||||
proforma: Proforma;
|
||||
onEdit: () => void;
|
||||
};
|
||||
|
||||
export const ProformaDetailNotesInspectorCard = ({
|
||||
proforma,
|
||||
onEdit,
|
||||
}: ProformaDetailNotesInspectorCardProps) => {
|
||||
return (
|
||||
<ProformaDetailInspectorCard onEdit={onEdit} title="Notas">
|
||||
<div className="space-y-4 text-sm leading-6 text-muted-foreground">
|
||||
<div>
|
||||
<p className="mb-1 font-medium text-foreground">Descripción</p>
|
||||
<p>{proforma.description ?? "Sin descripción."}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<p className="mb-1 font-medium text-foreground">Observaciones</p>
|
||||
<p>{proforma.notes ?? "Sin notas adicionales."}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ProformaDetailInspectorCard>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { Card, CardContent, Skeleton } from "@repo/shadcn-ui/components";
|
||||
|
||||
export const ProformaDetailSkeleton = () => {
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<Skeleton className="h-5 w-40" />
|
||||
<Skeleton className="h-10 w-96" />
|
||||
<Skeleton className="h-5 w-80" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.8fr)_360px]">
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-[840px] w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-28 w-full rounded-xl" />
|
||||
<Skeleton className="h-40 w-full rounded-xl" />
|
||||
<Skeleton className="h-44 w-full rounded-xl" />
|
||||
<Skeleton className="h-48 w-full rounded-xl" />
|
||||
<Skeleton className="h-36 w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,76 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { CheckCircle2Icon } from "lucide-react";
|
||||
|
||||
import type { Proforma } from "../../../shared";
|
||||
|
||||
const getStatusTone = (status: Proforma["status"]) => {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return "border-emerald-200 bg-emerald-50 text-emerald-900";
|
||||
case "sent":
|
||||
return "border-amber-200 bg-amber-50 text-amber-900";
|
||||
case "issued":
|
||||
return "border-sky-200 bg-sky-50 text-sky-900";
|
||||
case "rejected":
|
||||
return "border-red-200 bg-red-50 text-red-900";
|
||||
default:
|
||||
return "border-slate-200 bg-slate-50 text-slate-900";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusSummary = (proforma: Proforma) => {
|
||||
if (proforma.isArchived) {
|
||||
return {
|
||||
title: "Proforma archivada",
|
||||
description: "Está fuera del flujo principal y puedes recuperarla cuando lo necesites.",
|
||||
};
|
||||
}
|
||||
|
||||
switch (proforma.status) {
|
||||
case "approved":
|
||||
return {
|
||||
title: "Lista para convertirse en factura",
|
||||
description: "La proforma ya está aprobada por el cliente y puede emitirse como factura.",
|
||||
};
|
||||
case "sent":
|
||||
return {
|
||||
title: "Pendiente de respuesta del cliente",
|
||||
description:
|
||||
"Se ha enviado al cliente y está a la espera de aceptación o rechazo por parte del cliente.",
|
||||
};
|
||||
case "issued":
|
||||
return {
|
||||
title: "Convertida en factura",
|
||||
description: "La proforma ya ha sido emitida y forma parte del flujo fiscal.",
|
||||
};
|
||||
case "rejected":
|
||||
return {
|
||||
title: "Requiere revisión",
|
||||
description: "El cliente la ha rechazado y conviene revisar condiciones o importes.",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Pendiente de revisión",
|
||||
description: "Sigue en borrador y todavía puede modificarse antes de enviarla al cliente.",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type ProformaDetailStatusInspectorCardProps = {
|
||||
proforma: Proforma;
|
||||
};
|
||||
|
||||
export const ProformaDetailStatusInspectorCard = ({
|
||||
proforma,
|
||||
}: ProformaDetailStatusInspectorCardProps) => {
|
||||
const summary = getStatusSummary(proforma);
|
||||
|
||||
return (
|
||||
<Alert className={cn("shadow-sm", getStatusTone(proforma.status))}>
|
||||
<CheckCircle2Icon className="size-4" />
|
||||
<AlertTitle>{summary.title}</AlertTitle>
|
||||
<AlertDescription>{summary.description}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { FieldValueList } from "@repo/rdx-ui/components";
|
||||
|
||||
import type { Proforma } from "../../../shared";
|
||||
|
||||
import { formatProformaMoney } from "./proforma-detail-formatters";
|
||||
import { ProformaDetailInspectorCard } from "./proforma-detail-inspector-card";
|
||||
|
||||
type ProformaDetailTotalsInspectorCardProps = {
|
||||
proforma: Proforma;
|
||||
};
|
||||
|
||||
export const ProformaDetailTotalsInspectorCard = ({
|
||||
proforma,
|
||||
}: ProformaDetailTotalsInspectorCardProps) => {
|
||||
const items = [
|
||||
{
|
||||
label: "Subtotal",
|
||||
value: formatProformaMoney(
|
||||
proforma.subtotalAmount,
|
||||
proforma.currencyCode,
|
||||
proforma.languageCode
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Descuentos",
|
||||
value: `-${formatProformaMoney(proforma.totalDiscountAmount, proforma.currencyCode, proforma.languageCode)}`,
|
||||
valueClassName: "text-red-600",
|
||||
},
|
||||
{
|
||||
label: "IVA",
|
||||
value: formatProformaMoney(proforma.ivaAmount, proforma.currencyCode, proforma.languageCode),
|
||||
},
|
||||
{
|
||||
label: "Total",
|
||||
value: formatProformaMoney(
|
||||
proforma.totalAmount,
|
||||
proforma.currencyCode,
|
||||
proforma.languageCode
|
||||
),
|
||||
valueClassName: "text-lg font-semibold text-foreground",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProformaDetailInspectorCard title="Resumen económico">
|
||||
<FieldValueList itemClassName="items-center" items={items} valueClassName="tabular-nums" />
|
||||
</ProformaDetailInspectorCard>
|
||||
);
|
||||
};
|
||||
@ -1 +1,3 @@
|
||||
export * from "./blocks";
|
||||
export * from "./components";
|
||||
export * from "./pages";
|
||||
|
||||
@ -1,331 +1,20 @@
|
||||
import { ErrorAlert, NotFoundCard } from "@erp/core/components";
|
||||
import { AppContent, BackHistoryButton, FieldValueList, PDFViewer } from "@repo/rdx-ui/components";
|
||||
import { MoneyHelper } from "@repo/rdx-utils";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Separator,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
DownloadIcon,
|
||||
MoreHorizontalIcon,
|
||||
PencilIcon,
|
||||
RefreshCwIcon,
|
||||
SquarePenIcon,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, useMemo } from "react";
|
||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
|
||||
import { IssueProformaDialog } from "../../../issue-proforma";
|
||||
import { ProformaStatusBadge } from "../../../list/ui/components";
|
||||
import { type Proforma, type ProformaItem, ProformaPageHeader } from "../../../shared";
|
||||
import { ProformaPageHeader } from "../../../shared";
|
||||
import { useProformaDetailPageController } from "../../controllers";
|
||||
|
||||
const ZOOM_OPTIONS = [
|
||||
{ label: "Ajustar", value: "fit" },
|
||||
{ label: "75%", value: "75" },
|
||||
{ label: "90%", value: "90" },
|
||||
{ label: "100%", value: "100" },
|
||||
{ label: "125%", value: "125" },
|
||||
];
|
||||
|
||||
const DOCUMENT_WIDTH = 794;
|
||||
const DOCUMENT_HEIGHT = 1123;
|
||||
|
||||
const formatDate = (value: string | null | undefined) => {
|
||||
if (!value) {
|
||||
return "No informado";
|
||||
}
|
||||
|
||||
const [year, month, day] = value.split("-");
|
||||
if (!(year && month && day)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const formatMoney = (amount: number, currencyCode: string) => {
|
||||
return MoneyHelper.formatCurrency(amount, 2, currencyCode);
|
||||
};
|
||||
|
||||
const formatQuantity = (value: number | null) => {
|
||||
if (value === null) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat("es-ES", {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const getStatusTone = (status: Proforma["status"]) => {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return "border-emerald-200 bg-emerald-50 text-emerald-900";
|
||||
case "sent":
|
||||
return "border-amber-200 bg-amber-50 text-amber-900";
|
||||
case "issued":
|
||||
return "border-sky-200 bg-sky-50 text-sky-900";
|
||||
case "rejected":
|
||||
return "border-red-200 bg-red-50 text-red-900";
|
||||
default:
|
||||
return "border-slate-200 bg-slate-50 text-slate-900";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusSummary = (proforma: Proforma) => {
|
||||
if (proforma.isArchived) {
|
||||
return {
|
||||
title: "Proforma archivada",
|
||||
description: "Está fuera del flujo principal y puedes recuperarla cuando lo necesites.",
|
||||
};
|
||||
}
|
||||
|
||||
switch (proforma.status) {
|
||||
case "approved":
|
||||
return {
|
||||
title: "Lista para convertirse en factura",
|
||||
description: "La proforma ya está aprobada y puede emitirse como factura.",
|
||||
};
|
||||
case "sent":
|
||||
return {
|
||||
title: "Pendiente de respuesta del cliente",
|
||||
description: "Se ha enviado y está a la espera de aceptación o rechazo.",
|
||||
};
|
||||
case "issued":
|
||||
return {
|
||||
title: "Convertida en factura",
|
||||
description: "La proforma ya ha sido emitida y forma parte del flujo fiscal.",
|
||||
};
|
||||
case "rejected":
|
||||
return {
|
||||
title: "Requiere revisión",
|
||||
description: "El cliente la ha rechazado y conviene revisar condiciones o importes.",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Pendiente de revisión",
|
||||
description: "Sigue en borrador y todavía puede ajustarse antes de enviarla.",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const InspectorCard = ({
|
||||
title,
|
||||
onEdit,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
onEdit?: () => void;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-xl">{title}</CardTitle>
|
||||
{onEdit ? (
|
||||
<Button onClick={onEdit} size="icon-sm" type="button" variant="ghost">
|
||||
<SquarePenIcon className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusInspectorCard = ({ proforma }: { proforma: Proforma }) => {
|
||||
const summary = getStatusSummary(proforma);
|
||||
|
||||
return (
|
||||
<Alert className={cn("shadow-sm", getStatusTone(proforma.status))}>
|
||||
<CheckCircle2Icon className="size-4" />
|
||||
<AlertTitle>{summary.title}</AlertTitle>
|
||||
<AlertDescription>{summary.description}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
const BillingInspectorCard = ({
|
||||
proforma,
|
||||
paymentMethodLabel,
|
||||
paymentTermLabel,
|
||||
onEdit,
|
||||
}: {
|
||||
proforma: Proforma;
|
||||
paymentMethodLabel: string | null;
|
||||
paymentTermLabel: string | null;
|
||||
onEdit: () => void;
|
||||
}) => {
|
||||
const items = [
|
||||
{
|
||||
label: "Método de pago",
|
||||
value: paymentMethodLabel ?? "No informado",
|
||||
},
|
||||
{
|
||||
label: "Plazo",
|
||||
value: paymentTermLabel ?? "No informado",
|
||||
},
|
||||
{
|
||||
label: "Moneda",
|
||||
value: proforma.currencyCode,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<InspectorCard onEdit={onEdit} title="Cobro">
|
||||
<FieldValueList items={items} />
|
||||
</InspectorCard>
|
||||
);
|
||||
};
|
||||
|
||||
const DatesInspectorCard = ({ proforma, onEdit }: { proforma: Proforma; onEdit: () => void }) => {
|
||||
const items = [
|
||||
{
|
||||
label: "Fecha proforma",
|
||||
value: formatDate(proforma.proformaDate),
|
||||
},
|
||||
{
|
||||
label: "Fecha operación",
|
||||
value: formatDate(proforma.operationDate),
|
||||
},
|
||||
{
|
||||
label: "Serie",
|
||||
value: proforma.series ?? "No informada",
|
||||
},
|
||||
{
|
||||
label: "Referencia interna",
|
||||
value: proforma.reference ?? "Sin referencia",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<InspectorCard onEdit={onEdit} title="Fechas y referencia">
|
||||
<FieldValueList items={items} />
|
||||
</InspectorCard>
|
||||
);
|
||||
};
|
||||
|
||||
const NotesInspectorCard = ({ proforma, onEdit }: { proforma: Proforma; onEdit: () => void }) => {
|
||||
return (
|
||||
<InspectorCard onEdit={onEdit} title="Notas">
|
||||
<div className="space-y-4 text-sm leading-6 text-muted-foreground">
|
||||
<div>
|
||||
<p className="mb-1 font-medium text-foreground">Descripción</p>
|
||||
<p>{proforma.description ?? "Sin descripción."}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<p className="mb-1 font-medium text-foreground">Observaciones</p>
|
||||
<p>{proforma.notes ?? "Sin notas adicionales."}</p>
|
||||
</div>
|
||||
</div>
|
||||
</InspectorCard>
|
||||
);
|
||||
};
|
||||
|
||||
const TotalsInspectorCard = ({ proforma }: { proforma: Proforma }) => {
|
||||
const items = [
|
||||
{
|
||||
label: "Subtotal",
|
||||
value: formatMoney(proforma.subtotalAmount, proforma.currencyCode),
|
||||
},
|
||||
{
|
||||
label: "Descuentos",
|
||||
value: `-${formatMoney(proforma.totalDiscountAmount, proforma.currencyCode)}`,
|
||||
valueClassName: "text-red-600",
|
||||
},
|
||||
{
|
||||
label: "IVA",
|
||||
value: formatMoney(proforma.ivaAmount, proforma.currencyCode),
|
||||
},
|
||||
{
|
||||
label: "Total",
|
||||
value: formatMoney(proforma.totalAmount, proforma.currencyCode),
|
||||
valueClassName: "text-lg font-semibold text-foreground",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<InspectorCard title="Resumen económico">
|
||||
<FieldValueList itemClassName="items-center" items={items} valueClassName="tabular-nums" />
|
||||
</InspectorCard>
|
||||
);
|
||||
};
|
||||
|
||||
const ProformaDetailSkeleton = () => {
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<Skeleton className="h-5 w-40" />
|
||||
<Skeleton className="h-10 w-96" />
|
||||
<Skeleton className="h-5 w-80" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.8fr)_360px]">
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-[840px] w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-28 w-full rounded-xl" />
|
||||
<Skeleton className="h-40 w-full rounded-xl" />
|
||||
<Skeleton className="h-44 w-full rounded-xl" />
|
||||
<Skeleton className="h-48 w-full rounded-xl" />
|
||||
<Skeleton className="h-36 w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderMeta = ({ proforma }: { proforma: Proforma }) => {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
proforma.recipient.name ?? "Cliente sin nombre",
|
||||
formatDate(proforma.proformaDate),
|
||||
`Total ${formatMoney(proforma.totalAmount, proforma.currencyCode)}`,
|
||||
],
|
||||
[proforma]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted-foreground">
|
||||
{items.map((item, index) => (
|
||||
<div className="flex items-center gap-3" key={`${item}-${index}`}>
|
||||
{index > 0 ? <span className="hidden text-border sm:inline">|</span> : null}
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import {
|
||||
ProformaDetailHeaderActions,
|
||||
ProformaDetailPreviewCard,
|
||||
ProformaDetailSidebar,
|
||||
} from "../blocks";
|
||||
import {
|
||||
ProformaDetailArchivedBadge,
|
||||
ProformaDetailHeaderMeta,
|
||||
ProformaDetailSkeleton,
|
||||
} from "../components";
|
||||
|
||||
export const ProformaDetailPage = () => {
|
||||
const ctrl = useProformaDetailPageController();
|
||||
@ -368,46 +57,14 @@ export const ProformaDetailPage = () => {
|
||||
<div className="flex h-full flex-col">
|
||||
<ProformaPageHeader
|
||||
actions={
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
<Button
|
||||
disabled={ctrl.issueDialogCtrl.isSubmitting}
|
||||
onClick={ctrl.primaryAction.onClick}
|
||||
type="button"
|
||||
>
|
||||
{ctrl.issueDialogCtrl.isSubmitting ? (
|
||||
<RefreshCwIcon className="mr-2 size-4 animate-spin" />
|
||||
) : null}
|
||||
{ctrl.primaryAction.label}
|
||||
</Button>
|
||||
|
||||
<Button onClick={ctrl.handleEdit} type="button" variant="outline">
|
||||
<PencilIcon className="mr-2 size-4" />
|
||||
Editar
|
||||
</Button>
|
||||
|
||||
<Button onClick={ctrl.handleDownload} type="button" variant="outline">
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
Descargar PDF
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button size="icon" type="button" variant="outline">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onClick={ctrl.handleDuplicate}>
|
||||
Duplicar proforma
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={ctrl.handleArchiveToggle}>
|
||||
{ctrl.proforma.isArchived ? "Desarchivar" : "Archivar"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<ProformaDetailHeaderActions
|
||||
isArchived={ctrl.proforma.isArchived}
|
||||
isPrimaryLoading={ctrl.issueDialogCtrl.isSubmitting}
|
||||
onArchiveToggle={ctrl.handleArchiveToggle}
|
||||
onDownload={ctrl.handleDownload}
|
||||
onDuplicate={ctrl.handleDuplicate}
|
||||
primaryAction={ctrl.primaryAction}
|
||||
/>
|
||||
}
|
||||
backLabel="Volver al listado"
|
||||
onBack={ctrl.navigateBack}
|
||||
@ -416,80 +73,35 @@ export const ProformaDetailPage = () => {
|
||||
<div className="border-b bg-background px-4 py-4 sm:px-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<ProformaStatusBadge className="text-sm" status={ctrl.proforma.status} />
|
||||
<HeaderMeta proforma={ctrl.proforma} />
|
||||
{ctrl.proforma.isArchived ? <ProformaDetailArchivedBadge /> : null}
|
||||
<ProformaDetailHeaderMeta proforma={ctrl.proforma} />
|
||||
</div>
|
||||
</div>
|
||||
</ProformaPageHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,2.2fr)_320px] 2xl:grid-cols-[minmax(0,2.5fr)_340px]">
|
||||
<Card className="overflow-hidden border-border/80 shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<PDFViewer
|
||||
className="h-[calc(100vh-11.5rem)] min-h-[720px]"
|
||||
defaultZoom="fit"
|
||||
description="Vista previa del PDF real generado para esta proforma."
|
||||
error={
|
||||
ctrl.previewPdfQuery.isError
|
||||
? ctrl.previewPdfQuery.error instanceof Error
|
||||
? ctrl.previewPdfQuery.error.message
|
||||
: "No se pudo cargar el PDF."
|
||||
: null
|
||||
}
|
||||
loading={ctrl.previewPdfQuery.isLoading || ctrl.previewPdfQuery.isFetching}
|
||||
onDownload={ctrl.handleDownload}
|
||||
onOpen={ctrl.handleOpenPreviewPdf}
|
||||
onPrint={ctrl.handlePrintPreview}
|
||||
src={ctrl.previewPdfUrl}
|
||||
title="Vista previa"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ProformaDetailPreviewCard
|
||||
error={
|
||||
ctrl.previewPdfQuery.isError
|
||||
? ctrl.previewPdfQuery.error instanceof Error
|
||||
? ctrl.previewPdfQuery.error.message
|
||||
: "No se pudo cargar el PDF."
|
||||
: null
|
||||
}
|
||||
isLoading={ctrl.previewPdfQuery.isLoading || ctrl.previewPdfQuery.isFetching}
|
||||
onDownload={ctrl.handleDownload}
|
||||
onOpen={ctrl.handleOpenPreviewPdf}
|
||||
onPrint={ctrl.handlePrintPreview}
|
||||
src={ctrl.previewPdfUrl}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<StatusInspectorCard proforma={ctrl.proforma} />
|
||||
|
||||
<BillingInspectorCard
|
||||
onEdit={ctrl.handleEdit}
|
||||
paymentMethodLabel={ctrl.paymentMethodLabel}
|
||||
paymentTermLabel={ctrl.paymentTermLabel}
|
||||
proforma={ctrl.proforma}
|
||||
/>
|
||||
|
||||
<DatesInspectorCard onEdit={ctrl.handleEdit} proforma={ctrl.proforma} />
|
||||
|
||||
<NotesInspectorCard onEdit={ctrl.handleEdit} proforma={ctrl.proforma} />
|
||||
|
||||
<TotalsInspectorCard proforma={ctrl.proforma} />
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-xl">Líneas incluidas</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Concepto</TableHead>
|
||||
<TableHead className="text-right">Total</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ctrl.proforma.items.slice(0, 4).map((item: ProformaItem) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="max-w-[220px] truncate">
|
||||
{item.description ?? "Sin descripción"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatMoney(item.totalAmount, ctrl.proforma.currencyCode)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<ProformaDetailSidebar
|
||||
onEdit={ctrl.handleEdit}
|
||||
paymentMethodLabel={ctrl.paymentMethodLabel}
|
||||
paymentTermLabel={ctrl.paymentTermLabel}
|
||||
proforma={ctrl.proforma}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user