const pptxgen = require("pptxgenjs"); const React = require("react"); const ReactDOMServer = require("react-dom/server"); const sharp = require("sharp"); // Icons from react-icons const { FaCalendarAlt, FaUsers, FaSms, FaMagic, FaShoppingCart, FaUber, FaMobileAlt, FaQrcode, FaRoute, FaCogs, FaChartLine, FaLock, FaServer, FaDatabase, FaDocker, FaCloudDownloadAlt, FaMapMarkerAlt, FaClock, FaBell, FaCheckCircle, FaTags, FaNetworkWired, FaToolbox, FaUserShield, FaPaperPlane } = require("react-icons/fa"); const { MdScheduleSend, MdDragIndicator, MdOutlineSmartphone, MdOutlineAssignment, MdOutlineQrCodeScanner } = require("react-icons/md"); function renderIconSvg(IconComponent, color = "#FFFFFF", size = 256) { return ReactDOMServer.renderToStaticMarkup( React.createElement(IconComponent, { color, size: String(size) }) ); } async function iconToBase64Png(IconComponent, color, size = 256) { const svg = renderIconSvg(IconComponent, color, size); const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer(); return "image/png;base64," + pngBuffer.toString("base64"); } // Color palette — Gigafibre purple theme const C = { darkBg: "0D0F18", cardBg: "181C2E", purple: "5C59A8", purpleDk: "3F3D7A", accent: "818CF8", green: "22C55E", amber: "F59E0B", red: "EF4444", white: "FFFFFF", text: "E2E4EF", muted: "7B80A0", lightBg: "F5F7FA", indigo: "6366F1", }; const mkShadow = () => ({ type: "outer", blur: 8, offset: 3, angle: 135, color: "000000", opacity: 0.3 }); async function build() { const pres = new pptxgen(); pres.layout = "LAYOUT_16x9"; pres.author = "Targo"; pres.title = "Gigafibre FSM — Plateforme Operations"; // Pre-render icons const icons = {}; const iconMap = { calendar: FaCalendarAlt, users: FaUsers, sms: FaSms, magic: FaMagic, cart: FaShoppingCart, uber: FaRoute, mobile: FaMobileAlt, qr: FaQrcode, route: FaRoute, cogs: FaCogs, chart: FaChartLine, lock: FaLock, server: FaServer, db: FaDatabase, docker: FaDocker, cloud: FaCloudDownloadAlt, marker: FaMapMarkerAlt, clock: FaClock, bell: FaBell, check: FaCheckCircle, tags: FaTags, network: FaNetworkWired, toolbox: FaToolbox, shield: FaUserShield, send: FaPaperPlane, }; for (const [k, v] of Object.entries(iconMap)) { icons[k] = await iconToBase64Png(v, "#" + C.white, 256); icons[k + "_purple"] = await iconToBase64Png(v, "#" + C.accent, 256); icons[k + "_dark"] = await iconToBase64Png(v, "#" + C.purpleDk, 256); } // ═══════════════════════════════════════ // SLIDE 1 — Title // ═══════════════════════════════════════ let s = pres.addSlide(); s.background = { color: C.darkBg }; // Purple accent bar top s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); // Logo area s.addShape(pres.shapes.RECTANGLE, { x: 0.8, y: 1.2, w: 0.12, h: 1.8, fill: { color: C.indigo } }); s.addText("GIGAFIBRE", { x: 1.15, y: 1.2, w: 8, h: 0.7, fontSize: 42, fontFace: "Arial Black", color: C.white, bold: true, margin: 0 }); s.addText("FSM", { x: 1.15, y: 1.85, w: 8, h: 0.6, fontSize: 36, fontFace: "Arial Black", color: C.accent, margin: 0 }); s.addText("Plateforme de gestion des operations terrain", { x: 1.15, y: 2.7, w: 7, h: 0.5, fontSize: 18, fontFace: "Calibri", color: C.muted, margin: 0 }); // Stats row const stats = [ { val: "6 600+", lbl: "Clients" }, { val: "46", lbl: "Techniciens" }, { val: "7 500+", lbl: "Equipements" }, { val: "115K", lbl: "Factures" }, ]; stats.forEach((st, i) => { const sx = 1.15 + i * 2.1; s.addShape(pres.shapes.RECTANGLE, { x: sx, y: 3.7, w: 1.8, h: 1.0, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addText(st.val, { x: sx, y: 3.72, w: 1.8, h: 0.55, fontSize: 22, fontFace: "Arial Black", color: C.accent, align: "center", valign: "middle", margin: 0 }); s.addText(st.lbl, { x: sx, y: 4.25, w: 1.8, h: 0.35, fontSize: 11, fontFace: "Calibri", color: C.muted, align: "center", valign: "top", margin: 0 }); }); s.addText("Presentation technique — Avril 2026", { x: 0.8, y: 5.1, w: 9, h: 0.3, fontSize: 10, fontFace: "Calibri", color: C.muted, margin: 0 }); // ═══════════════════════════════════════ // SLIDE 2 — Architecture Overview // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Architecture de la plateforme", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); s.addText("Monorepo • Docker • Traefik • ERPNext • Vue 3", { x: 0.6, y: 0.8, w: 9, h: 0.35, fontSize: 12, fontFace: "Calibri", color: C.muted, margin: 0 }); const archBoxes = [ { icon: "mobile", title: "Ops App", desc: "Vue 3 / Quasar PWA\n12 pages, 40 composables\n9 modules API", x: 0.4, y: 1.5 }, { icon: "server", title: "targo-hub", desc: "Node.js 20\n30 modules\nSSE temps reel", x: 2.65, y: 1.5 }, { icon: "db", title: "ERPNext v16", desc: "PostgreSQL\nDoctypes custom\nAPI REST", x: 4.9, y: 1.5 }, { icon: "shield", title: "Authentik SSO", desc: "Staff + Client\nForwardAuth\nOAuth/OIDC", x: 7.15, y: 1.5 }, ]; for (const b of archBoxes) { s.addShape(pres.shapes.RECTANGLE, { x: b.x, y: b.y, w: 2.05, h: 2.3, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addShape(pres.shapes.RECTANGLE, { x: b.x, y: b.y, w: 2.05, h: 0.06, fill: { color: C.indigo } }); s.addImage({ data: icons[b.icon], x: b.x + 0.75, y: b.y + 0.25, w: 0.5, h: 0.5 }); s.addText(b.title, { x: b.x + 0.1, y: b.y + 0.85, w: 1.85, h: 0.35, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, align: "center", margin: 0 }); s.addText(b.desc, { x: b.x + 0.1, y: b.y + 1.2, w: 1.85, h: 1.0, fontSize: 10, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 }); } // Integration row const integ = [ { name: "GenieACS", sub: "TR-069 / 7560 CPE" }, { name: "Twilio", sub: "SMS / Voix" }, { name: "Mapbox", sub: "Cartes / Routes" }, { name: "Gemini AI", sub: "OCR / Agent" }, { name: "Stripe", sub: "Paiements" }, { name: "Traccar", sub: "GPS temps reel" }, ]; s.addText("INTEGRATIONS", { x: 0.6, y: 4.05, w: 9, h: 0.3, fontSize: 10, fontFace: "Calibri", color: C.muted, charSpacing: 3, margin: 0 }); integ.forEach((it, i) => { const ix = 0.4 + i * 1.55; s.addShape(pres.shapes.RECTANGLE, { x: ix, y: 4.4, w: 1.4, h: 0.85, fill: { color: "111422" } }); s.addText(it.name, { x: ix, y: 4.42, w: 1.4, h: 0.4, fontSize: 10, fontFace: "Calibri", color: C.accent, bold: true, align: "center", margin: 0 }); s.addText(it.sub, { x: ix, y: 4.78, w: 1.4, h: 0.35, fontSize: 8, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 }); }); // ═══════════════════════════════════════ // SLIDE 3 — Dispatch Timeline // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Dispatch & planification", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); s.addText("Timeline interactive avec drag-and-drop, carte Mapbox, et vues jour/semaine/mois", { x: 0.6, y: 0.8, w: 9, h: 0.35, fontSize: 12, fontFace: "Calibri", color: C.muted, margin: 0 }); // Feature grid 2x3 const dispFeats = [ { icon: "calendar", title: "Timeline Gantt", desc: "Vue jour : rangees par technicien, blocs de jobs colores, drag-and-drop pour assigner ou reordonner" }, { icon: "route", title: "Routes optimisees", desc: "Calcul d'itineraires Mapbox, temps de deplacement, geo-fixation sur carte" }, { icon: "tags", title: "Tags / Competences", desc: "Niveaux 1-5 sur techs et jobs. Auto-dispatch : match minimum adequat, preserver les experts" }, { icon: "clock", title: "Mode planification", desc: "Shifts reguliers en fond bleu, garde en ambre, absences en rouge. Editeur d'horaire inline" }, { icon: "check", title: "Draft / Publish", desc: "Jobs en brouillon (hachures) puis publication en masse + envoi SMS du resume horaire" }, { icon: "bell", title: "Recurrence Google", desc: "Selecteur RRULE style Google Calendar avec options contextuelles + editeur personnalise" }, ]; dispFeats.forEach((f, i) => { const col = i % 3, row = Math.floor(i / 3); const fx = 0.4 + col * 3.1, fy = 1.35 + row * 2.05; s.addShape(pres.shapes.RECTANGLE, { x: fx, y: fy, w: 2.9, h: 1.8, fill: { color: C.cardBg }, shadow: mkShadow() }); // Icon circle s.addShape(pres.shapes.OVAL, { x: fx + 0.2, y: fy + 0.2, w: 0.55, h: 0.55, fill: { color: C.purpleDk } }); s.addImage({ data: icons[f.icon], x: fx + 0.3, y: fy + 0.3, w: 0.35, h: 0.35 }); s.addText(f.title, { x: fx + 0.9, y: fy + 0.2, w: 1.85, h: 0.4, fontSize: 13, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); s.addText(f.desc, { x: fx + 0.2, y: fy + 0.85, w: 2.5, h: 0.85, fontSize: 9.5, fontFace: "Calibri", color: C.muted, margin: 0 }); }); // ═══════════════════════════════════════ // SLIDE 4 — Gestion des horaires // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Gestion des horaires techniciens", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); // Left: schedule editor mockup s.addShape(pres.shapes.RECTANGLE, { x: 0.5, y: 1.1, w: 4.3, h: 4.1, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addText("Editeur d'horaire", { x: 0.7, y: 1.2, w: 3.9, h: 0.4, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); const days = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]; const schedActive = [true, true, true, true, true, false, false]; days.forEach((d, i) => { const dy = 1.75 + i * 0.48; s.addShape(pres.shapes.RECTANGLE, { x: 0.7, y: dy, w: 3.9, h: 0.38, fill: { color: schedActive[i] ? "1E2338" : "141726" } }); s.addText(d, { x: 0.8, y: dy, w: 0.6, h: 0.38, fontSize: 10, fontFace: "Calibri", color: schedActive[i] ? C.green : C.muted, bold: true, valign: "middle", margin: 0 }); if (schedActive[i]) { s.addText("08:00 → 16:00", { x: 1.5, y: dy, w: 2.0, h: 0.38, fontSize: 10, fontFace: "Calibri", color: C.text, valign: "middle", margin: 0 }); } else { s.addText("Repos", { x: 1.5, y: dy, w: 2.0, h: 0.38, fontSize: 10, fontFace: "Calibri", color: C.muted, italic: true, valign: "middle", margin: 0 }); } }); // Garde shift s.addText("Shifts de garde", { x: 0.7, y: 5.2 - 0.85, w: 3.9, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.amber, bold: true, margin: 0 }); s.addShape(pres.shapes.RECTANGLE, { x: 0.7, y: 5.2 - 0.48, w: 3.9, h: 0.38, fill: { color: "2A2510" } }); s.addText("Garde 08:00 → 16:00 1 fin de semaine sur 4", { x: 0.8, y: 5.2 - 0.48, w: 3.7, h: 0.38, fontSize: 9, fontFace: "Calibri", color: C.amber, valign: "middle", margin: 0 }); // Right: features s.addShape(pres.shapes.RECTANGLE, { x: 5.2, y: 1.1, w: 4.3, h: 4.1, fill: { color: C.cardBg }, shadow: mkShadow() }); const schedFeats = [ { title: "Horaire hebdomadaire", desc: "Configuration par jour, presets (temps plein, soirs, nuits)" }, { title: "Shifts de garde (RRULE)", desc: "Recurrence flexible : 1 weekend sur N, soirs de semaine, personnalise" }, { title: "RecurrenceSelector", desc: "Composant Google Calendar : options contextuelles + editeur custom RRULE" }, { title: "Visualisation timeline", desc: "Blocs bleus (regulier), ambres (garde) en fond du timeline" }, { title: "Presets partages", desc: "Groupes de ressources sauvegardes dans ERPNext, partages entre superviseurs" }, ]; schedFeats.forEach((f, i) => { const fy = 1.3 + i * 0.78; s.addShape(pres.shapes.OVAL, { x: 5.4, y: fy + 0.05, w: 0.28, h: 0.28, fill: { color: C.purpleDk } }); s.addText(String(i + 1), { x: 5.4, y: fy + 0.05, w: 0.28, h: 0.28, fontSize: 9, fontFace: "Calibri", color: C.white, bold: true, align: "center", valign: "middle", margin: 0 }); s.addText(f.title, { x: 5.85, y: fy, w: 3.4, h: 0.3, fontSize: 12, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); s.addText(f.desc, { x: 5.85, y: fy + 0.3, w: 3.4, h: 0.35, fontSize: 9, fontFace: "Calibri", color: C.muted, margin: 0 }); }); // ═══════════════════════════════════════ // SLIDE 5 — Attribution de jobs & SMS // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Attribution de jobs & communication", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); // Flow diagram const flowSteps = [ { icon: "calendar", title: "1. Planifier", desc: "Drag-and-drop sur\nle timeline", color: C.indigo }, { icon: "send", title: "2. Publier", desc: "Selection en masse\n+ confirmation", color: C.purple }, { icon: "sms", title: "3. SMS", desc: "Resume horaire\nenvoye via Twilio", color: C.green }, { icon: "mobile", title: "4. Lien tech", desc: "Vue mobile /j\navec bottom sheet", color: C.amber }, ]; flowSteps.forEach((f, i) => { const fx = 0.4 + i * 2.4; s.addShape(pres.shapes.RECTANGLE, { x: fx, y: 1.2, w: 2.15, h: 1.8, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addShape(pres.shapes.RECTANGLE, { x: fx, y: 1.2, w: 2.15, h: 0.06, fill: { color: f.color } }); s.addShape(pres.shapes.OVAL, { x: fx + 0.75, y: 1.4, w: 0.6, h: 0.6, fill: { color: f.color, transparency: 70 } }); s.addImage({ data: icons[f.icon], x: fx + 0.85, y: 1.5, w: 0.4, h: 0.4 }); s.addText(f.title, { x: fx + 0.1, y: 2.15, w: 1.95, h: 0.35, fontSize: 13, fontFace: "Calibri", color: C.white, bold: true, align: "center", margin: 0 }); s.addText(f.desc, { x: fx + 0.1, y: 2.5, w: 1.95, h: 0.45, fontSize: 10, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 }); }); // SMS example mockup s.addShape(pres.shapes.RECTANGLE, { x: 0.5, y: 3.3, w: 4.3, h: 2.0, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addImage({ data: icons.sms_purple, x: 0.7, y: 3.45, w: 0.35, h: 0.35 }); s.addText("Envoi SMS (Twilio)", { x: 1.15, y: 3.45, w: 3.3, h: 0.35, fontSize: 13, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); s.addShape(pres.shapes.RECTANGLE, { x: 0.7, y: 3.95, w: 3.9, h: 1.15, fill: { color: "111422" } }); s.addText([ { text: "Bonjour Louis-Paul,\n", options: { fontSize: 9, color: C.text, breakLine: true } }, { text: "Votre horaire pour le 9 avril :\n", options: { fontSize: 9, color: C.text, breakLine: true } }, { text: "08h00 - SUP-003 Panne IP Phone\n", options: { fontSize: 9, color: C.accent, breakLine: true } }, { text: "10h30 - INS-047 Installation Fibre\n", options: { fontSize: 9, color: C.accent, breakLine: true } }, { text: "erp.gigafibre.ca/ops/#/j", options: { fontSize: 8, color: C.green } }, ], { x: 0.85, y: 4.0, w: 3.6, h: 1.05, margin: 0 }); // Magic links s.addShape(pres.shapes.RECTANGLE, { x: 5.2, y: 3.3, w: 4.3, h: 2.0, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addImage({ data: icons.magic_purple, x: 5.4, y: 3.45, w: 0.35, h: 0.35 }); s.addText("Magic Links & OTP", { x: 5.85, y: 3.45, w: 3.3, h: 0.35, fontSize: 13, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); const mlFeats = [ "Authentification sans mot de passe", "Lien unique par SMS ou email", "OTP 6 chiffres via Twilio", "Migration transparente legacy MD5", "Session SSO via Authentik", ]; mlFeats.forEach((f, i) => { s.addImage({ data: icons.check_purple, x: 5.4, y: 3.95 + i * 0.22, w: 0.15, h: 0.15 }); s.addText(f, { x: 5.7, y: 3.95 + i * 0.22, w: 3.5, h: 0.22, fontSize: 10, fontFace: "Calibri", color: C.text, margin: 0, valign: "middle" }); }); // ═══════════════════════════════════════ // SLIDE 6 — Uber-style offer pool // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Pool d'offres — Style Uber", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); s.addText("Proposer des taches aux ressources internes et externes avec tarification dynamique", { x: 0.6, y: 0.8, w: 9, h: 0.35, fontSize: 12, fontFace: "Calibri", color: C.muted, margin: 0 }); // 3 mode cards const modes = [ { title: "Broadcast", desc: "Envoyer a toutes les\nressources disponibles", icon: "bell", color: C.indigo }, { title: "Targeted", desc: "Selectionner des\ntechniciens specifiques", icon: "users", color: C.green }, { title: "Pool", desc: "Match par competences\net disponibilite", icon: "tags", color: C.amber }, ]; modes.forEach((m, i) => { const mx = 0.4 + i * 3.15; s.addShape(pres.shapes.RECTANGLE, { x: mx, y: 1.3, w: 2.95, h: 1.5, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addShape(pres.shapes.OVAL, { x: mx + 0.2, y: 1.5, w: 0.5, h: 0.5, fill: { color: m.color, transparency: 70 } }); s.addImage({ data: icons[m.icon], x: mx + 0.28, y: 1.58, w: 0.35, h: 0.35 }); s.addText(m.title, { x: mx + 0.85, y: 1.45, w: 1.9, h: 0.35, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); s.addText(m.desc, { x: mx + 0.85, y: 1.8, w: 1.9, h: 0.5, fontSize: 10, fontFace: "Calibri", color: C.muted, margin: 0 }); }); // Pricing section s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: 3.1, w: 4.5, h: 2.2, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addText("Tarification", { x: 0.6, y: 3.2, w: 4.1, h: 0.4, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); const pricing = [ { label: "Deplacement (base)", val: "150 $", color: C.accent }, { label: "Taux horaire", val: "125 $/h", color: C.accent }, { label: "Majoration urgence", val: "+50%", color: C.red }, { label: "Weekend / ferie", val: "+75%", color: C.amber }, ]; pricing.forEach((p, i) => { const py = 3.7 + i * 0.4; s.addText(p.label, { x: 0.7, y: py, w: 2.5, h: 0.35, fontSize: 11, fontFace: "Calibri", color: C.text, margin: 0, valign: "middle" }); s.addText(p.val, { x: 3.2, y: py, w: 1.5, h: 0.35, fontSize: 13, fontFace: "Arial Black", color: p.color, margin: 0, valign: "middle", align: "right" }); }); // Flow s.addShape(pres.shapes.RECTANGLE, { x: 5.2, y: 3.1, w: 4.3, h: 2.2, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addText("Flux d'offre", { x: 5.4, y: 3.2, w: 3.9, h: 0.4, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); const offerFlow = [ { step: "1", text: "Superviseur cree l'offre (cout estime)" }, { step: "2", text: "Notification SMS aux techniciens cibles" }, { step: "3", text: "Technicien accepte ou decline" }, { step: "4", text: "Job auto-assigne au premier accepte" }, { step: "5", text: "Alerte surcharge si capacite > 100%" }, ]; offerFlow.forEach((f, i) => { const fy = 3.7 + i * 0.32; s.addShape(pres.shapes.OVAL, { x: 5.4, y: fy + 0.03, w: 0.24, h: 0.24, fill: { color: C.indigo } }); s.addText(f.step, { x: 5.4, y: fy + 0.03, w: 0.24, h: 0.24, fontSize: 8, fontFace: "Calibri", color: C.white, bold: true, align: "center", valign: "middle", margin: 0 }); s.addText(f.text, { x: 5.8, y: fy, w: 3.5, h: 0.28, fontSize: 10, fontFace: "Calibri", color: C.text, margin: 0, valign: "middle" }); }); // ═══════════════════════════════════════ // SLIDE 7 — Portail client & Catalogue // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Portail client & catalogue", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); s.addText("Self-service client avec Stripe, catalogue produits, et gestion de compte", { x: 0.6, y: 0.8, w: 9, h: 0.35, fontSize: 12, fontFace: "Calibri", color: C.muted, margin: 0 }); // Client portal features — left s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: 1.3, w: 4.5, h: 3.7, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addImage({ data: icons.cart_purple, x: 0.6, y: 1.45, w: 0.4, h: 0.4 }); s.addText("Portail client (client.gigafibre.ca)", { x: 1.1, y: 1.45, w: 3.5, h: 0.4, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); const portalFeats = [ "Consultation factures et historique", "Paiement en ligne via Stripe Checkout", "Gestion des abonnements actifs", "Tickets de support (creation + suivi)", "Catalogue produits avec panier", "Checkout + soumission commande", "Authentification SSO (id.gigafibre.ca)", "OTP SMS pour recuperation de compte", ]; portalFeats.forEach((f, i) => { s.addImage({ data: icons.check_purple, x: 0.7, y: 2.05 + i * 0.35, w: 0.18, h: 0.18 }); s.addText(f, { x: 1.0, y: 2.05 + i * 0.35, w: 3.7, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.text, margin: 0, valign: "middle" }); }); // Catalogue — right s.addShape(pres.shapes.RECTANGLE, { x: 5.2, y: 1.3, w: 4.3, h: 3.7, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addImage({ data: icons.toolbox_purple, x: 5.4, y: 1.45, w: 0.4, h: 0.4 }); s.addText("Catalogue & wizard", { x: 5.9, y: 1.45, w: 3.3, h: 0.4, fontSize: 14, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); const catalogItems = [ { name: "Internet Fibre", price: "a partir de 55$/mois" }, { name: "Television IPTV", price: "20$/mois" }, { name: "Telephonie VoIP", price: "15$/mois" }, { name: "Support Prioritaire", price: "10$/mois" }, ]; catalogItems.forEach((it, i) => { const cy = 2.1 + i * 0.7; s.addShape(pres.shapes.RECTANGLE, { x: 5.4, y: cy, w: 3.9, h: 0.55, fill: { color: "1E2338" } }); s.addText(it.name, { x: 5.6, y: cy, w: 2.4, h: 0.55, fontSize: 12, fontFace: "Calibri", color: C.white, bold: true, margin: 0, valign: "middle" }); s.addText(it.price, { x: 7.8, y: cy, w: 1.4, h: 0.55, fontSize: 11, fontFace: "Calibri", color: C.green, margin: 0, valign: "middle", align: "right" }); }); // ═══════════════════════════════════════ // SLIDE 8 — Vue mobile technicien // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Vue mobile technicien", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); s.addText("Integree dans l'ops app a /j — envoyee par SMS apres publication de l'horaire", { x: 0.6, y: 0.8, w: 9, h: 0.35, fontSize: 12, fontFace: "Calibri", color: C.muted, margin: 0 }); // Phone mockup - left s.addShape(pres.shapes.RECTANGLE, { x: 0.8, y: 1.3, w: 3.0, h: 4.0, fill: { color: "1A1D30" }, shadow: mkShadow() }); s.addShape(pres.shapes.RECTANGLE, { x: 0.8, y: 1.3, w: 3.0, h: 0.06, fill: { color: C.purple } }); // Phone header s.addShape(pres.shapes.RECTANGLE, { x: 0.8, y: 1.36, w: 3.0, h: 1.0, fill: { color: C.purpleDk } }); s.addText("jeudi 9 avril", { x: 0.95, y: 1.4, w: 2.5, h: 0.25, fontSize: 8, fontFace: "Calibri", color: C.muted, margin: 0 }); s.addText("Louis-Paul Bourdon", { x: 0.95, y: 1.6, w: 2.5, h: 0.3, fontSize: 12, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); // Stats mini const miniStats = [{ v: "3", l: "Total" }, { v: "3", l: "A faire" }, { v: "0", l: "Faits" }]; miniStats.forEach((ms, i) => { s.addShape(pres.shapes.RECTANGLE, { x: 0.95 + i * 0.85, y: 2.0, w: 0.75, h: 0.42, fill: { color: "4A4880" } }); s.addText(ms.v, { x: 0.95 + i * 0.85, y: 2.0, w: 0.75, h: 0.25, fontSize: 11, fontFace: "Arial Black", color: C.white, align: "center", margin: 0 }); s.addText(ms.l, { x: 0.95 + i * 0.85, y: 2.22, w: 0.75, h: 0.18, fontSize: 6, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 }); }); // Job cards mini const miniJobs = [ { id: "SUP-003", title: "Panne IP Phone", loc: "Vieux-Port", time: "08h09", urgent: true }, { id: "INS-047", title: "Installation Fibre", loc: "Plateau", time: "10h30", urgent: false }, ]; miniJobs.forEach((mj, i) => { const my = 2.6 + i * 0.85; s.addShape(pres.shapes.RECTANGLE, { x: 0.95, y: my, w: 2.5, h: 0.7, fill: { color: C.cardBg } }); s.addShape(pres.shapes.RECTANGLE, { x: 0.95, y: my, w: 0.06, h: 0.7, fill: { color: mj.urgent ? C.red : C.purple } }); s.addText(mj.id, { x: 1.1, y: my + 0.03, w: 1.0, h: 0.2, fontSize: 7, fontFace: "Calibri", color: C.accent, bold: true, margin: 0 }); s.addText(mj.time, { x: 2.85, y: my + 0.03, w: 0.55, h: 0.2, fontSize: 7, fontFace: "Calibri", color: C.text, margin: 0, align: "right" }); s.addText(mj.title, { x: 1.1, y: my + 0.22, w: 2.2, h: 0.22, fontSize: 8, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); s.addText(mj.loc, { x: 1.1, y: my + 0.45, w: 2.2, h: 0.2, fontSize: 7, fontFace: "Calibri", color: C.muted, margin: 0 }); }); // Features — right const techFeats = [ { icon: "check", title: "Actions rapides", desc: "En route / Terminer depuis la liste ou le bottom sheet. GPS navigation integree." }, { icon: "qr", title: "Scanner equipement", desc: "Recherche par SN/MAC, creation inline, liaison automatique au job en cours." }, { icon: "cogs", title: "Edition inline", desc: "Modifier sujet, heure, duree, notes directement depuis le telephone." }, { icon: "chart", title: "Diagnostic reseau", desc: "Speed test, verification d'hotes, latence — directement depuis le terrain." }, ]; techFeats.forEach((f, i) => { const fy = 1.3 + i * 1.0; s.addShape(pres.shapes.RECTANGLE, { x: 4.5, y: fy, w: 5.1, h: 0.85, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addShape(pres.shapes.OVAL, { x: 4.7, y: fy + 0.15, w: 0.5, h: 0.5, fill: { color: C.purpleDk } }); s.addImage({ data: icons[f.icon], x: 4.8, y: fy + 0.25, w: 0.3, h: 0.3 }); s.addText(f.title, { x: 5.35, y: fy + 0.05, w: 4.0, h: 0.3, fontSize: 13, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); s.addText(f.desc, { x: 5.35, y: fy + 0.35, w: 4.0, h: 0.45, fontSize: 10, fontFace: "Calibri", color: C.muted, margin: 0 }); }); // ═══════════════════════════════════════ // SLIDE 9 — Stack technique // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addText("Stack technique", { x: 0.6, y: 0.25, w: 9, h: 0.6, fontSize: 28, fontFace: "Arial Black", color: C.white, margin: 0 }); const techStack = [ { cat: "Frontend", items: "Vue 3 (Composition API)\nQuasar v2.19 (PWA)\nPinia stores\nMapbox GL JS\nSCSS + CSS variables" }, { cat: "Backend", items: "Node.js 20 (targo-hub)\nERPNext v16 (Python)\nn8n (automations)\nPostgreSQL 14\nRedis (cache/queue)" }, { cat: "Infra", items: "Docker Compose\nTraefik v2.11 (TLS)\nAuthentik SSO\nLet's Encrypt\nProxmox VM" }, { cat: "Integrations", items: "Twilio (SMS/Voix)\nGenieACS (TR-069)\nStripe (Paiements)\nGemini AI (OCR)\nCloudflare (DNS)" }, ]; techStack.forEach((ts, i) => { const tx = 0.4 + i * 2.35; s.addShape(pres.shapes.RECTANGLE, { x: tx, y: 1.1, w: 2.15, h: 3.5, fill: { color: C.cardBg }, shadow: mkShadow() }); s.addShape(pres.shapes.RECTANGLE, { x: tx, y: 1.1, w: 2.15, h: 0.06, fill: { color: i === 0 ? C.indigo : i === 1 ? C.green : i === 2 ? C.amber : C.red } }); s.addText(ts.cat, { x: tx + 0.15, y: 1.3, w: 1.85, h: 0.4, fontSize: 15, fontFace: "Calibri", color: C.white, bold: true, margin: 0 }); s.addText(ts.items, { x: tx + 0.15, y: 1.8, w: 1.85, h: 2.6, fontSize: 11, fontFace: "Calibri", color: C.muted, margin: 0 }); }); // Deployment s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: 4.85, w: 9.2, h: 0.55, fill: { color: C.cardBg } }); s.addText([ { text: "Deploy: ", options: { bold: true, color: C.white, fontSize: 11 } }, { text: "npx quasar build", options: { color: C.green, fontSize: 11, fontFace: "Consolas" } }, { text: " → ", options: { color: C.muted, fontSize: 11 } }, { text: "scp dist/spa/* root@96.125.196.67:/opt/ops-app/", options: { color: C.accent, fontSize: 10, fontFace: "Consolas" } }, { text: " (no restart needed)", options: { color: C.muted, fontSize: 10 } }, ], { x: 0.6, y: 4.85, w: 8.8, h: 0.55, margin: 0, valign: "middle" }); // ═══════════════════════════════════════ // SLIDE 10 — Closing // ═══════════════════════════════════════ s = pres.addSlide(); s.background = { color: C.darkBg }; s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } }); s.addShape(pres.shapes.RECTANGLE, { x: 0.8, y: 1.8, w: 0.12, h: 1.8, fill: { color: C.indigo } }); s.addText("GIGAFIBRE FSM", { x: 1.15, y: 1.8, w: 8, h: 0.7, fontSize: 36, fontFace: "Arial Black", color: C.white, margin: 0 }); s.addText("Questions ?", { x: 1.15, y: 2.5, w: 8, h: 0.5, fontSize: 22, fontFace: "Calibri", color: C.accent, margin: 0 }); const urls = [ { lbl: "Ops App", url: "erp.gigafibre.ca/ops/" }, { lbl: "ERPNext", url: "erp.gigafibre.ca" }, { lbl: "Vue Tech", url: "erp.gigafibre.ca/ops/#/j" }, { lbl: "Portail", url: "client.gigafibre.ca" }, { lbl: "Git", url: "git.targo.ca/louis/gigafibre-fsm" }, ]; urls.forEach((u, i) => { s.addText(u.lbl, { x: 1.15, y: 3.4 + i * 0.35, w: 1.5, h: 0.3, fontSize: 12, fontFace: "Calibri", color: C.muted, margin: 0, valign: "middle" }); s.addText(u.url, { x: 2.6, y: 3.4 + i * 0.35, w: 5, h: 0.3, fontSize: 12, fontFace: "Consolas", color: C.accent, margin: 0, valign: "middle" }); }); s.addText("Targo — Avril 2026", { x: 0.8, y: 5.1, w: 9, h: 0.3, fontSize: 10, fontFace: "Calibri", color: C.muted, margin: 0 }); // Write file const path = require("path"); await pres.writeFile({ fileName: path.join(__dirname, "Gigafibre-FSM-Features.pptx") }); console.log("Presentation created!"); } build().catch(e => { console.error(e); process.exit(1); });