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>
490 lines
32 KiB
JavaScript
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); });
|