import type { Locale } from "@/i18n/config"; import { CertificateLayoutError, type CertificateField, type CertificateLayout } from "@/lib/certificate/elements"; import { FONT_KEYS, findUnsupportedCharacters } from "@/lib/certificate/fonts"; import { fixedTermFor, type CertificateTerm, type Personalization } from "@/lib/certificate/schema"; import { composeCertificateText, type CertificateText } from "@/lib/certificate/text"; import { CURRENT_TEMPLATE_VERSION, getTemplate } from "@/lib/certificate/templates"; /** * Einstieg in die Layout-Berechnung (Spec "Ein Layout, mehrere Ausgaben"): * * composeCertificateText() → aufgeloester Text * layoutCertificate() → positionierte Elemente (diese Datei) * renderCertificatePdf() / renderCertificateSvg() / renderCertificateImage() * * Reine Funktionen ohne Datenbank: dieselbe Eingabe ergibt dasselbe Layout, * auf dem Server fuer Vorschau, Textur und PDF gleichermassen. */ export { CertificateLayoutError, PAGE_HEIGHT, PAGE_WIDTH, SAFE_CONTENT, SAFE_FRAME, MIN_TEXT_SIZE, MIN_BODY_SIZE, textStartX, elementBounds, type CertificateField, type CertificateLayout, type LayoutElement, type TextElement, type PathElement, type ImageElement, type TextRole, } from "@/lib/certificate/elements"; export { CURRENT_TEMPLATE_VERSION, getTemplate } from "@/lib/certificate/templates"; export function layoutCertificate(text: CertificateText, templateVersion: string = CURRENT_TEMPLATE_VERSION): CertificateLayout { return getTemplate(templateVersion).layout(text); } /** Alle Textzeilen in Zeichenreihenfolge als "rolle: text" — Grundlage fuer den Zeilenvergleich der Ausgaben. */ export function layoutLines(layout: CertificateLayout): string[] { return layout.elements.flatMap((el) => (el.kind === "text" ? [`${el.role}: ${el.text}`] : [])); } export type FitCheck = | { ok: true; layout: CertificateLayout } | { ok: false; field: CertificateField; reason: "glyphs"; characters: string[]; message: string } | { ok: false; field: CertificateField; reason: "fit"; message: string }; const FIELD_LABELS: Record = { beeName: "Der Name der Patenbiene", recipientName: "Der Name auf der Urkunde", giverNames: "Die Angabe der Schenkenden", dedication: "Die Widmung", occasion: "Die Anlasszeile", layout: "Der Urkundentext", }; /** Deutsche Meldung fuer Formular und Checkout; die Felder field/reason erlauben eine eigene Uebersetzung. */ function fitMessage(field: CertificateField, reason: "glyphs" | "fit", characters: string[] = []): string { if (reason === "glyphs") { return `${FIELD_LABELS[field]} enthält Zeichen, die auf der Urkunde nicht gedruckt werden können: ${characters.join(" ")}`; } return `${FIELD_LABELS[field]} passt so nicht auf die Urkunde. Bitte kürzen oder an einer Leerstelle teilen.`; } // Breiteste plausible Nummer fuer die Pruefung vor der Bestellung (die echte // Nummer entsteht erst nach der Zahlung). const SAMPLE_NUMBER = "WH-P-2026-00000"; const CUSTOMER_FIELDS: ReadonlyArray<[CertificateField, keyof Personalization]> = [ ["beeName", "beeName"], ["recipientName", "recipientName"], ["giverNames", "giverNames"], ["dedication", "dedication"], ]; /** * Serverseitige Pruefung einer bereits per zod validierten Personalisierung: * 1. jedes Zeichen der Kundenfelder hat in allen Urkundenschriften eine * Glyphe (bewusst alle Schnitte, nicht nur die der aktuellen Vorlage — * so bleibt eine Bestellung auch mit einer spaeteren Vorlage druckbar); * 2. das Layout passt in die kleinste Stufe. * Vorschau-API und Checkout nutzen dieselbe Funktion. */ export function checkCertificateFits( personalization: Personalization, locale: Locale, opts?: { term?: CertificateTerm; templateVersion?: string; number?: string } ): FitCheck { for (const [field, key] of CUSTOMER_FIELDS) { const value = personalization[key]; if (!value) continue; const characters = findUnsupportedCharacters(value, FONT_KEYS); if (characters.length > 0) return { ok: false, field, reason: "glyphs", characters, message: fitMessage(field, "glyphs", characters) }; } const text = composeCertificateText({ personalization, locale, term: opts?.term ?? fixedTermFor(personalization.startDate), number: opts?.number ?? SAMPLE_NUMBER, }); try { return { ok: true, layout: layoutCertificate(text, opts?.templateVersion) }; } catch (error: unknown) { if (error instanceof CertificateLayoutError) { return { ok: false, field: error.field, reason: "fit", message: fitMessage(error.field, "fit") }; } throw error; } }