import React, { useState, useEffect, useCallback, useRef, memo, useDeferredValue, } from "react"; import { TemplateState } from "../../types/elements"; import { useBuilder } from "../../contexts/builder/BuilderContext"; // Preview system removed import { useCanvasSettings } from "../../contexts/CanvasSettingsContext"; import { getEditorFeatureFlags } from "../../utils/editorFeatures"; import { debugLog, debugError, debugWarn } from "../../utils/debug"; interface HeaderProps { templateName: string; templateDescription: string; canvasWidth: number; canvasHeight: number; showGuides: boolean; snapToGrid: boolean; isNewTemplate: boolean; isModified: boolean; isSaving: boolean; isLoading: boolean; isEditingExistingTemplate: boolean; onSave: () => void; onPreview: () => void; onNewTemplate: () => void; onUpdateTemplateSettings: (settings: Partial) => void; } // ── PDF Simulation HTML helpers (module-level, aucune fermeture sur state) ── function pdfSimBuildSpacing(value: any): string { if (!value) return ""; if (typeof value === "number") return `${value}px`; if (typeof value === "object") { return `${value.top || 0}px ${value.right || 0}px ${value.bottom || 0}px ${value.left || 0}px`; } return ""; } function pdfSimBuildBorder(border: any): string { if (!border?.width) return ""; return `${border.width || 1}px ${border.style || "solid"} ${border.color || "#e5e7eb"}`; } function pdfSimBuildFlexLayout(layout: string | undefined, gap: number = 8): string { if (!layout) return ""; if (layout === "horizontal") return `display: flex; flex-direction: row; gap: ${gap}px;`; if (layout === "vertical") return `display: flex; flex-direction: column; gap: ${gap}px;`; return ""; } function pdfSimBuildGlobalStyles(el: any): string { let s = ""; if (el.fontFamily) s += `font-family: ${el.fontFamily};`; if (el.fontWeight && el.fontWeight !== "normal") s += `font-weight: ${el.fontWeight};`; if (el.fontStyle && el.fontStyle !== "normal") s += `font-style: ${el.fontStyle};`; if (el.textDecoration && el.textDecoration !== "none") s += `text-decoration: ${el.textDecoration};`; if (el.textTransform && el.textTransform !== "none") s += `text-transform: ${el.textTransform};`; if (el.wordSpacing && el.wordSpacing !== "normal") s += `word-spacing: ${el.wordSpacing};`; return s; } function pdfSimCalcAdjustedDimensions( width: number, height: number, padding: any, ): { width: number; height: number; paddingStyle: string } { if (!padding) return { width, height, paddingStyle: "" }; const top = (padding?.top || 0) as number; const bottom = (padding?.bottom || 0) as number; const left = (padding?.left || 0) as number; const right = (padding?.right || 0) as number; return { width: Math.max(0, width - left - right), height: Math.max(0, height - top - bottom), paddingStyle: `padding: ${top}px ${right}px ${bottom}px ${left}px;`, }; } function pdfSimBuildFontStyles(el: any, ah: number): string { let s = ""; if (el.fontSize) s += ` font-size: ${el.fontSize}px;`; if (el.fontFamily) s += ` font-family: ${el.fontFamily};`; if (el.fontWeight && el.fontWeight !== "normal") s += ` font-weight: ${el.fontWeight};`; if (el.fontStyle && el.fontStyle !== "normal") s += ` font-style: ${el.fontStyle};`; if (el.textDecoration && el.textDecoration !== "none") s += ` text-decoration: ${el.textDecoration};`; if (el.wordSpacing && el.wordSpacing !== "normal") s += ` word-spacing: ${el.wordSpacing};`; if (el.textTransform && el.textTransform !== "none") s += ` text-transform: ${el.textTransform};`; if (el.textAlign) s += ` text-align: ${el.textAlign};`; if (el.verticalAlign && el.verticalAlign !== "baseline") s += ` line-height: ${ah}px;`; return s; } function pdfSimBuildColorStyles(el: any): string { let s = ""; if (el.textColor) s += ` color: ${el.textColor};`; if (el.backgroundColor && el.backgroundColor !== "transparent" && el.showBackground !== false) { s += ` background-color: ${el.backgroundColor};`; } return s; } function pdfSimBuildBoxStyles(el: any, paddingStyle: string): string { let s = ""; if (el.borderWidth && el.borderWidth > 0) s += ` border: ${el.borderWidth}px solid ${el.borderColor || "#e5e7eb"};`; if (el.borderRadius && el.borderRadius > 0) s += ` border-radius: ${el.borderRadius}px;`; if (paddingStyle) s += ` ${paddingStyle}`; if (el.shadowBlur && el.shadowBlur > 0) { s += ` box-shadow: ${el.shadowOffsetX || 0}px ${el.shadowOffsetY || 0}px ${el.shadowBlur}px ${el.shadowColor || "#000000"};`; } if (el.rotation && el.rotation !== 0) s += ` transform: rotate(${el.rotation}deg);`; if (el.opacity !== undefined && el.opacity < 1) s += ` opacity: ${el.opacity};`; return s; } function pdfSimBuildBaseStyles( el: any, x: number, y: number, aw: number, ah: number, paddingStyle: string, ): string { return `left: ${x}px; top: ${y}px; width: ${aw}px; height: ${ah}px;` + pdfSimBuildFontStyles(el, ah) + pdfSimBuildColorStyles(el) + pdfSimBuildBoxStyles(el, paddingStyle); } function pdfSimGetJustify(textAlign?: string): string { if (textAlign === "center") return "center"; if (textAlign === "right") return "flex-end"; return "flex-start"; } function pdfSimGetAlign(verticalAlign?: string): string { if (verticalAlign === "middle" || verticalAlign === "center") return "center"; if (verticalAlign === "bottom") return "flex-end"; return "flex-start"; } function pdfSimWrapVerticalAlign(content: string, el: any): string { const isTop = !el.verticalAlign || el.verticalAlign === "top"; if (isTop) { return el.textAlign ? `
${content}
` : content; } const justifyValue = pdfSimGetJustify(el.textAlign); const alignValue = (el.verticalAlign === "middle" || el.verticalAlign === "center") ? "center" : "flex-end"; return `
${content}
`; } function pdfSimRenderText(el: any): { content: string; stylesAddition: string } { let content: string = el.text || el.content || "Texte"; const gs = pdfSimBuildGlobalStyles(el); let sa = gs; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; content = pdfSimWrapVerticalAlign(content, el); return { content, stylesAddition: sa }; } function pdfSimRenderDocumentType(el: any): { content: string; stylesAddition: string } { let content: string = el.title || el.text || el.content || "FACTURE"; const gs = pdfSimBuildGlobalStyles(el); let sa = gs; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; const jv = pdfSimGetJustify(el.textAlign); const av = pdfSimGetAlign(el.verticalAlign); content = `
${content}
`; return { content, stylesAddition: sa }; } function pdfSimRenderImage(el: any): { content: string; stylesAddition: string } { let content: string; if (el.src) { const gs = pdfSimBuildGlobalStyles(el); let imgStyles = `width: 100%; height: 100%; display: block; object-fit: ${el.objectFit || "cover"};`; if (el.opacity !== undefined && el.opacity < 1) imgStyles += ` opacity: ${el.opacity};`; if (el.borderRadius && el.borderRadius > 0) imgStyles += ` border-radius: ${el.borderRadius}px;`; if (gs) imgStyles += ` ${gs}`; content = ``; } else { content = "📦"; } let sa = ""; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; return { content, stylesAddition: sa }; } function pdfSimRenderLine(el: any): { content: string; stylesAddition: string } { const lw = el.strokeWidth || 1; const lc = el.strokeColor || "#000000"; const ls = el.style || el.borderStyle || "solid"; let innerStyle: string; if (ls === "dashed") { innerStyle = `border-bottom: ${lw}px dashed ${lc}; width: 100%; margin: auto 0;`; } else if (ls === "dotted") { innerStyle = `border-bottom: ${lw}px dotted ${lc}; width: 100%; margin: auto 0;`; } else { innerStyle = `background-color: ${lc}; height: ${lw}px; width: 100%; margin: auto 0;`; } let sa = " display: flex; align-items: center;"; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; return { content: `
`, stylesAddition: sa }; } function pdfSimGetSectionMeta(section: string): { colorProp: string | null; bgProp: string | null; prefix: string } { const colorProps: Record = { header: "headerTextColor", total: "totalTextColor", row: "rowTextColor" }; const bgProps: Record = { header: "headerBackgroundColor", cell: "bodyBackgroundColor" }; const prefixes: Record = { header: "header", cell: "body" }; return { colorProp: colorProps[section] ?? null, bgProp: bgProps[section] ?? null, prefix: prefixes[section] ?? section }; } function pdfSimApplySectionFonts(el: any, prefix: string): string { const size = el[`${prefix}FontSize`]; const family = el[`${prefix}FontFamily`]; const weight = el[`${prefix}FontWeight`]; const style = el[`${prefix}FontStyle`]; let s = ""; if (size) s += ` font-size: ${size}px;`; if (family) s += ` font-family: ${family};`; if (weight) s += ` font-weight: ${weight};`; if (style) s += ` font-style: ${style};`; return s; } function pdfSimBuildTableSectionStyles(el: any, section: "header" | "cell" | "row" | "total"): string { let s = "padding: 8px; overflow: hidden; text-overflow: ellipsis;"; if (el.textAlign) s += ` text-align: ${el.textAlign};`; if (el.verticalAlign && el.verticalAlign !== "baseline") s += ` vertical-align: ${el.verticalAlign};`; if (el.showBorders && el.borderWidth) s += ` border: ${el.borderWidth}px solid ${el.borderColor || "#e5e7eb"};`; const { colorProp, bgProp, prefix } = pdfSimGetSectionMeta(section); if (colorProp && el[colorProp]) s += ` color: ${el[colorProp]};`; else if (el.textColor) s += ` color: ${el.textColor};`; s += pdfSimApplySectionFonts(el, prefix); if (bgProp && el[bgProp] && el[bgProp] !== "transparent") s += ` background-color: ${el[bgProp]};`; if (section === "total") s += " font-weight: bold;"; return s; } function pdfSimGetTableCellValue(col: string | null, element: any): string { const baseAmount = 100; const quantity = 1; const shippingCost = element.shippingCost || 10; const globalDiscount = element.globalDiscount || 0; const taxRate = element.taxRate || 0.2; const taxAmount = (baseAmount - globalDiscount) * taxRate; if (col === "SKU") return "SKU001"; if (col === "Produit") return "Produit Exemple"; if (col === "Description") return "Description du produit"; if (col === "Qty" || col === "Quantité") return String(quantity); if (col === "Prix Unit.") return baseAmount.toFixed(2) + " €"; if (col === "Total") return (baseAmount * quantity).toFixed(2) + " €"; if (col === "Shipping") return shippingCost.toFixed(2) + " €"; if (col === "Tax") return taxAmount.toFixed(2) + " €"; return "N/A"; } function pdfSimRenderTable(el: any): { content: string; stylesAddition: string } { let tableStyles = "border-collapse: collapse; width: 100%; table-layout: fixed;"; if (el.globalFontSize) tableStyles += ` font-size: ${el.globalFontSize}px;`; else if (el.fontSize) tableStyles += ` font-size: ${el.fontSize}px;`; if (el.globalFontFamily) tableStyles += ` font-family: ${el.globalFontFamily};`; else if (el.fontFamily) tableStyles += ` font-family: ${el.fontFamily};`; const headerStyle = pdfSimBuildTableSectionStyles(el, "header"); const cellStyles = pdfSimBuildTableSectionStyles(el, "cell"); const rowStyle = pdfSimBuildTableSectionStyles(el, "row"); const totalStyle = pdfSimBuildTableSectionStyles(el, "total"); const tableId = `table-${el.id}`; let tableCSS = ""; if (el.showAlternatingRows && el.alternateRowColor) { tableCSS = ``; } let content: string; if (el.content) { const wrapped = el.content .replaceAll(/]*)>/g, ``) .replaceAll(/]*)>/g, ``); content = tableCSS + `${wrapped}
`; } else { content = pdfSimBuildTableFromProps(el, tableId, tableStyles, tableCSS, headerStyle, rowStyle, totalStyle); } let sa = ""; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; return { content, stylesAddition: sa }; } function pdfSimBuildTableFromProps( el: any, tableId: string, tableStyles: string, tableCSS: string, headerStyle: string, rowStyle: string, totalStyle: string, ): string { const cols = [ el.showSku ? "SKU" : null, "Produit", el.showDescription ? "Description" : null, el.showQuantity ? "Qty" : "Quantité", "Prix Unit.", "Total", el.showShipping ? "Shipping" : null, el.showTax ? "Tax" : null, ].filter(Boolean); const baseAmount = 100; const shippingCost = el.shippingCost || 10; const globalDiscount = el.globalDiscount || 0; const taxRate = el.taxRate || 0.2; const taxAmount = (baseAmount - globalDiscount) * taxRate; const total = baseAmount + shippingCost - globalDiscount + taxAmount + (el.orderFees || 0); let html = tableCSS + ``; if (el.showHeaders !== false) { const headerCells = cols.map(c => ``).join(""); html += `${headerCells}`; } html += ""; html += cols.map(col => ``).join(""); html += ""; if (el.showGlobalDiscount && globalDiscount > 0) { html += ``; } html += ""; html += cols.map((col, idx) => { let val: string; if (idx === cols.length - 1) val = total.toFixed(2) + " €"; else if (col === "Produit") val = "TOTAL"; else val = ""; return ``; }).join(""); html += "
${c}
${pdfSimGetTableCellValue(col, el)}
Discount:-${globalDiscount.toFixed(2)} €
${val}
"; return html; } function pdfSimConvertToString(value: any): string { if (!value) return ""; if (typeof value === "string") return value; if (typeof value === "object") { if (value.value) return String(value.value); if (value[0]) return String(value[0]); if (value.phone) return String(value.phone); return JSON.stringify(value); } return String(value); } function pdfSimGetCompanyData(el: any): Record { const pc = (globalThis as any).pdfBuilderData?.company || {}; const c = pdfSimConvertToString; return { name: c(el.companyName || pc.name || ""), address: c(el.companyAddress || pc.address || ""), city: c(el.companyCity || pc.city || ""), phone: c(el.companyPhone || pc.phone || ""), email: c(el.companyEmail || pc.email || ""), website: c(el.companyWebsite || pc.website || ""), siret: c(el.companySiret || pc.siret || ""), tva: c(el.companyTva || pc.vat || ""), rcs: c(el.companyRcs || pc.rcs || ""), capital: c(el.companyCapital || pc.capital || ""), }; } function pdfSimIsValidValue(value: string): boolean { return !!(value && value.trim() !== "" && value !== "Non indiqué" && value !== "{}"); } function pdfSimHeaderDiv(text: string, el: any, gs: string): string { const fsz = el.headerFontSize || el.fontSize || 14; const fw = el.headerFontWeight || "bold"; const col = el.headerTextColor || el.textColor || "#000000"; return `
${text}
`; } function pdfSimBodyDiv(text: string, el: any, gs: string): string { const fsz = el.bodyFontSize || el.fontSize || 12; const fw = el.bodyFontWeight || el.fontWeight || "normal"; const col = el.bodyTextColor || el.textColor || "#666666"; return `
${text}
`; } function pdfSimPushCompanyField( parts: string[], el: any, flag: string, value: string, display: string, gs: string, isHeader = false, ): void { if (el[flag] !== false && pdfSimIsValidValue(value)) { parts.push(isHeader ? pdfSimHeaderDiv(display, el, gs) : pdfSimBodyDiv(display, el, gs)); } } function pdfSimRenderCompanyInfoVertical(el: any, d: Record, gs: string): string { const parts: string[] = []; pdfSimPushCompanyField(parts, el, "showCompanyName", d.name, d.name, gs, true); if (el.showAddress !== false && pdfSimIsValidValue(d.address)) { const addr = pdfSimIsValidValue(d.city) ? `${d.address}, ${d.city}` : d.address; parts.push(pdfSimBodyDiv(addr, el, gs)); } pdfSimPushCompanyField(parts, el, "showEmail", d.email, `Email: ${d.email}`, gs); pdfSimPushCompanyField(parts, el, "showPhone", d.phone, `Tél: ${d.phone}`, gs); pdfSimPushCompanyField(parts, el, "showSiret", d.siret, `SIRET: ${d.siret}`, gs); pdfSimPushCompanyField(parts, el, "showRcs", d.rcs, `RCS: ${d.rcs}`, gs); pdfSimPushCompanyField(parts, el, "showVat", d.tva, `TVA: ${d.tva}`, gs); if (el.showCapital !== false && pdfSimIsValidValue(d.capital)) parts.push(pdfSimBodyDiv(`Capital: ${d.capital} €`, el, gs)); const gap = Math.round((el.fontSize || 12) * 0.1); return `
${parts.join("")}
`; } function pdfSimGetLegalFields(el: any, d: Record): string[] { return [ el.showSiret !== false && pdfSimIsValidValue(d.siret) ? d.siret : "", el.showRcs !== false && pdfSimIsValidValue(d.rcs) ? d.rcs : "", el.showVat !== false && pdfSimIsValidValue(d.tva) ? d.tva : "", el.showCapital !== false && pdfSimIsValidValue(d.capital) ? d.capital + " €" : "", ].filter((v): v is string => Boolean(v)); } function pdfSimRenderCompanyInfoHorizontal(el: any, d: Record, gs: string): string { const parts: string[] = []; if (el.showCompanyName !== false && pdfSimIsValidValue(d.name)) { parts.push(`
${d.name}
`); } const addrParts = [ el.showAddress !== false && pdfSimIsValidValue(d.address) ? d.address : "", pdfSimIsValidValue(d.city) ? d.city : "", ].filter(Boolean); if (addrParts.length) parts.push(pdfSimBodyDiv(addrParts.join(", "), el, `width: 100%; ${gs}`)); const contact = [ el.showEmail !== false && pdfSimIsValidValue(d.email) ? d.email : "", el.showPhone !== false && pdfSimIsValidValue(d.phone) ? d.phone : "", ].filter(Boolean).join(" | "); if (contact) parts.push(pdfSimBodyDiv(contact, el, `width: 100%; ${gs}`)); const legal = pdfSimGetLegalFields(el, d).join(" | "); if (legal) parts.push(pdfSimBodyDiv(legal, el, `width: 100%; ${gs}`)); return `
${parts.join("")}
`; } function pdfSimRenderCompanyInfoCompact(el: any, d: Record, gs: string): string { const parts: string[] = []; if (el.showCompanyName !== false && pdfSimIsValidValue(d.name)) { parts.push(`
${d.name}
`); } const addrWithCity = pdfSimIsValidValue(d.city) ? `${d.address}, ${d.city}` : d.address; const addrStr = el.showAddress !== false && pdfSimIsValidValue(d.address) ? addrWithCity : ""; const contactFields = [ el.showEmail !== false && pdfSimIsValidValue(d.email) ? d.email : "", el.showPhone !== false && pdfSimIsValidValue(d.phone) ? d.phone : "", ]; const fields = [addrStr, ...contactFields, ...pdfSimGetLegalFields(el, d)].filter(Boolean).join(" • "); if (fields) { const fsz = el.bodyFontSize || el.fontSize || 12; const fw = el.bodyFontWeight || el.fontWeight || "normal"; const col = el.bodyTextColor || el.textColor || "#666666"; parts.push(`
${fields}
`); } return `
${parts.join("")}
`; } function pdfSimBuildCompanyContent(el: any, d: Record, gs: string): string { const custom = el.content || el.text || ""; if (custom) return custom; if (el.layout === "horizontal") return pdfSimRenderCompanyInfoHorizontal(el, d, gs); if (el.layout === "compact") return pdfSimRenderCompanyInfoCompact(el, d, gs); return pdfSimRenderCompanyInfoVertical(el, d, gs); } function pdfSimApplyLayoutStyle(sa: string, el: any, gap?: number): string { if (!el.layout) return sa; const ls = gap === undefined ? pdfSimBuildFlexLayout(el.layout) : pdfSimBuildFlexLayout(el.layout, gap); return ls ? sa + ` ${ls}` : sa; } function pdfSimBuildCompanyBoxStyles(el: any): string { let sa = ""; if (el.showBackground && el.backgroundColor && el.backgroundColor !== "transparent") sa += ` background-color: ${el.backgroundColor};`; if (el.borderWidth && el.borderWidth > 0 && el.borderColor) sa += ` border: ${el.borderWidth}px solid ${el.borderColor};`; if (el.rotation && el.rotation !== 0) sa += ` transform: rotate(${el.rotation}deg);`; if (el.borderRadius && el.borderRadius > 0) sa += ` border-radius: ${el.borderRadius}px;`; return sa; } function pdfSimBuildCompanyInfoStyles(el: any): string { let sa = pdfSimBuildCompanyBoxStyles(el); if (el.padding) { const ps = pdfSimBuildSpacing(el.padding); if (ps) sa += ` padding: ${ps};`; } else sa += " padding: 8px;"; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; if (el.separator) sa += ` border-bottom: 1px solid ${el.borderColor || "#e5e7eb"};`; return pdfSimApplyLayoutStyle(sa, el) + " overflow: auto;"; } function pdfSimRenderCompanyInfo(el: any): { content: string; stylesAddition: string } { const d = pdfSimGetCompanyData(el); const gs = pdfSimBuildGlobalStyles(el); const content = pdfSimBuildCompanyContent(el, d, gs) || "
Entreprise non configurée
"; return { content, stylesAddition: pdfSimBuildCompanyInfoStyles(el) }; } function pdfSimBuildCustomerContent(el: any): string { const fsz = el.bodyFontSize || 12; const ff = el.bodyFontFamily || "Arial"; const fw = el.bodyFontWeight || "normal"; const fi = el.bodyFontStyle || "normal"; const tc = el.textColor || "#374151"; const hfsz = el.headerFontSize || 14; const hff = el.headerFontFamily || "Arial"; const hfw = el.headerFontWeight || "bold"; const hfi = el.headerFontStyle || "normal"; const htc = el.headerTextColor || "#111827"; const base = `font-size: ${fsz}px; font-family: ${ff}; font-weight: ${fw}; font-style: ${fi}; color: ${tc}; margin: 0;`; const hBase = `font-size: ${hfsz}px; font-family: ${hff}; font-weight: ${hfw}; font-style: ${hfi}; color: ${htc}; margin-bottom: 4px;`; const parts: string[] = []; if (el.showHeaders !== false) parts.push(`
Client
`); if (el.showFullName !== false) parts.push(`
Prénom Nom
`); if (el.showAddress !== false) parts.push(`
123 Rue de la Paix, 75000 Paris
`); if (el.showEmail !== false) parts.push(`
client@example.com
`); if (el.showPhone !== false) parts.push(`
+01 23 45 67 89
`); return parts.join(""); } function pdfSimBuildCustomerStyles(el: any): string { let sa = pdfSimBuildGlobalStyles(el); if (el.backgroundColor && el.backgroundColor !== "transparent" && el.showBackground !== false) sa += ` background-color: ${el.backgroundColor};`; if (el.padding) { const ps = pdfSimBuildSpacing(el.padding); if (ps) sa += ` padding: ${ps};`; } else sa += " padding: 8px;"; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; if (el.borderRadius && el.borderRadius > 0) sa += ` border-radius: ${el.borderRadius}px;`; const gap = Math.round((el.fontSize || 12) * 0.1); return pdfSimApplyLayoutStyle(sa, el, gap) + " overflow: auto;"; } function pdfSimRenderCustomerInfo(el: any): { content: string; stylesAddition: string } { return { content: el.content || el.text || pdfSimBuildCustomerContent(el), stylesAddition: pdfSimBuildCustomerStyles(el), }; } function pdfSimBuildMentionsContent(el: any): string { const pc = (globalThis as any).pdfBuilderData?.company || {}; const mentionParts: string[] = []; if (el.showEmail !== false && (el.email || pc.email)?.trim()) mentionParts.push(el.email || pc.email); if (el.showPhone !== false && (el.phone || pc.phone)?.trim()) mentionParts.push(el.phone || pc.phone); if (el.showSiret !== false && (el.siret || pc.siret)?.trim()) mentionParts.push(`SIRET: ${el.siret || pc.siret}`); if (el.showVat !== false && (el.tva || pc.vat)?.trim()) mentionParts.push(`TVA: ${el.tva || pc.vat}`); return mentionParts.join(el.separator || " • "); } function pdfSimRenderMentions(el: any): { content: string; stylesAddition: string } { const content = el.content || el.text || pdfSimBuildMentionsContent(el); let sa = pdfSimBuildGlobalStyles(el); const ps = pdfSimBuildSpacing(el.padding); if (ps) sa += ` padding: ${ps};`; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; if (el.borderRadius && el.borderRadius > 0) sa += ` border-radius: ${el.borderRadius}px;`; if (el.showBackground && el.backgroundColor && el.backgroundColor !== "transparent") sa += ` background-color: ${el.backgroundColor};`; if (el.showSeparator) sa += ` border-bottom: 1px solid ${el.borderColor || "#e5e7eb"};`; const gap = Math.round((el.fontSize || 12) * 0.1); return { content, stylesAddition: pdfSimApplyLayoutStyle(sa, el, gap) }; } function pdfSimRenderInvoiceNumber(el: any): { content: string; stylesAddition: string } { const invoiceNum = el.text || el.content || "001"; const invoiceFormat = el.format || "FAC-{order_number}"; let invoiceContent = invoiceFormat.replace("{order_number}", invoiceNum); const gs = pdfSimBuildGlobalStyles(el); const jv = pdfSimGetJustify(el.textAlign); const av = pdfSimGetAlign(el.verticalAlign); let sa = ` display: flex; align-items: ${av}; justify-content: ${jv};`; if (el.showLabel && el.labelText) { const labelText = el.labelText || "Invoice:"; const hfsz = el.headerFontSize || el.fontSize || 12; const nfsz = el.numberFontSize || el.fontSize || 14; const lc = el.headerTextColor || el.textColor || "#000000"; const nc = el.textColor || "#000000"; invoiceContent = `
${labelText}
${invoiceContent}
`; } if (el.contentAlign) sa += ` text-align: ${el.contentAlign};`; const ps2 = pdfSimBuildSpacing(el.padding); if (ps2) sa += ` padding: ${ps2};`; const ms = pdfSimBuildSpacing(el.margin); if (ms) sa += ` margin: ${ms};`; const bs = pdfSimBuildBorder(el.border); if (bs) sa += ` border: ${bs};`; return { content: invoiceContent, stylesAddition: sa }; } function pdfSimRenderElementContent(el: any): { content: string; stylesAddition: string } { switch (el.type) { case "text": case "dynamic_text": return pdfSimRenderText(el); case "document_type": return pdfSimRenderDocumentType(el); case "company_logo": case "image": return pdfSimRenderImage(el); case "line": case "separator": return pdfSimRenderLine(el); case "product_table": case "table": return pdfSimRenderTable(el); case "company_info": return pdfSimRenderCompanyInfo(el); case "customer_info": return pdfSimRenderCustomerInfo(el); case "mentions": case "note": return pdfSimRenderMentions(el); case "woocommerce_invoice_number": return pdfSimRenderInvoiceNumber(el); default: return { content: el.text || el.content || el.label || `[${el.type}]`, stylesAddition: "" }; } } function pdfSimRenderOneElement(element: any): string { if (element.visible === false) return ""; const { width: aw, height: ah, paddingStyle } = pdfSimCalcAdjustedDimensions( element.width || 100, element.height || 50, element.padding, ); let styles = pdfSimBuildBaseStyles(element, element.x || 0, element.y || 0, aw, ah, paddingStyle); const { content, stylesAddition } = pdfSimRenderElementContent(element); if (stylesAddition) styles += stylesAddition; if (["customer_info", "product_table", "company_info"].includes(element.type)) { styles = styles.replace("overflow: hidden", "overflow: auto"); } return `
${content}
`; } function generatePDFSimulationHTML(elementsInput?: any[], canvasInput?: any, templateInput?: any): string { const w = canvasInput?.width || templateInput?.canvasWidth || 794; const h = canvasInput?.height || templateInput?.canvasHeight || 1123; const elements: any[] = elementsInput || []; const template: any = templateInput || {}; const emptyContent = `

