gigafibre-fsm/docs/build-pptx.js
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
Backend services:
- targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons
  lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas,
  extract dispatch scoring weights, trim section dividers across 9 files
- modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(),
  consolidate DM query factory, fix duplicate username fill bug, trim headers
  (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%)

Frontend:
- useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into
  6 focused helpers (processOnlineStatus, processWanIPs, processRadios,
  processMeshNodes, processClients, checkRadioIssues)
- EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments

Documentation (17 → 13 files, -1,400 lines):
- New consolidated README.md (architecture, services, dependencies, auth)
- Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md
- Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md
- Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md
- Update ROADMAP.md with current phase status
- Delete CONTEXT.md (absorbed into README)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:39:58 -04:00

490 lines
32 KiB
JavaScript

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
await pres.writeFile({ fileName: "/Users/louispaul/Documents/testap/gigafibre-fsm/docs/Gigafibre-FSM-Features.pptx" });
console.log("Presentation created!");
}
build().catch(e => { console.error(e); process.exit(1); });