import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties } from "react"; import type { FileMeta, Page, Session, TemplateFields } from "../types/session"; import { BASE_H, BASE_W } from "../lib/report"; import { JobSheetTemplate } from "./JobSheetTemplate"; type ReportPageCanvasProps = { session: Session | null; page: Page | null; pageIndex: number; pageCount: number; scale: number; template?: TemplateFields; sectionLabel?: string; className?: string; adaptive?: boolean; }; export function ReportPageCanvas({ session, page, pageIndex, pageCount, scale, template, sectionLabel, className = "", adaptive = false, }: ReportPageCanvasProps) { const items = page?.items ?? []; const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1; const pageVariant = page?.variant ?? "full"; const photos = resolvePagePhotos(session, page, pageIndex); const sheets = adaptive ? [photos] : [photos]; const sheetRefs = useRef>([]); const [sheetHeights, setSheetHeights] = useState([]); const defaultHeight = BASE_H * safeScale; const resolvedHeights = useMemo(() => { if (sheetHeights.length !== sheets.length) { return sheets.map(() => defaultHeight); } return sheetHeights.map((height) => (height > 0 ? height : defaultHeight)); }, [defaultHeight, sheetHeights, sheets.length]); const offsets = useMemo(() => { const values: number[] = []; let running = 0; resolvedHeights.forEach((height) => { values.push(running); running += height; }); return values; }, [resolvedHeights]); const containerHeight = resolvedHeights.reduce((sum, height) => sum + height, 0); const measureHeights = () => { const next = sheets.map((_, index) => { const node = sheetRefs.current[index]; if (!node) return defaultHeight; const rect = node.getBoundingClientRect(); return rect.height || defaultHeight; }); setSheetHeights((prev) => { if (prev.length === next.length && prev.every((val, idx) => Math.abs(val - next[idx]) < 1)) { return prev; } return next; }); }; useLayoutEffect(() => { measureHeights(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sheets.length, safeScale]); useEffect(() => { const observer = new ResizeObserver(() => measureHeights()); sheetRefs.current.forEach((node) => { if (node) observer.observe(node); }); return () => observer.disconnect(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sheets.length]); return (
{sheets.map((sheetPhotos, sheetIndex) => (
{ sheetRefs.current[sheetIndex] = node; }} style={{ width: `${BASE_W}px`, minHeight: `${BASE_H}px`, transform: `scale(${safeScale})`, transformOrigin: "top left", }} > {page?.blank ? (
) : ( )}
))}
{items .slice() .sort((a, b) => (a.z ?? 0) - (b.z ?? 0)) .map((item) => { const itemStyle: CSSProperties = { position: "absolute", left: `${item.x * safeScale}px`, top: `${item.y * safeScale}px`, width: `${item.w * safeScale}px`, height: `${item.h * safeScale}px`, zIndex: item.z ?? 0, }; if (item.type === "text") { return (
{item.content ?? ""}
); } if (item.type === "image") { return (
{item.name
); } return (
); })}
); } function resolvePagePhotos( session: Session | null, page: Page | null | undefined, pageIndex: number, ): FileMeta[] { if (!session) return []; const uploads = session.uploads?.photos ?? []; const byId = new Map(uploads.map((photo) => [photo.id, photo])); const explicit = page?.photo_ids ?? []; if (explicit.length) { return explicit.map((id) => byId.get(id)).filter(Boolean) as FileMeta[]; } return []; }