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 { PDFViewer } from "@repo/rdx-ui/components";
import { Card, CardContent } from "@repo/shadcn-ui/components";
type ProformaDetailPreviewCardProps = { type ProformaDetailPreviewCardProps = {
src: string | null; src: string | null;
@ -19,21 +18,15 @@ export const ProformaDetailPreviewCard = ({
onPrint, onPrint,
}: ProformaDetailPreviewCardProps) => { }: ProformaDetailPreviewCardProps) => {
return ( return (
<Card className="overflow-hidden border-border/80 shadow-sm"> <PDFViewer
<CardContent className="p-0"> className="h-[calc(100vh-11.5rem)] min-h-[720px]"
<PDFViewer defaultZoom="fit"
className="h-[calc(100vh-11.5rem)] min-h-[720px]" error={error}
defaultZoom="fit" loading={isLoading}
description="Vista previa del PDF real generado para esta proforma." onDownload={onDownload}
error={error} onOpen={onOpen}
loading={isLoading} onPrint={onPrint}
onDownload={onDownload} src={src}
onOpen={onOpen} />
onPrint={onPrint}
src={src}
title="Vista previa"
/>
</CardContent>
</Card>
); );
}; };

View File

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

View File

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