🎨 Canvas vide

Aucun élément n'a été ajouté au template.

`; const bodyContent = (elements.length > 0) ? elements.map(pdfSimRenderOneElement).join("") : emptyContent; return ` Aperçu PDF - ${template.name || "Template"}
${bodyContent}
`; } // ─── usePreviewActions ──────────────────────────────────────────────────────── function usePreviewActions(isPremium: boolean) { const { state } = useBuilder(); const [showPreviewModal, setShowPreviewModal] = useState(false); const [previewOrderId, setPreviewOrderId] = useState(""); const [isGeneratingPreview, setIsGeneratingPreview] = useState(false); const [availableOrders, setAvailableOrders] = useState< Array<{ id: string; number: string; customer: string; date: string; total: string; }> >([]); const [isLoadingOrders, setIsLoadingOrders] = useState(false); const [activeEngine, setActiveEngine] = useState<{ name: string; icon: string; } | null>(null); // État du compte à rebours (null = inactif, 1-10 = en cours) const [countdown, setCountdown] = useState(null); // Position dans la file d'attente du service (null = inconnu ou non en file) const [queuePosition, setQueuePosition] = useState(null); // Taille courante de la file pour afficher la progression côté UI const [queueSize, setQueueSize] = useState(null); // Vrai dès qu'on entre dans la phase de file (avant countdown), faux dès que le countdown démarre const [isInQueuePhase, setIsInQueuePhase] = useState(false); // Refs pour la communication cross-closure entre le timer et le fetch const prefetchedPdfRef = useRef(null); const pdfReadyRef = useRef(false); const countdownDoneRef = useRef(false); const queuePollIntervalId = useRef | null>(null); const countdownIntervalRef = useRef | null>(null); const prefetchAbortControllerRef = useRef(null); const getAjaxUrl = () => (globalThis as any).pdfBuilderAjax?.ajax_url || (globalThis as any).pdfBuilderAjax?.url || (globalThis as any).pdfBuilderData?.ajaxUrl || "/wp-admin/admin-ajax.php"; const getAjaxNonce = () => (globalThis as any).pdfBuilderAjax?.nonce || (globalThis as any).pdfBuilderData?.nonce || (globalThis as any).pdfBuilderNonce || ""; const stopQueuePoll = () => { if (queuePollIntervalId.current !== null) { clearInterval(queuePollIntervalId.current); queuePollIntervalId.current = null; } setQueuePosition(null); setQueueSize(null); }; const leaveQueue = () => { const fd = new FormData(); fd.append("action", "pdfib_pdf_queue_leave"); fd.append("nonce", getAjaxNonce()); fetch(getAjaxUrl(), { method: "POST", body: fd }).catch(() => { /* best-effort */ }); }; const cancelQueuedPreview = () => { stopQueuePoll(); leaveQueue(); // Stopper le compte à rebours if (countdownIntervalRef.current !== null) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } // Annuler le fetch de préchargement PDF if (prefetchAbortControllerRef.current) { prefetchAbortControllerRef.current.abort(); prefetchAbortControllerRef.current = null; } prefetchedPdfRef.current = null; pdfReadyRef.current = false; countdownDoneRef.current = false; setCountdown(null); setIsInQueuePhase(false); setIsGeneratingPreview(false); }; // Charger le moteur PDF actif const loadActiveEngine = async () => { try { const response = await fetch( (globalThis as any).pdfBuilderData?.ajaxUrl || "", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ action: "pdfib_get_active_engine", nonce: getAjaxNonce(), }), }, ); if (response.ok) { const data = await response.json(); if (data.success && data.data) { setActiveEngine({ name: data.data.display_name, icon: data.data.icon, }); } } } catch (error) { debugError("Erreur lors du chargement du moteur PDF:", error); } }; // Ouvrir la modale d'aperçu const handlePreview = async () => { setShowPreviewModal(true); setPreviewOrderId(""); setIsLoadingOrders(true); // Charger le moteur actif en parallèle loadActiveEngine(); try { // Récupérer la liste des commandes WooCommerce const response = await fetch( (globalThis as any).pdfBuilderData?.ajaxUrl || "", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ action: "pdfib_get_orders_list", nonce: getAjaxNonce(), }), }, ); if (response.ok) { const data = await response.json(); if (data.success && data.data) { setAvailableOrders(data.data); } else { alert( "Erreur lors du chargement des commandes: " + (data.data?.message || "Erreur inconnue"), ); } } else { alert("Erreur réseau lors du chargement des commandes"); } } catch (error) { debugError("Erreur lors du chargement des commandes:", error); alert( "Erreur: " + (error instanceof Error ? error.message : String(error)), ); } finally { setIsLoadingOrders(false); } }; // Ouvrir le HTML avec boutons Télécharger et Imprimer const openDebugHTML = async () => { if (!previewOrderId || previewOrderId.trim() === "") { alert("Veuillez sélectionner une commande"); return; } const templateId = state.template?.id; if (!templateId) { alert("Erreur: Template ID manquant."); return; } try { // Récupérer le HTML via endpoint const formData = new FormData(); formData.append("action", "pdfib_get_preview_html"); formData.append("template_id", templateId.toString()); formData.append("order_id", previewOrderId.trim()); formData.append("nonce", getAjaxNonce()); const response = await fetch( (globalThis as any).pdfBuilderData?.ajaxUrl || "/wp-admin/admin-ajax.php", { method: "POST", body: formData, }, ); if (!response.ok) { throw new Error(`Erreur serveur ${response.status}`); } const result = await response.json(); if (!result.success) { throw new Error(result.data?.message || "Erreur inconnue"); } const { html, order_number } = result.data; // Extraire les styles du head et le contenu du body const stylesMatch = html.match(/]*>(.*?)<\/head>/is); const bodyMatch = html.match(/]*>(.*?)<\/body>/is); const originalStyles = stylesMatch ? stylesMatch[1] : ""; const bodyContent = bodyMatch ? bodyMatch[1] : html; // Créer une page HTML avec le contenu et les boutons const htmlPage = ` Facture ${order_number} - HTML ${originalStyles}
100%
${bodyContent}
`; // Ouvrir dans un blob const htmlBlob = new Blob([htmlPage], { type: "text/html" }); const htmlUrl = URL.createObjectURL(htmlBlob); globalThis.open(htmlUrl, "_blank"); setTimeout(() => URL.revokeObjectURL(htmlUrl), 2000); } catch (error) { debugError("[HTML] Erreur:", error); alert("Erreur lors de la récupération du HTML"); } }; // Générer un PDF via AJAX – avec compte à rebours pour les utilisateurs gratuits const generatePDF = async () => { if (!previewOrderId || previewOrderId.trim() === "") { alert("Veuillez sélectionner une commande"); return; } const templateId = state.template?.id; if (!templateId) { alert( "Erreur: Template ID manquant. Veuillez d'abord enregistrer le template.", ); return; } setIsGeneratingPreview(true); // ── Utilisateurs gratuits : compte à rebours + préchargement en parallèle ────── // TODO: Pendant ce compte à rebours, afficher un slider publicitaire présentant // d'autres plugins Threeaxe (ex. WooCommerce Booster, Invoice Pro…). // Prévoir 3-4 slides avec image, titre, description courte et bouton CTA. // Le PDF s'ouvre automatiquement dès que les 10 s sont écoulées ET que le // fichier est prêt (le premier des deux événements qui survient le dernier). // ───────────────────────────────────────────────────────────────────────────── if (!isPremium) { prefetchedPdfRef.current = null; pdfReadyRef.current = false; countdownDoneRef.current = false; setQueuePosition(0); // défaut #1 affiché pendant le join setQueueSize(1); setIsInQueuePhase(true); const ajaxUrl = getAjaxUrl(); const nonce = getAjaxNonce(); // Token transmis à pdfib_generate_pdf pour le suivi interne Puppeteer const requestToken = crypto.randomUUID(); const openPdf = (url: string) => { stopQueuePoll(); leaveQueue(); globalThis.open(url, "_blank"); URL.revokeObjectURL(url); setShowPreviewModal(false); setIsGeneratingPreview(false); setCountdown(null); }; // Démarre le compte à rebours (10 s) et la génération PDF en arrière-plan const startCountdownAndPrefetch = () => { setIsInQueuePhase(false); let currentCount = 10; setCountdown(currentCount); countdownIntervalRef.current = setInterval(() => { currentCount -= 1; setCountdown(currentCount); if (currentCount <= 0) { if (countdownIntervalRef.current !== null) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } countdownDoneRef.current = true; if (pdfReadyRef.current && prefetchedPdfRef.current) { openPdf(prefetchedPdfRef.current); } } }, 1000); // Préchargement du PDF en arrière-plan prefetchAbortControllerRef.current = new AbortController(); const formData = new FormData(); formData.append("action", "pdfib_generate_pdf"); formData.append("template_id", templateId.toString()); formData.append("order_id", previewOrderId.trim()); formData.append("nonce", nonce); formData.append("request_token", requestToken); fetch(ajaxUrl, { method: "POST", body: formData, signal: prefetchAbortControllerRef.current.signal }) .then(async (response) => { if (!response.ok) throw new Error("Erreur lors de la génération du PDF"); const blob = await response.blob(); prefetchedPdfRef.current = URL.createObjectURL(blob); pdfReadyRef.current = true; if (countdownDoneRef.current) { if (countdownIntervalRef.current !== null) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } openPdf(prefetchedPdfRef.current); } }) .catch((error) => { if ((error as Error).name === "AbortError") return; // Annulé par l'utilisateur if (countdownIntervalRef.current !== null) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } cancelQueuedPreview(); debugError("[PREVIEW] Erreur génération PDF:", error); alert("Erreur lors de la génération du PDF. Vérifiez la console pour plus de détails."); }); }; // ── File d'attente côté serveur (partagée avec la metabox WooCommerce) ────── try { const joinFd = new FormData(); joinFd.append("action", "pdfib_pdf_queue_join"); joinFd.append("nonce", nonce); const joinRes = await fetch(ajaxUrl, { method: "POST", body: joinFd }); const joinData = await joinRes.json(); if (!joinData?.success) { throw new Error("Impossible de rejoindre la file d'attente"); } // Toujours capturer la position retournée par le serveur (même si slot dispo) setQueuePosition(typeof joinData.data?.position === "number" ? joinData.data.position : 0); setQueueSize(typeof joinData.data?.queue_size === "number" ? joinData.data.queue_size : 1); if (joinData.data?.slot_available) { // Slot disponible immédiatement → démarrer directement startCountdownAndPrefetch(); } else { // En attente dans la file → afficher la position et poller toutes les 2 s queuePollIntervalId.current = setInterval(async () => { try { const pollFd = new FormData(); pollFd.append("action", "pdfib_pdf_queue_poll"); pollFd.append("nonce", nonce); const pollRes = await fetch(ajaxUrl, { method: "POST", body: pollFd }); if (!pollRes.ok) return; const pollData = await pollRes.json(); if (!pollData?.success) return; const pos = pollData.data?.position; const size = pollData.data?.queue_size; setQueuePosition(typeof pos === "number" ? pos : null); setQueueSize(typeof size === "number" ? size : null); if (pollData.data?.slot_available) { // Notre tour ! Arrêter le poll et démarrer stopQueuePoll(); startCountdownAndPrefetch(); } } catch { /* silencieux */ } }, 2000); } } catch (error) { cancelQueuedPreview(); debugError("[QUEUE] Erreur d'accès à la file:", error); alert("Impossible de rejoindre la file d'attente PDF."); } return; } // ── Utilisateurs premium : génération directe, pas de délai ────────────────── try { const formData = new FormData(); formData.append("action", "pdfib_generate_pdf"); formData.append("template_id", templateId.toString()); formData.append("order_id", previewOrderId.trim()); formData.append("nonce", getAjaxNonce()); const response = await fetch( (globalThis as any).pdfBuilderData?.ajaxUrl || "/wp-admin/admin-ajax.php", { method: "POST", body: formData }, ); if (!response.ok) throw new Error("Erreur lors de la génération du PDF"); const blob = await response.blob(); const url = URL.createObjectURL(blob); globalThis.open(url, "_blank"); setShowPreviewModal(false); } catch (error) { debugError("[PREVIEW] Erreur génération PDF:", error); alert("Erreur lors de la génération du PDF. Vérifiez la console pour plus de détails."); } finally { setIsGeneratingPreview(false); } }; return { showPreviewModal, setShowPreviewModal, previewOrderId, setPreviewOrderId, isGeneratingPreview, availableOrders, isLoadingOrders, activeEngine, loadActiveEngine, handlePreview, openDebugHTML, generatePDF, countdown, queuePosition, queueSize, isInQueuePhase, cancelQueuedPreview, }; } // ───────────────────────────────────────────────────────────────────────────── // ─── Scroll header hook ─────────────────────────────────────────────────────── function useHeaderScroll() { const [isHeaderFixed, setIsHeaderFixed] = useState(false); const [scrollTimeout, setScrollTimeout] = useState | null>(null); const handleScroll = useCallback(() => { if (scrollTimeout) return; setScrollTimeout( globalThis.setTimeout(() => { const scrollTop = globalThis.pageYOffset || document.documentElement.scrollTop; setIsHeaderFixed(scrollTop > 120); setScrollTimeout(null); }, 50), ); }, [scrollTimeout]); useEffect(() => { globalThis.addEventListener("scroll", handleScroll, { passive: true }); return () => globalThis.removeEventListener("scroll", handleScroll); }, [handleScroll]); return { isHeaderFixed }; } // ─── Modal drag hook ────────────────────────────────────────────────────────── function useModalDrag(jsonModalMode: "json" | "html", showJsonModal: boolean) { const [modalPosition, setModalPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [isDraggingModal, setIsDraggingModal] = useState(false); const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); useEffect(() => { if (!isDraggingModal || !dragStart) return; const handleMouseMove = (e: MouseEvent) => { setModalPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }; const handleMouseUp = () => { setIsDraggingModal(false); setDragStart(null); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDraggingModal, dragStart]); useEffect(() => { setIsDraggingModal(false); setDragStart(null); }, [jsonModalMode, showJsonModal]); return { modalPosition, isDraggingModal, setIsDraggingModal, setDragStart }; } // ─── Button state helpers ────────────────────────────────────────────────────── function getSaveButtonTitle(isLoading: boolean, isModified: boolean, isEditing: boolean): string { if (isLoading) return "Chargement du template..."; if (isModified) return isEditing ? "Modifier le template" : "Enregistrer les modifications"; return "Aucune modification"; } function getSaveButtonLabel(isSaving: boolean, isEditing: boolean): string { if (isSaving) return "Enregistrement..."; return isEditing ? "Modifier" : "Enregistrer"; } function getTemplateStatusText(isSaving: boolean, isModified: boolean): string { if (isSaving) return "Enregistrement..."; if (isModified) return "Modifié"; return "Sauvegardé"; } function computeHeaderButtonState(params: { deferredIsLoading: boolean; deferredIsModified: boolean; deferredIsSaving: boolean; deferredIsEditingExistingTemplate: boolean; isSaving: boolean; isPremium: boolean; previewOrderId: string; }) { const { deferredIsLoading, deferredIsModified, deferredIsSaving, deferredIsEditingExistingTemplate, isSaving, isPremium, previewOrderId } = params; const noOrderSelected = "Veuillez sélectionner une commande"; const premiumRequired = "Fonctionnalité premium - Activez votre licence"; let newButtonOpacity: number; if (isSaving) { newButtonOpacity = 0.6; } else { newButtonOpacity = 1; } return { saveButtonTitle: getSaveButtonTitle(deferredIsLoading, deferredIsModified, deferredIsEditingExistingTemplate), saveButtonLabel: getSaveButtonLabel(deferredIsSaving, deferredIsEditingExistingTemplate), saveButtonIcon: deferredIsSaving ? "⟳" : "💾", templateStatusText: getTemplateStatusText(deferredIsSaving, deferredIsModified), newButtonOpacity, }; } // ─── HTML preview hook ──────────────────────────────────────────────────────── function useHtmlPreview( state: ReturnType["state"], setJsonModalMode: (mode: "json" | "html") => void, ) { const [isGeneratingHtml, setIsGeneratingHtml] = useState(false); const [generatedHtml, setGeneratedHtml] = useState(""); const handleShowHtmlPreview = useCallback(async () => { setIsGeneratingHtml(true); try { const html = generatePDFSimulationHTML(state.elements, state.canvas, state.template); setGeneratedHtml(html); setJsonModalMode("html"); } catch (error) { debugError("Erreur lors de la génération HTML:", error); alert(`❌ Erreur: ${error instanceof Error ? error.message : "Erreur inconnue"}`); } finally { setIsGeneratingHtml(false); } }, [state, setJsonModalMode]); return { isGeneratingHtml, generatedHtml, handleShowHtmlPreview }; } // ─── Orientation permissions hook ────────────────────────────────────────────── function useOrientationPermissions(): void { const [, setOrientationPermissions] = useState<{ allowPortrait: boolean; allowLandscape: boolean; defaultOrientation: "portrait" | "landscape"; availableOrientations: string[]; }>({ allowPortrait: true, allowLandscape: true, defaultOrientation: "portrait", availableOrientations: ["portrait", "landscape"], }); useEffect(() => { try { const availableOrientations = (globalThis as any).availableOrientations || ["portrait", "landscape"]; setOrientationPermissions({ allowPortrait: availableOrientations.includes("portrait"), allowLandscape: availableOrientations.includes("landscape"), defaultOrientation: ((globalThis as any).pdfBuilderCanvasSettings?.default_canvas_orientation || "portrait") as "portrait" | "landscape", availableOrientations, }); } catch (error) { debugError("Erreur lors du chargement des permissions d'orientation", error); } }, []); } // ─── Performance metrics hook ──────────────────────────────────────────────── function usePerformanceMetrics(enabled: boolean): { performanceMetrics: { fps: number; memoryUsage: number; lastUpdate: number } } { const [performanceMetrics, setPerformanceMetrics] = useState({ fps: 0, memoryUsage: 0, lastUpdate: 0 }); useEffect(() => { if (!enabled) return; const updateMetrics = () => { const now = Date.now(); setPerformanceMetrics(() => ({ fps: Math.floor(Math.random() * 20) + 40, memoryUsage: Math.floor(Math.random() * 50) + 80, lastUpdate: now, })); }; const interval = setInterval(updateMetrics, 2000); updateMetrics(); return () => clearInterval(interval); }, [enabled]); return { performanceMetrics }; } // ─── Container style helper ─────────────────────────────────────────────────── function buildHeaderContainerStyle(fixed: boolean): React.CSSProperties { const padding = fixed ? "16px" : "12px"; return { display: "flex", alignItems: "center", justifyContent: "space-between", padding, paddingLeft: padding, paddingRight: padding, backgroundColor: "#ffffff", borderBottom: "2px solid #e0e0e0", borderRadius: "0px", boxShadow: fixed ? "0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1)" : "none", gap: "16px", position: fixed ? "fixed" : "relative", top: fixed ? "32px" : "auto", left: fixed ? "160px" : "auto", right: fixed ? "0" : "auto", width: fixed ? "calc(100% - 160px)" : "auto", zIndex: 1000, boxSizing: "border-box", transition: "all 0.25s ease-in-out", }; } // ─── useHeaderSave hook ─────────────────────────────────────────────────────── function useHeaderSave(params: { templateName: string; deferredIsModified: boolean; deferredIsSaving: boolean; deferredIsLoading: boolean; canvasWidth: number; canvasHeight: number; showGuides: boolean; snapToGrid: boolean; state: ReturnType["state"]; canvasSettings: ReturnType; onSave: () => void; }): () => Promise { const { templateName, deferredIsModified, deferredIsSaving, deferredIsLoading, canvasWidth, canvasHeight, showGuides, snapToGrid, state, canvasSettings, onSave, } = params; return useCallback(async () => { const startTime = performance.now(); debugLog("🚀 [PDF Builder] Bouton Enregistrer cliqué", { templateName, isModified: deferredIsModified, isSaving: deferredIsSaving, isLoading: deferredIsLoading, timestamp: new Date().toISOString(), canvasInfo: { width: canvasWidth, height: canvasHeight, showGuides, snapToGrid }, elementsInfo: { totalElements: state.elements?.length || 0, elementTypes: state.elements?.reduce((acc: Record, el) => { acc[el.type] = (acc[el.type] || 0) + 1; return acc; }, {}) || {}, }, builderState: { template: state.template ? { name: state.template.name, description: state.template.description, hasBackground: !!state.canvas.backgroundColor } : null, selectedElement: state.selection.selectedElements[0] || null, zoom: state.canvas.zoom || 1, }, canvasSettings: { guidesEnabled: canvasSettings.guidesEnabled, memoryLimit: canvasSettings.memoryLimitJs }, }); try { debugLog("⏳ [PDF Builder] Début de la sauvegarde..."); onSave(); const saveDuration = performance.now() - startTime; debugLog("✅ [PDF Builder] Sauvegarde réussie", { templateName, timestamp: new Date().toISOString(), duration: `${saveDuration.toFixed(2)}ms`, performance: { saveTime: saveDuration, elementsCount: state.elements?.length || 0, templateSize: JSON.stringify(state.template).length, elementsSize: JSON.stringify(state.elements).length }, postSaveState: { isModified: false, isSaving: false }, }); debugLog("📊 [PDF Builder] Métriques de sauvegarde", { duration: saveDuration, avgTimePerElement: state.elements?.length ? saveDuration / state.elements.length : 0, memoryUsage: (performance as any).memory ? { used: (performance as any).memory.usedJSHeapSize, total: (performance as any).memory.totalJSHeapSize, limit: (performance as any).memory.jsHeapSizeLimit } : "N/A", }); } catch (error) { const failedDuration = performance.now() - startTime; debugError("❌ [PDF Builder] Erreur lors de la sauvegarde:", { error: error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : error, templateName, timestamp: new Date().toISOString(), duration: `${failedDuration.toFixed(2)}ms`, context: { isModified: deferredIsModified, isSaving: deferredIsSaving, elementsCount: state.elements?.length || 0 }, }); alert("Erreur lors de la sauvegarde: " + (error instanceof Error ? error.message : "Erreur inconnue")); } }, [templateName, deferredIsModified, deferredIsSaving, deferredIsLoading, canvasWidth, canvasHeight, showGuides, snapToGrid, state, canvasSettings, onSave]); } // ─── SettingsModal sub-component ───────────────────────────────────────────── interface SettingsModalProps { show: boolean; onClose: () => void; originalTemplateName: string; editedTemplateName: string; setEditedTemplateName: React.Dispatch>; editedTemplateDescription: string; setEditedTemplateDescription: React.Dispatch>; editedCanvasWidth: number; editedCanvasHeight: number; isNewTemplate: boolean; deferredIsModified: boolean; isEditingExistingTemplate: boolean; templateStatusText: string; canvasSettings: ReturnType; performanceMetrics: { fps: number; memoryUsage: number }; showGuides: boolean; snapToGrid: boolean; onSaveSettings: (settings: { name: string; description: string; canvasWidth: number; canvasHeight: number; showGuides: boolean; snapToGrid: boolean }) => void; } function SettingsModal({ show, onClose, originalTemplateName, editedTemplateName, setEditedTemplateName, editedTemplateDescription, setEditedTemplateDescription, editedCanvasWidth, editedCanvasHeight, isNewTemplate, deferredIsModified, isEditingExistingTemplate, templateStatusText, canvasSettings, performanceMetrics, showGuides, snapToGrid, onSaveSettings, }: Readonly) { if (!show) return null; return (

📄 Paramètres du template

setEditedTemplateName(e.target.value)} className="pdfb-setting-input" placeholder="Entrez le nom du template" autoComplete="off" name="template_name_unique" style={{ width: "100%", padding: "8px 12px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "14px" }} />