const pptxgen = require("pptxgenjs"); const fs = require("fs"); const path = require("path"); function outerShadow() { return { type: "outer", color: "000000", opacity: 0.10, blur: 3, angle: 45, distance: 2 }; } function addTopBar(slide, colors, title) { slide.addShape("rect", { x: 0, y: 0, w: 13.333, h: 7.5, fill: { color: colors.bg } }); slide.addShape("rect", { x: 0, y: 0, w: 0.25, h: 7.5, fill: { color: colors.accent } }); slide.addText(title || "", { x: 0.55, y: 0.35, w: 12.4, h: 0.5, fontFace: "Aptos Display", fontSize: 20, bold: true, color: colors.text }); slide.addShape("line", { x: 0.55, y: 0.95, w: 12.4, h: 0, line: { color: "E5E7EB", width: 1 } }); } function addNavButton(slide, x, y, w, h, text, toSlide, colors) { slide.addShape("roundRect", { x, y, w, h, fill: { color: "FFFFFF" }, line: { color: "E5E7EB", width: 1 }, radius: 0.18, shadow: outerShadow() }); slide.addText(text, { x, y: y + 0.06, w, h, fontFace: "Aptos", fontSize: 14, bold: true, color: colors.text, align: "center", valign: "mid", hyperlink: { slide: toSlide, tooltip: "Aller à la section" } }); } function addCard(slide, x, y, w, h, title, value, note, colors) { slide.addShape("roundRect", { x, y, w, h, fill: { color: "FFFFFF" }, line: { color: "E5E7EB", width: 1 }, radius: 0.22, shadow: outerShadow() }); slide.addText(title, { x: x + 0.25, y: y + 0.18, w: w - 0.5, h: 0.3, fontFace: "Aptos", fontSize: 12, color: colors.muted }); slide.addText(value, { x: x + 0.25, y: y + 0.5, w: w - 0.5, h: 0.55, fontFace: "Aptos Display", fontSize: 24, bold: true, color: colors.text }); slide.addText(note || "", { x: x + 0.25, y: y + 1.1, w: w - 0.5, h: 0.3, fontFace: "Aptos", fontSize: 12, color: colors.accent }); } function addSwotBox(slide, x, y, w, h, label, items, headerColor, colors) { slide.addShape("roundRect", { x, y, w, h, fill: { color: "FFFFFF" }, line: { color: "E5E7EB", width: 1 }, radius: 0.18, shadow: outerShadow() }); slide.addShape("roundRect", { x, y, w, h: 0.55, fill: { color: headerColor }, line: { color: headerColor }, radius: 0.18 }); slide.addText(label, { x: x + 0.25, y: y + 0.12, w: w - 0.5, h: 0.35, fontFace: "Aptos", fontSize: 14, bold: true, color: "FFFFFF" }); const body = (items && items.length ? items : ["(à compléter)"]) .slice(0, 8) .map(t => `• ${t}`) .join("\n"); slide.addText(body, { x: x + 0.25, y: y + 0.75, w: w - 0.5, h: h - 0.9, fontFace: "Aptos", fontSize: 12, color: colors.text, valign: "top" }); } async function buildPpt(data, outFile) { const pptx = new pptxgen(); pptx.layout = "LAYOUT_WIDE"; pptx.theme = { headFontFace: "Aptos Display", bodyFontFace: "Aptos", lang: "fr-FR" }; const colors = data.brand || { accent: "2563EB", bg: "F8FAFC", text: "111827", muted: "6B7280", strength: "16A34A", weakness: "DC2626", opportunity: "2563EB", threat: "F59E0B" }; // 1) Cover let s = pptx.addSlide(); s.addShape("rect", { x: 0, y: 0, w: 13.333, h: 7.5, fill: { color: colors.bg } }); s.addShape("rect", { x: 0, y: 0, w: 0.38, h: 7.5, fill: { color: colors.accent } }); s.addText(data.meta?.title || "Analyse SWOT", { x: 0.9, y: 2.3, w: 12.0, h: 0.8, fontFace: "Aptos Display", fontSize: 38, bold: true, color: colors.text }); s.addText(data.meta?.subtitle || "Deck éditable + génération automatique", { x: 0.9, y: 3.2, w: 12.0, h: 0.5, fontFace: "Aptos", fontSize: 16, color: colors.muted }); s.addShape("roundRect", { x: 0.9, y: 4.1, w: 6.6, h: 1.2, fill: { color: "FFFFFF" }, line: { color: "E5E7EB", width: 1 }, radius: 0.22, shadow: outerShadow() }); s.addText(`${data.meta?.company || "Nom de l’organisation"}\n${data.meta?.date || ""}`, { x: 1.15, y: 4.25, w: 6.1, h: 0.9, fontFace: "Aptos", fontSize: 14, color: colors.text }); // 2) Menu s = pptx.addSlide(); addTopBar(s, colors, "Menu (boutons cliquables)"); s.addText("Automatisation visible : navigation + graphiques qui se mettent à jour via 'Modifier les données'.", { x: 0.55, y: 1.15, w: 12.3, h: 0.6, fontFace: "Aptos", fontSize: 12, color: colors.muted }); addNavButton(s, 0.8, 2.1, 3.0, 0.8, "Matrice SWOT", 3, colors); addNavButton(s, 4.0, 2.1, 3.0, 0.8, "KPI & Graphiques", 4, colors); addNavButton(s, 7.2, 2.1, 3.0, 0.8, "Stratégies TOWS", 5, colors); addNavButton(s, 10.4, 2.1, 2.6, 0.8, "Roadmap", 6, colors); // 3) SWOT s = pptx.addSlide(); addTopBar(s, colors, "Matrice SWOT (Top 8)"); s.addText("Chaque bloc est une zone de texte éditable.", { x: 0.55, y: 1.15, w: 12.4, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.muted }); const bx = 0.7, by = 1.7, bw = 6.1, bh = 2.55, gap = 0.35; addSwotBox(s, bx, by, bw, bh, "FORCES", data.swot?.Forces, colors.strength, colors); addSwotBox(s, bx + bw + gap, by, bw, bh, "FAIBLESSES", data.swot?.Faiblesses, colors.weakness, colors); addSwotBox(s, bx, by + bh + gap, bw, bh, "OPPORTUNITÉS", data.swot?.Opportunités, colors.opportunity, colors); addSwotBox(s, bx + bw + gap, by + bh + gap, bw, bh, "MENACES", data.swot?.Menaces, colors.threat, colors); s.addText("⬅ Menu", { x: 11.6, y: 0.35, w: 1.7, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.accent, hyperlink: { slide: 2 } }); // 4) KPI + chart s = pptx.addSlide(); addTopBar(s, colors, "KPI & Graphiques (auto)"); s.addText("Clic droit sur le graphique → Modifier les données (mise à jour automatique).", { x: 0.55, y: 1.15, w: 12.4, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.muted }); const kx = 0.7, ky = 1.65, cw = 3.05, ch = 1.55, cg = 0.25; (data.kpi || []).slice(0, 4).forEach((k, i) => addCard(s, kx + i * (cw + cg), ky, cw, ch, k.label, k.value, k.note, colors)); const labels = data.chart?.labels || ["Forces", "Faiblesses", "Opportunités", "Menaces"]; const vals = data.chart?.values || [60, 40, 55, 50]; s.addChart(pptx.ChartType.bar, [{ name: "Score", labels, values: vals }], { x: 0.7, y: 3.45, w: 12.6, h: 3.7, chartColors: [colors.accent], dataLabelPosition: "outEnd", showLegend: false, valAxisLabelFontSize: 10, catAxisLabelFontSize: 10 }); s.addText("⬅ Menu", { x: 11.6, y: 0.35, w: 1.7, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.accent, hyperlink: { slide: 2 } }); // 5) TOWS s = pptx.addSlide(); addTopBar(s, colors, "Stratégies TOWS"); s.addText("Zones éditables. Dupliquez une ligne pour créer plusieurs stratégies.", { x: 0.55, y: 1.15, w: 12.4, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.muted }); const rows = [ ["SO", "Forces × Opportunités", data.tows?.SO, colors.strength], ["ST", "Forces × Menaces", data.tows?.ST, colors.threat], ["WO", "Faiblesses × Opportunités", data.tows?.WO, colors.opportunity], ["WT", "Faiblesses × Menaces", data.tows?.WT, colors.weakness], ]; const tx = 0.7, tw = 12.6, th = 1.35, tgap = 0.25; rows.forEach((r, idx) => { const y = 1.75 + idx * (th + tgap); s.addShape("roundRect", { x: tx, y, w: tw, h: th, fill: { color: "FFFFFF" }, line: { color: "E5E7EB", width: 1 }, radius: 0.18 }); s.addShape("roundRect", { x: tx, y, w: 0.9, h: th, fill: { color: r[3] }, line: { color: r[3] }, radius: 0.18 }); s.addText(r[0], { x: tx, y: y + 0.35, w: 0.9, h: 0.6, fontFace: "Aptos Display", fontSize: 18, bold: true, color: "FFFFFF", align: "center" }); s.addText(r[1], { x: tx + 1.1, y: y + 0.15, w: tw - 1.2, h: 0.35, fontFace: "Aptos", fontSize: 12, bold: true, color: colors.text }); s.addText(r[2] || "(à compléter)", { x: tx + 1.1, y: y + 0.55, w: tw - 1.2, h: 0.75, fontFace: "Aptos", fontSize: 12, color: colors.text, valign: "top" }); }); s.addText("⬅ Menu", { x: 11.6, y: 0.35, w: 1.7, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.accent, hyperlink: { slide: 2 } }); // 6) Roadmap s = pptx.addSlide(); addTopBar(s, colors, "Roadmap"); s.addText("Colonnes éditables (dupliquez selon vos horizons).", { x: 0.55, y: 1.15, w: 12.4, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.muted }); const phases = (data.roadmap || []).slice(0, 3); const rx = 0.7, ry = 1.7, rw = 12.6, colGap = 0.35; const colW = (rw - colGap * (phases.length - 1)) / phases.length; phases.forEach((p, i) => { const x = rx + i * (colW + colGap); s.addShape("roundRect", { x, y: ry, w: colW, h: 5.45, fill: { color: "FFFFFF" }, line: { color: "E5E7EB", width: 1 }, radius: 0.18, shadow: outerShadow() }); s.addShape("roundRect", { x, y: ry, w: colW, h: 0.6, fill: { color: colors.accent }, line: { color: colors.accent }, radius: 0.18 }); s.addText(p.phase, { x: x + 0.2, y: ry + 0.15, w: colW - 0.4, h: 0.35, fontFace: "Aptos", fontSize: 14, bold: true, color: "FFFFFF" }); const body = (p.items || []).map(t => `• ${t}`).join("\n"); s.addText(body || "(à compléter)", { x: x + 0.25, y: ry + 0.85, w: colW - 0.5, h: 4.45, fontFace: "Aptos", fontSize: 12, color: colors.text, valign: "top" }); }); s.addText("⬅ Menu", { x: 11.6, y: 0.35, w: 1.7, h: 0.4, fontFace: "Aptos", fontSize: 12, color: colors.accent, hyperlink: { slide: 2 } }); // Footer pptx._slides.forEach((sl, idx) => { sl.addText(`${idx + 1} / ${pptx._slides.length}`, { x: 12.2, y: 7.1, w: 1.0, h: 0.3, fontFace: "Aptos", fontSize: 10, color: colors.muted, align: "right" }); }); await pptx.writeFile({ fileName: outFile }); } async function main() { const argv = process.argv.slice(2); const jsonFile = argv[0] || path.join(__dirname, "swot_case.json"); const outFile = argv[1] || path.join(__dirname, "Deck_SWOT.pptx"); if (!fs.existsSync(jsonFile)) { console.error("JSON introuvable :", jsonFile); process.exit(1); } const data = JSON.parse(fs.readFileSync(jsonFile, "utf-8")); await buildPpt(data, outFile); console.log("OK ->", outFile); } main().catch(e => { console.error(e); process.exit(1); });