This commit is contained in:
David Arranz 2026-08-04 17:15:57 +02:00
parent 92b17ba307
commit 32598c174c
3 changed files with 102 additions and 92 deletions

View File

@ -1,5 +1,4 @@
import { PDFViewer } from "@repo/rdx-ui/components";
import { Card, CardContent } from "@repo/shadcn-ui/components";
type ProformaDetailPreviewCardProps = {
src: string | null;
@ -19,21 +18,15 @@ export const ProformaDetailPreviewCard = ({
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>
<PDFViewer
className="h-[calc(100vh-11.5rem)] min-h-[720px]"
defaultZoom="fit"
error={error}
loading={isLoading}
onDownload={onDownload}
onOpen={onOpen}
onPrint={onPrint}
src={src}
/>
);
};

View File

@ -13,6 +13,7 @@ interface FormSectionCardProps {
description?: string;
icon?: ReactNode;
disabled?: boolean;
actions?: ReactNode;
children: ReactNode;
className?: string;
headerClassName?: string;
@ -24,10 +25,12 @@ export const FormSectionCard = ({
description,
icon,
disabled = false,
children,
className,
headerClassName,
contentClassName,
actions,
children,
}: FormSectionCardProps) => {
const hasHeader = Boolean(title || description || icon);
@ -35,25 +38,28 @@ export const FormSectionCard = ({
<Card className={className}>
{hasHeader && (
<CardHeader className={headerClassName}>
<div className="flex items-start gap-2">
{icon ? (
<div
className={cn(
"flex size-6 shrink-0 items-center justify-center",
disabled ? "text-muted-foreground" : "text-primary"
)}
>
{" "}
{icon}{" "}
</div>
) : null}
<div className="space-y-1">
{title ? <CardTitle className="font-semibold">{title}</CardTitle> : null}
{description ? (
<CardDescription className="font-medium">{description}</CardDescription>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-2">
{icon ? (
<div
className={cn(
"flex size-6 shrink-0 items-center justify-center",
disabled ? "text-muted-foreground" : "text-primary"
)}
>
{" "}
{icon}{" "}
</div>
) : null}
<div className="space-y-1">
{title ? <CardTitle className="font-semibold">{title}</CardTitle> : null}
{description ? (
<CardDescription className="font-medium">{description}</CardDescription>
) : null}
</div>
</div>
{actions ? <>{actions}</> : null}
</div>
</CardHeader>
)}

View File

@ -1,3 +1,4 @@
import { FormSectionCard, FormSectionGrid } from "@repo/rdx-ui/components";
import {
Button,
Select,
@ -7,8 +8,14 @@ import {
SelectValue,
Spinner,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { DownloadIcon, ExternalLinkIcon, PrinterIcon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
import {
DownloadIcon,
ExternalLinkIcon,
EyeIcon,
PrinterIcon,
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
const DEFAULT_ZOOM_OPTIONS = [50, 75, 100, 125, 150];
@ -77,8 +84,10 @@ export function PDFViewer({
const updateFitZoom = () => {
const styles = window.getComputedStyle(element);
const paddingX = Number.parseFloat(styles.paddingLeft) + Number.parseFloat(styles.paddingRight);
const paddingY = Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
const paddingX =
Number.parseFloat(styles.paddingLeft) + Number.parseFloat(styles.paddingRight);
const paddingY =
Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
const availableWidth = Math.max(element.clientWidth - paddingX, 320);
const availableHeight = Math.max(element.clientHeight - paddingY, 320);
const widthScale = availableWidth / PDF_PAGE_WIDTH;
@ -143,13 +152,8 @@ export function PDFViewer({
};
return (
<div className={cn("flex h-full flex-col", className)}>
<div className="flex flex-col gap-3 border-b px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold tracking-tight">{title}</h2>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<FormSectionCard
actions={
<div className="flex flex-wrap items-center gap-2">
<Button
disabled={loading || !src || (zoom !== FIT_ZOOM_VALUE && Number(zoom) <= minZoom)}
@ -226,58 +230,65 @@ export function PDFViewer({
<PrinterIcon className="size-4" />
</Button>
</div>
</div>
<div className="flex-1 overflow-auto bg-muted/30 p-4 md:p-5" ref={viewportRef}>
{loading ? (
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80">
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<Spinner className="size-4" />
Cargando vista previa del PDF...
}
className="shadow-sm"
contentClassName="space-y-5"
description={description}
icon={<EyeIcon className="size-5" />}
title={title}
>
<FormSectionGrid className="gap-y-6 md:grid-cols-1">
<div className="flex-1 overflow-auto bg-muted p-4 md:p-5" ref={viewportRef}>
{loading ? (
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80">
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<Spinner className="size-4" />
Cargando vista previa del PDF...
</div>
</div>
</div>
) : null}
) : null}
{!loading && error ? (
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
<div className="space-y-2">
<p className="font-medium text-foreground">No se pudo cargar la vista previa</p>
<p className="text-sm text-muted-foreground">{error}</p>
{!loading && error ? (
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
<div className="space-y-2">
<p className="font-medium text-foreground">No se pudo cargar la vista previa</p>
<p className="text-sm text-muted-foreground">{error}</p>
</div>
</div>
</div>
) : null}
) : null}
{!(loading || error) && resolvedSrc ? (
<div className="mx-auto flex min-h-full justify-center">
<div
className="origin-top rounded-xl border bg-background shadow-sm transition-transform"
style={{
transform: `scale(${zoomScale})`,
transformOrigin: "top center",
width: `${PDF_PAGE_WIDTH}px`,
}}
>
<iframe
className="w-full rounded-xl"
src={resolvedSrc}
style={{ height: `${PDF_PAGE_HEIGHT}px` }}
title={title}
/>
{!(loading || error) && resolvedSrc ? (
<div className="mx-auto flex min-h-full justify-center">
<div
className="origin-top rounded-xl border bg-red-500 shadow-sm transition-transform"
style={{
transform: `scale(${zoomScale})`,
transformOrigin: "top center",
width: `${PDF_PAGE_WIDTH}px`,
}}
>
<iframe
className="w-full rounded-xl bg-amber-500"
src={resolvedSrc}
style={{ height: `${PDF_PAGE_HEIGHT}px` }}
title={title}
/>
</div>
</div>
</div>
) : null}
) : null}
{loading || error || resolvedSrc ? null : (
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
<div className="space-y-2">
<p className="font-medium text-foreground">Vista previa no disponible</p>
<p className="text-sm text-muted-foreground">
El PDF de la proforma todavía no está listo para mostrarse.
</p>
{loading || error || resolvedSrc ? null : (
<div className="flex h-full min-h-[480px] items-center justify-center rounded-xl border border-dashed bg-background/80 px-6 text-center">
<div className="space-y-2">
<p className="font-medium text-foreground">Vista previa no disponible</p>
<p className="text-sm text-muted-foreground">
El PDF de la proforma todavía no está listo para mostrarse.
</p>
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
</FormSectionGrid>
</FormSectionCard>
);
}