// ══════════════════════════════════════════════════════════════════════════════ // ndfPdfGenerator.js — v5 (pdfkit pur, support proratisation repas) // ══════════════════════════════════════════════════════════════════════════════ import PDFDocument from 'pdfkit'; const C = { blue: '#1B4F8A', header: '#2563EB', totalBg: '#DBEAFE', altRow: '#EFF6FF', amountBg: '#EEF2FF', greenBg: '#F0FDF4', greenText: '#15803D', headerRow: '#E2E8F0', border: '#CBD5E1', dark: '#0F172A', grey: '#64748B', light: '#94A3B8', white: '#FFFFFF', collabBg: '#F0FDF4', collabBorder: '#A7F3D0', collabText: '#059669', validBg: '#EEF2FF', validBorder: '#C7D2FE', validText: '#4F46E5', refusBg: '#FEF2F2', refusBorder: '#FCA5A5', refusText: '#DC2626', waitBg: '#F8FAFC', waitBorder: '#E2E8F0', kmBg: '#F5F3FF', kmBorder: '#DDD6FE', kmText: '#7C3AED', kmTotalBg: '#EDE9FE', // ✅ Nouveaux — lignes proratisées prorataBg: '#FFFBEB', prorataText: '#D97706', prorataBorder: '#FDE68A', }; const COLS = [ { key: 'num', label: 'N°pièce', w: 34, align: 'center' }, { key: 'date', label: 'Date', w: 58, align: 'left' }, { key: 'nature', label: 'Nature', w: 70, align: 'left' }, { key: 'lib', label: 'Libellé', w: 140, align: 'left' }, { key: 'km', label: 'Km', w: 36, align: 'right' }, { key: 'tarifKm', label: 'Tarif €/km', w: 46, align: 'right' }, { key: 'sousKm', label: 'S/Total Km', w: 50, align: 'right' }, { key: 'ttc', label: 'TTC', w: 50, align: 'right' }, { key: 'tva21', label: 'TVA 2,1%', w: 46, align: 'right' }, { key: 'tva55', label: 'TVA 5,5%', w: 46, align: 'right' }, { key: 'tva10', label: 'TVA 10%', w: 46, align: 'right' }, { key: 'tva20', label: 'TVA 20%', w: 46, align: 'right' }, { key: 'ht', label: 'HT', w: 50, align: 'right' }, ]; const KM_COLS = ['km', 'tarifKm', 'sousKm']; const MARGIN = 30; const ROW_H = 16; const HEAD_H = 20; const PAGE_W = 841.89; const PAGE_H = 595.28; // ───────────────────────────────────────────────────────────────────────────── // HELPERS // ───────────────────────────────────────────────────────────────────────────── const f2 = v => (parseFloat(v) || 0).toFixed(2); const f3 = v => (parseFloat(v) || 0).toFixed(3); function fmtDate(s) { if (!s) return ''; try { const d = new Date(s); if (isNaN(d)) return String(s).slice(0, 10); return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`; } catch { return ''; } } function fmtDateTime(s) { if (!s) return ''; try { const d = new Date(s); if (isNaN(d)) return String(s).slice(0, 16); return d.toLocaleString('fr-FR', { timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return ''; } } function drawRect(doc, x, y, w, h, fill, stroke = null, lw = 0.3) { doc.save(); if (stroke) doc.lineWidth(lw).rect(x, y, w, h).fillAndStroke(fill, stroke); else doc.rect(x, y, w, h).fill(fill); doc.restore(); } function drawCellText(doc, text, x, y, w, h, font, size, color, align, padX = 3) { text = String(text ?? ''); const maxW = w - padX * 2; doc.save().font(font).fontSize(size).fillColor(color); while (text.length > 1 && doc.widthOfString(text) > maxW) text = text.slice(0, -1); const ty = y + h * 0.28; if (align === 'right') doc.text(text, x + padX, ty, { width: maxW, align: 'right', lineBreak: false }); else if (align === 'center') doc.text(text, x + padX, ty, { width: maxW, align: 'center', lineBreak: false }); else doc.text(text, x + padX, ty, { width: maxW, align: 'left', lineBreak: false }); doc.restore(); } function drawVLine(doc, x, y1, y2) { doc.save().strokeColor(C.border).lineWidth(0.4).moveTo(x, y1).lineTo(x, y2).stroke().restore(); } function drawHLine(doc, x1, x2, y) { doc.save().strokeColor(C.border).lineWidth(0.3).moveTo(x1, y).lineTo(x2, y).stroke().restore(); } // ───────────────────────────────────────────────────────────────────────────── // BARÈME KM fiscal (miroir de server.js) // ───────────────────────────────────────────────────────────────────────────── const BAREME_KM = { 3: { t1: 0.529, t2_a: 0.316, t2_b: 1061, t3: 0.369 }, 4: { t1: 0.606, t2_a: 0.340, t2_b: 1330, t3: 0.408 }, 5: { t1: 0.636, t2_a: 0.356, t2_b: 1391, t3: 0.427 }, 6: { t1: 0.665, t2_a: 0.374, t2_b: 1457, t3: 0.448 }, 7: { t1: 0.697, t2_a: 0.394, t2_b: 1512, t3: 0.470 }, }; function getIndemniteKm(kmTotal, chevaux) { const cv = Math.min(Math.max(parseInt(chevaux) || 7, 3), 7); const b = BAREME_KM[cv]; if (!b || kmTotal <= 0) return 0; if (kmTotal <= 5000) return parseFloat((kmTotal * b.t1).toFixed(2)); if (kmTotal <= 20000) return parseFloat((kmTotal * b.t2_a + b.t2_b).toFixed(2)); return parseFloat((kmTotal * b.t3).toFixed(2)); } // ───────────────────────────────────────────────────────────────────────────── // preparerLignesPDF — v5 : gère montantAjuste + barème CV fiscal // ───────────────────────────────────────────────────────────────────────────── export function preparerLignesPDF(lignesParsed, tarifKm = 0.697) { console.log('🔍 preparerLignesPDF reçoit:', JSON.stringify(lignesParsed, null, 2)); return (lignesParsed || []).map((l, idx) => { const isKm = (l.categorie || '').toLowerCase().includes('kilom'); const km = parseFloat(l.km) || 0; const cv = parseInt(l.chevaux) || 7; const indemniteKm = isKm ? (() => { const b = BAREME_KM[cv]; if (!b || km <= 0) return 0; if (km <= 5000) return parseFloat((km * b.t1).toFixed(2)); if (km <= 20000) return parseFloat((km * b.t2_a + b.t2_b).toFixed(2)); return parseFloat((km * b.t3).toFixed(2)); })() : 0; const tarifKmAffiche = isKm ? (km > 0 ? parseFloat((indemniteKm / km).toFixed(3)) : tarifKm) : 0; const montantAjuste = l.montantAjuste === true; const montantOriginal = montantAjuste ? (parseFloat(l.montantOriginal) || 0) : 0; // ✅ Toutes les variables avec let let ttc = 0, tva21 = 0, tva55 = 0, tva10 = 0, tva20 = 0, ht = 0; if (!isKm) { if (montantAjuste) { // ✅ Ligne proratisée — recalcul depuis montant retenu ttc = parseFloat(l.montant) || 0; const taux = parseFloat(l.tauxTVA) || 0; if (taux > 0 && ttc > 0) { ht = parseFloat((ttc / (1 + taux / 100)).toFixed(2)); const tvaM = parseFloat((ttc - ht).toFixed(2)); if (Math.abs(taux - 2.1) < 0.01) tva21 = tvaM; else if (Math.abs(taux - 5.5) < 0.01) tva55 = tvaM; else if (Math.abs(taux - 10 ) < 0.01) tva10 = tvaM; else if (Math.abs(taux - 20 ) < 0.01) tva20 = tvaM; } else { ht = ttc; } } else if (Array.isArray(l.tvaItems) && l.tvaItems.length > 0) { // ✅ Multi-TVA normal for (const item of l.tvaItems) { const itemTTC = parseFloat(item.montantTTC) || 0; const itemHT = parseFloat(item.montantHT) || 0; const itemTau = parseFloat(item.taux) || 0; ttc += itemTTC; ht += itemHT; const tvaM = parseFloat((itemTTC - itemHT).toFixed(2)); if (Math.abs(itemTau - 2.1) < 0.01) tva21 += tvaM; else if (Math.abs(itemTau - 5.5) < 0.01) tva55 += tvaM; else if (Math.abs(itemTau - 10 ) < 0.01) tva10 += tvaM; else if (Math.abs(itemTau - 20 ) < 0.01) tva20 += tvaM; } ttc = parseFloat(ttc.toFixed(2)); ht = parseFloat(ht.toFixed(2)); } else { // ✅ Mono-TVA simple ttc = parseFloat(l.montant) || 0; const taux = parseFloat(l.tauxTVA) || 0; if (taux > 0 && ttc > 0) { ht = parseFloat((ttc / (1 + taux / 100)).toFixed(2)); const tvaM = parseFloat((ttc - ht).toFixed(2)); if (Math.abs(taux - 2.1) < 0.01) tva21 = tvaM; else if (Math.abs(taux - 5.5) < 0.01) tva55 = tvaM; else if (Math.abs(taux - 10 ) < 0.01) tva10 = tvaM; else if (Math.abs(taux - 20 ) < 0.01) tva20 = tvaM; } else { ht = ttc; } } } return { numPiece: idx + 1, date: l.date, nature: l.categorie || '', libelle: l.libelle || '', km: isKm ? km : 0, tarifKmVal: isKm ? tarifKmAffiche : 0, montantTTC: isKm ? 0 : ttc, tva21, tva55, tva10, tva20, montantHT: isKm ? 0 : ht, indemniteKm, montantAjuste, montantOriginal, }; }); } // ───────────────────────────────────────────────────────────────────────────── // generateFicheSignee // ───────────────────────────────────────────────────────────────────────────── export async function generateFicheSignee(note, signatures = []) { console.log('🔍 generateFicheSignee montantServeur reçu =', note.montant); const tarifKm = parseFloat(note.tarifKm) || 0.697; let lignesPDF = []; if (note.lignesJson) { try { const parsed = typeof note.lignesJson === 'string' ? JSON.parse(note.lignesJson) : note.lignesJson; lignesPDF = preparerLignesPDF(parsed, tarifKm); } catch (e) { console.error('ndfPdfGenerator — Parse lignesJson:', e.message); } } else if (note.lignes && Array.isArray(note.lignes)) { lignesPDF = preparerLignesPDF(note.lignes, tarifKm); } else { // Fallback ligne unique (rétrocompat) const isKm = !!(note.km && parseFloat(note.km) > 0); const km = isKm ? parseFloat(note.km) : 0; const cv = parseInt(note.chevaux) || 7; const indem = isKm ? getIndemniteKm(km, cv) : 0; lignesPDF = [{ numPiece: 1, date: note.date, nature: note.categorie || '', libelle: note.libelle || '', km: isKm ? km : 0, tarifKmVal: isKm && km > 0 ? parseFloat((indem / km).toFixed(3)) : tarifKm, montantTTC: isKm ? 0 : parseFloat(note.montant || 0), tva21: 0, tva55: 0, tva10: 0, tva20: 0, montantHT: isKm ? 0 : parseFloat(note.montantHT || note.montant || 0), indemniteKm: indem, montantAjuste: false, montantOriginal: 0, }]; } let mois = note.mois || ''; if (!mois && note.date) { const d = new Date(note.date); const m = d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); mois = m.charAt(0).toUpperCase() + m.slice(1); } return _buildPDF({ reference: note.reference || '', nomPrenom: note.nomPrenom || note.collaborateur || '', mois, departement: note.departement || '', lignes: lignesPDF, tarifKm, signatures, statut: note.statut || 'enattente', montantServeur: note.montant ? parseFloat(note.montant) : null, }); } // ───────────────────────────────────────────────────────────────────────────── // _buildPDF // ───────────────────────────────────────────────────────────────────────────── function _buildPDF({ reference, nomPrenom, mois, departement, lignes, tarifKm, signatures, montantServeur }) { return new Promise((resolve, reject) => { const doc = new PDFDocument({ size: 'A4', layout: 'landscape', margin: 0, info: { Title: `Note de Frais ${reference}`, Author: `ENSUP — ${nomPrenom}`, Subject: `NDF ${reference}`, Creator: 'NDF ENSUP v5', }, }); const chunks = []; doc.on('data', c => chunks.push(c)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', e => reject(e)); // Positions X colonnes const colX = {}; let cx = MARGIN; for (const col of COLS) { colX[col.key] = cx; cx += col.w; } const tableW = cx - MARGIN; // ── Bandeau titre ──────────────────────────────────────────── const bandY = 44; drawRect(doc, MARGIN, bandY, tableW, 18, C.header); doc.font('Helvetica-Bold').fontSize(11).fillColor(C.white) .text('NOTE DE FRAIS', MARGIN, bandY + 3.5, { width: tableW, align: 'center', lineBreak: false }); // ── Infos collaborateur ────────────────────────────────────── const infoY = bandY + 23; doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark) .text(`NOM : ${nomPrenom}`, MARGIN, infoY, { lineBreak: false }); if (departement) doc.font('Helvetica').fontSize(8).fillColor(C.grey) .text(`Service : ${departement}`, MARGIN + 200, infoY, { lineBreak: false }); doc.font('Helvetica').fontSize(8).fillColor(C.grey) .text('Repas, déplacement, autres', MARGIN + 380, infoY, { lineBreak: false }); doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark) .text(`Mois : ${mois}`, MARGIN, infoY + 13, { lineBreak: false }); // ── En-tête colonnes ───────────────────────────────────────── const tableTop = infoY + 27; drawRect(doc, MARGIN, tableTop, tableW, HEAD_H, C.headerRow, C.border, 0.5); for (const key of KM_COLS) drawRect(doc, colX[key], tableTop, COLS.find(c => c.key === key).w, HEAD_H, C.kmBg); for (const col of COLS) { const isKmCol = KM_COLS.includes(col.key); drawCellText(doc, col.label, colX[col.key], tableTop, col.w, HEAD_H, 'Helvetica-Bold', isKmCol ? 6.5 : 7, isKmCol ? C.kmText : C.dark, col.align); drawVLine(doc, colX[col.key], tableTop, tableTop + HEAD_H); } drawVLine(doc, MARGIN + tableW, tableTop, tableTop + HEAD_H); drawHLine(doc, MARGIN, MARGIN + tableW, tableTop); drawHLine(doc, MARGIN, MARGIN + tableW, tableTop + HEAD_H); // ── Lignes données ─────────────────────────────────────────── const MIN_ROWS = 18; const totalRows = Math.max(MIN_ROWS, lignes.length); let y = tableTop + HEAD_H; let totKm = 0, totTTC = 0, totT21 = 0, totT55 = 0, totT10 = 0, totT20 = 0, totHT = 0, totSousKm = 0; for (let i = 0; i < totalRows; i++) { const lig = lignes[i] || null; const isProrata = lig?.montantAjuste === true; // Fond : prorata = amber pâle, sinon alternance const rowBg = isProrata ? C.prorataBg : (i % 2 === 1 ? C.altRow : C.white); drawRect(doc, MARGIN, y, tableW, ROW_H, rowBg); // Fond violet km for (const key of KM_COLS) { const col = COLS.find(c => c.key === key); drawRect(doc, colX[key], y, col.w, ROW_H, i % 2 === 1 ? '#F3F0FF' : '#FAF8FF'); } if (lig) { const km = parseFloat(lig.km) || 0; const tarif = parseFloat(lig.tarifKmVal) || 0; const sousKm = parseFloat(lig.indemniteKm) || 0; const ttc = parseFloat(lig.montantTTC) || 0; const t21 = parseFloat(lig.tva21) || 0; const t55 = parseFloat(lig.tva55) || 0; const t10 = parseFloat(lig.tva10) || 0; const t20 = parseFloat(lig.tva20) || 0; const ht = parseFloat(lig.montantHT) || 0; totKm += km; totTTC += ttc; totT21 += t21; totT55 += t55; totT10 += t10; totT20 += t20; totHT += ht; totSousKm += sousKm; // ✅ Libellé enrichi si proratisé : afficher montant original barré let libelleAffiche = lig.libelle || ''; const r = { num: String(lig.numPiece || i + 1), date: fmtDate(lig.date), nature: lig.nature || '', lib: libelleAffiche, km: km > 0 ? f2(km) : '', tarifKm: tarif > 0 ? f3(tarif) : '', sousKm: sousKm > 0 ? f2(sousKm) + ' €' : '', ttc: f2(ttc), tva21: f2(t21), tva55: f2(t55), tva10: f2(t10), tva20: f2(t20), ht: f2(ht), }; for (const col of COLS) { const isKmCol = KM_COLS.includes(col.key); // ✅ Couleur ambre pour montants proratisés const textColor = isProrata && ['ttc', 'ht'].includes(col.key) ? C.prorataText : isKmCol ? C.kmText : C.dark; drawCellText(doc, r[col.key], colX[col.key], y, col.w, ROW_H, 'Helvetica', 7, textColor, col.align); } // ✅ Indicateur proratisation — petit triangle orange en coin haut-gauche if (isProrata) { doc.save() .fillColor(C.prorataText) .moveTo(MARGIN, y) .lineTo(MARGIN + 6, y) .lineTo(MARGIN, y + 6) .fill() .restore(); } } else { // Ligne vide — zéros en gris for (const col of COLS) { if (['ttc', 'tva21', 'tva55', 'tva10', 'tva20', 'ht'].includes(col.key)) drawCellText(doc, '0.00', colX[col.key], y, col.w, ROW_H, 'Helvetica', 7, C.border, 'right'); } } drawHLine(doc, MARGIN, MARGIN + tableW, y + ROW_H); for (const col of COLS) drawVLine(doc, colX[col.key], y, y + ROW_H); drawVLine(doc, MARGIN + tableW, y, y + ROW_H); y += ROW_H; } // ── Ligne Total ────────────────────────────────────────────── const totalY = y; drawRect(doc, MARGIN, totalY, tableW, ROW_H + 2, C.totalBg, C.border, 0.5); for (const key of KM_COLS) { const col = COLS.find(c => c.key === key); drawRect(doc, colX[key], totalY, col.w, ROW_H + 2, C.kmTotalBg); } doc.font('Helvetica-Bold').fontSize(8).fillColor(C.dark) .text('Total', MARGIN + 3, totalY + 4, { lineBreak: false }); const totMap = { km: totKm > 0 ? f2(totKm) : '', tarifKm: '', sousKm: totSousKm > 0 ? f2(totSousKm) + ' €' : '', ttc: f2(totTTC), tva21: f2(totT21), tva55: f2(totT55), tva10: f2(totT10), tva20: f2(totT20), ht: f2(totHT), }; for (const col of COLS) { if (!totMap[col.key] && totMap[col.key] !== '0.00') continue; if (totMap[col.key] === '') continue; const isKmCol = KM_COLS.includes(col.key); drawCellText(doc, totMap[col.key], colX[col.key], totalY, col.w, ROW_H + 2, 'Helvetica-Bold', 8, isKmCol ? C.kmText : C.dark, 'right'); } // ── Zone bas ───────────────────────────────────────────────── const footY = totalY + ROW_H + 12; const montantR = parseFloat((totTTC + totSousKm).toFixed(2)); const bw = 64; // ✅ Légende proratisation si au moins une ligne ajustée const hasProrata = lignes.some(l => l?.montantAjuste === true); if (hasProrata) { const nbProrata = lignes.filter(l => l?.montantAjuste === true).length; const montantOriginalTotal = lignes .filter(l => l?.montantAjuste === true) .reduce((s, l) => s + (parseFloat(l.montantOriginal) || 0), 0); const economie = parseFloat((montantOriginalTotal - lignes .filter(l => l?.montantAjuste === true) .reduce((s, l) => s + (parseFloat(l.montantTTC) || 0), 0)).toFixed(2)); drawRect(doc, MARGIN, footY - 1, tableW * 0.6, 14, C.prorataBg, C.prorataBorder, 0.5); doc.font('Helvetica').fontSize(6.5).fillColor(C.prorataText) .text( `⚠ ${nbProrata} ligne${nbProrata > 1 ? 's' : ''} de repas plafonnée${nbProrata > 1 ? 's' : ''} à 25 €/pers. par la Finance — ` + `Montant soumis : ${f2(montantOriginalTotal)} € → Retenu : ${f2(montantOriginalTotal - economie)} €`, MARGIN + 3, footY + 1.5, { lineBreak: false } ); } const labelOffsetY = hasProrata ? 16 : 0; // APRÈS doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark) .text('Montant total à rembourser', MARGIN, footY + 4 + labelOffsetY, { lineBreak: false }); // Détail calcul km + dépenses doc.font('Helvetica').fontSize(7.5).fillColor(C.grey) .text( totSousKm > 0 && totTTC > 0 ? `${f2(totSousKm)} € (km) + ${f2(totTTC)} € (dépenses) =` : totSousKm > 0 ? `${f2(totSousKm)} € (indemnités kilométriques) =` : `${f2(totTTC)} € (dépenses) =`, MARGIN, footY + 17 + labelOffsetY, { lineBreak: false } ); drawRect(doc, MARGIN + 220, footY + 13 + labelOffsetY, bw + 10, 18, C.amountBg, C.border, 0.5); doc.font('Helvetica-Bold').fontSize(11).fillColor(C.blue) .text(f2(montantR) + ' €', MARGIN + 222, footY + 16.5 + labelOffsetY, { width: bw + 6, align: 'right', lineBreak: false }); doc.font('Helvetica').fontSize(6.5).fillColor(C.grey) .text( ` Sous-total km : ${f2(totSousKm)} € — TTC hors km : ${f2(totTTC)} € — Total : ${f2(totSousKm)} € + ${f2(totTTC)} € = ${f2(montantR)} €`, MARGIN, footY + 34 + labelOffsetY, { lineBreak: false } ); // ── Signatures ──────────────────────────────────────────────── const sigStartX = MARGIN + 310; const sigW = (tableW - 313) / 2 - 4; const sigH = 52; const sigY = footY - 2 + labelOffsetY; const sigCollab = signatures.find(s => s.niveau === 'COLLAB'); const sigManager = signatures.find(s => ['N1', 'N2', 'VERIF'].includes(s.niveau)); // ✅ Afficher toutes les signatures (jusqu'à 3 : COLLAB, N1/N2, VERIF) const sigsAffichees = ['COLLAB', 'N1', 'N2', 'VERIF'] .map(niv => signatures.find(s => s.niveau === niv)) .filter(Boolean) .slice(0, 3); const nbSigs = sigsAffichees.length; const sigWAdj = nbSigs > 2 ? (tableW - 313) / 3 - 4 : sigW; sigsAffichees.forEach((sig, idx) => { const label = sig.niveau === 'COLLAB' ? 'Date et signature Collaborateur' : sig.niveau === 'VERIF' ? 'Vérification Finance' : `Date et signature Validateur ${sig.niveau}`; _drawSigBox(doc, sig, sigStartX + idx * (sigWAdj + 4), sigY, sigWAdj, sigH, label, sig.niveau !== 'COLLAB'); }); // ── Pied de page ────────────────────────────────────────────── doc.font('Helvetica').fontSize(6).fillColor(C.light) .text( `Réf. ${reference} — Généré le ${new Date().toLocaleDateString('fr-FR')} — NDF ENSUP Groupe — Document électronique`, MARGIN, PAGE_H - 13, { width: tableW, align: 'center', lineBreak: false } ); doc.end(); }); } // ───────────────────────────────────────────────────────────────────────────── // _drawSigBox // ───────────────────────────────────────────────────────────────────────────── function _drawSigBox(doc, sig, x, y, w, h, label, isManager) { let bg, border, accent, icon; if (sig) { const a = sig.action || ''; if (a === 'refuser' || a === 'refuse') { bg = C.refusBg; border = C.refusBorder; accent = C.refusText; icon = '✗ REFUSÉ'; } else if (sig.niveau === 'VERIF') { bg = '#FEFCE8'; border = '#FDE047'; accent = '#CA8A04'; icon = '✓ VÉRIFIÉ Finance'; } else if (isManager) { bg = C.validBg; border = C.validBorder; accent = C.validText; icon = '✓ VALIDÉ'; } else { bg = C.collabBg; border = C.collabBorder; accent = C.collabText; icon = '✓ SOUMIS'; } } else { bg = C.waitBg; border = C.waitBorder; accent = C.grey; icon = null; } drawRect(doc, x, y, w, h, bg, border, 1); doc.font('Helvetica-Bold').fontSize(6.5).fillColor(C.grey) .text(label, x + 4, y + 4, { width: w - 8, lineBreak: false }); if (sig) { let nom = String(sig.nomPrenom || ''); const ds = fmtDateTime(sig.date); const comment = String(sig.commentaire || ''); doc.font('Helvetica-Bold').fontSize(8).fillColor(accent) .text(icon, x + 4, y + 14, { lineBreak: false }); doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark); while (nom.length > 1 && doc.widthOfString(nom) > w - 10) nom = nom.slice(0, -1); doc.text(nom, x + 4, y + 25, { width: w - 8, lineBreak: false }); doc.font('Helvetica').fontSize(7).fillColor(C.grey) .text(`Le ${ds}`, x + 4, y + 36, { width: w - 8, lineBreak: false }); if (comment) doc.font('Helvetica-Oblique').fontSize(6.5).fillColor(C.grey) .text(comment, x + 4, y + 45, { width: w - 8, lineBreak: false }); doc.save().strokeColor(border).lineWidth(0.5) .moveTo(x + 3, y + h - 9).lineTo(x + w - 3, y + h - 9).stroke().restore(); doc.font('Helvetica').fontSize(6).fillColor(C.grey) .text('Signature — NDF ENSUP', x + 4, y + h - 7, { width: w - 8, align: 'center', lineBreak: false }); } else { doc.font('Helvetica').fontSize(8).fillColor(C.grey) .text('En attente de signature', x + 4, y + h / 2 - 5, { width: w - 8, align: 'center', lineBreak: false }); } }