gigafibre-fsm/docs/build-billing-pptx.js
louispaulb 41d9b5f316 feat: flow editor, Gemini QR scanner with offline queue, dispatch planning v2
Major additions accumulated over 9 days — single commit per request.

Flow editor (new):
- Generic visual editor for step trees, usable by project wizard + agent flows
- PROJECT_KINDS / AGENT_KINDS catalogs decouple UI from domain
- Drag-and-drop reorder via vuedraggable with scope isolation per peer group
- Chain-aware depends_on rewrite on reorder (sequential only — DAGs preserved)
- Variable picker with per-applies_to catalog (Customer / Quotation /
  Service Contract / Issue / Subscription), insert + copy-clipboard modes
- trigger_condition helper with domain-specific JSONLogic examples
- Global FlowEditorDialog mounted once in MainLayout, Odoo inline pattern
- Server: targo-hub flow-runtime.js, flow-api.js, flow-templates.js
- ERPNext: Flow Template/Run doctypes, scheduler, 5 seeded system templates
- depends_on chips resolve to step labels instead of opaque "s4" ids

QR/OCR scanner (field app):
- Camera capture → Gemini Vision via targo-hub with 8s timeout
- IndexedDB offline queue retries photos when signal returns
- Watcher merges late-arriving scan results into the live UI

Dispatch:
- Planning mode (draft → publish) with offer pool for unassigned jobs
- Shared presets, recurrence selector, suggested-slots dialog
- PublishScheduleModal, unassign confirmation

Ops app:
- ClientDetailPage composables extraction (useClientData, useDeviceStatus,
  useWifiDiagnostic, useModemDiagnostic)
- Project wizard: shared detail sections, wizard catalog/publish composables
- Address pricing composable + pricing-mock data
- Settings redesign hosting flow templates

Targo-hub:
- Contract acceptance (JWT residential + DocuSeal commercial tracks)
- Referral system
- Modem-bridge diagnostic normalizer
- Device extractors consolidated

Migration scripts:
- Invoice/quote print format setup, Jinja rendering
- Additional import + fix scripts (reversals, dates, customers, payments)

Docs:
- Consolidated: old scattered MDs → HANDOFF, ARCHITECTURE, DATA_AND_FLOWS,
  FLOW_EDITOR_ARCHITECTURE, BILLING_AND_PAYMENTS, CPE_MANAGEMENT,
  APP_DESIGN_GUIDELINES
- Archived legacy wizard PHP for reference
- STATUS snapshots for 2026-04-18/19

Cleanup:
- Removed ~40 generated PDFs/HTMLs (invoice_preview*, rendered_jinja*)
- .gitignore now covers invoice preview output + nested .DS_Store

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 10:44:17 -04:00

365 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Billing & Payments — Dev handoff presentation
// Regenerate with: cd docs && node build-billing-pptx.js
//
// Structure (per user feedback 2026-04-17):
// 1. Title
// 2. End-to-end flow (facture → paiement) with scripts listed under each stage
// 3. OPS app modules with scripts/components per module
// 4. PDF preview + pay-public preview (aspect ratios preserved)
const fs = require("fs");
const path = require("path");
const pptxgen = require("pptxgenjs");
const sharp = require("sharp");
function imgPath(rel) {
const p = path.join(__dirname, rel);
return fs.existsSync(p) ? p : null;
}
async function imgRatio(rel) {
const p = path.join(__dirname, rel);
if (!fs.existsSync(p)) return null;
const m = await sharp(p).metadata();
return { w: m.width, h: m.height, ratio: m.width / m.height, path: p };
}
// Fit an image inside a target box (tx, ty, tw, th) maintaining its native ratio
function fitImage(meta, tx, ty, tw, th) {
const boxR = tw / th;
let w, h;
if (meta.ratio > boxR) { w = tw; h = tw / meta.ratio; }
else { h = th; w = th * meta.ratio; }
const x = tx + (tw - w) / 2;
const y = ty + (th - h) / 2;
return { path: meta.path, x, y, w, h };
}
const C = {
darkBg: "0D0F18",
cardBg: "181C2E",
cardBg2: "111422",
purple: "5C59A8",
purpleDk: "3F3D7A",
accent: "818CF8",
green: "22C55E",
amber: "F59E0B",
red: "EF4444",
white: "FFFFFF",
text: "E2E4EF",
muted: "7B80A0",
indigo: "6366F1",
};
const shadow = () => ({ type: "outer", blur: 8, offset: 3, angle: 135, color: "000000", opacity: 0.3 });
let pres;
function header(s, title, subtitle) {
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(title, { x: 0.6, y: 0.22, w: 9, h: 0.6, fontSize: 26, fontFace: "Arial Black", color: C.white, margin: 0 });
if (subtitle) s.addText(subtitle, { x: 0.6, y: 0.76, w: 9, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.muted, margin: 0 });
}
async function build() {
pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.author = "Targo";
pres.title = "Facturation & Paiements — Handoff dev";
// ═════════ SLIDE 1 — Title ═════════
let 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.2, w: 0.12, h: 1.8, fill: { color: C.indigo } });
s.addText("Facturation", { 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("& Paiements", { x: 1.15, y: 1.85, w: 8, h: 0.6, fontSize: 36, fontFace: "Arial Black", color: C.accent, margin: 0 });
s.addText("Handoff technique — pipeline end-to-end + modules OPS", { x: 1.15, y: 2.7, w: 8.5, h: 0.4, fontSize: 16, fontFace: "Calibri", color: C.muted, margin: 0 });
const heroStats = [
{ v: "80", l: "Scripts migration" },
{ v: "115 K", l: "Factures importées" },
{ v: "1,6 M", l: "Items (incl. backfill)" },
{ v: "15", l: "Pages OPS" },
];
heroStats.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: shadow() });
s.addText(st.v, { 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.l, { x: sx, y: 4.25, w: 1.8, h: 0.35, fontSize: 10, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 });
});
s.addText("Avril 2026 • erp.gigafibre.ca + client.gigafibre.ca + ops", { x: 0.8, y: 5.1, w: 9, h: 0.3, fontSize: 10, fontFace: "Calibri", color: C.muted, margin: 0 });
// ═════════ SLIDE 2 — End-to-end flow with scripts per stage ═════════
s = pres.addSlide();
header(s, "Flux facture → paiement", "Scripts & fichiers utilisés à chaque étape");
// 6 stages laid out left-to-right, each with a card + scripts list below
const stages = [
{
title: "1. Import legacy",
tagline: "MariaDB → PostgreSQL",
color: C.purple,
scripts: [
"import_invoices.py",
"import_payments.py",
"import_payment_methods.py",
"import_payment_arrangements.py",
"import_expro_payments.py",
"backfill_service_location.py",
"fix_invoice_outstanding.py",
"fix_invoice_customer_names.py",
"add_missing_custom_fields.py",
],
},
{
title: "2. Facture",
tagline: "ERPNext Sales Invoice",
color: C.accent,
scripts: [
"setup_invoice_print_format.py",
"test_jinja_render.py",
"Print Format: « Facture TARGO »",
"Jinja template (inline)",
"pdf_generator=\"chrome\"",
"Doctype: Sales Invoice",
"Custom field: service_location",
],
},
{
title: "3. Rendu PDF",
tagline: "Chromium --print-to-pdf",
color: C.green,
scripts: [
"gigafibre_utils.api.invoice_pdf",
"Dockerfile (chromium install)",
"common_site_config:",
" chromium_path=/usr/bin/chromium",
"cairosvg → logo PNG",
"Producer: Skia/PDF m147",
"~1 s / facture",
],
},
{
title: "4. QR / Magic-link",
tagline: "Tokens HMAC-SHA256",
color: C.indigo,
scripts: [
"api.invoice_qr(invoice)",
"api.invoice_qr_datauri(invoice)",
"api._sign_pay_token",
"api.validate_pay_token",
"api.request_magic_link",
"api.pay_token (admin)",
"api.pay_redirect (portal)",
"Secret: gigafibre_pay_secret",
],
},
{
title: "5. Landing publique",
tagline: "client.gigafibre.ca/pay-public",
color: C.amber,
scripts: [
"gigafibre_utils/www/",
" pay-public.html",
"/opt/traefik/dynamic/",
" pay-public.yml",
"Traefik priority ≥ 250",
"(carve-out Authentik)",
"Vanilla JS, pas de build",
],
},
{
title: "6. Stripe + Payment Entry",
tagline: "Checkout → webhook → réconciliation",
color: C.red,
scripts: [
"api.create_checkout_session",
"api.stripe_webhook",
"api._stripe_post",
"api._stripe_verify_signature",
"Secret: gigafibre_stripe_secret_key",
"Secret: gigafibre_stripe_webhook_secret",
"Twilio: twilio_account_sid / token / from",
],
},
];
const cardW = 1.5, cardH = 0.7, gap = 0.08;
const totalW = stages.length * cardW + (stages.length - 1) * gap;
const startX = (10 - totalW) / 2;
const cardY = 1.3;
stages.forEach((st, i) => {
const x = startX + i * (cardW + gap);
s.addShape(pres.shapes.RECTANGLE, {
x, y: cardY, w: cardW, h: cardH,
fill: { color: C.cardBg }, line: { color: st.color, width: 1.5 }, shadow: shadow(),
});
s.addText(st.title, { x, y: cardY + 0.03, w: cardW, h: 0.32, fontSize: 10, fontFace: "Arial Black", color: C.white, align: "center", valign: "middle", margin: 0 });
s.addText(st.tagline, { x, y: cardY + 0.35, w: cardW, h: 0.3, fontSize: 7.5, fontFace: "Calibri", color: st.color, align: "center", valign: "middle", margin: 0 });
// Arrow to next stage
if (i < stages.length - 1) {
s.addText("→", { x: x + cardW - 0.05, y: cardY + 0.18, w: gap + 0.1, h: 0.35, fontSize: 14, color: C.muted, align: "center", valign: "middle", margin: 0, bold: true });
}
// Scripts list under the card
const listY = cardY + cardH + 0.15;
s.addShape(pres.shapes.RECTANGLE, {
x, y: listY, w: cardW, h: 3.3,
fill: { color: C.cardBg2 }, line: { color: st.color, width: 0.5 },
});
const txt = st.scripts.map(sc => "• " + sc).join("\n");
s.addText(txt, {
x: x + 0.04, y: listY + 0.05, w: cardW - 0.08, h: 3.2,
fontSize: 6.8, fontFace: "Courier New", color: C.text, valign: "top", margin: 0,
paraSpaceAfter: 2,
});
});
// Footer: common secrets
s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: 5.5, w: 9.2, h: 0.8, fill: { color: "1a1a2e" }, line: { color: C.purple, width: 0.5 } });
s.addText("Configuration (common_site_config.json)", { x: 0.55, y: 5.55, w: 9, h: 0.2, fontSize: 9, fontFace: "Calibri", color: C.accent, bold: true, margin: 0 });
s.addText("gigafibre_pay_secret · gigafibre_pay_host · gigafibre_stripe_secret_key · gigafibre_stripe_webhook_secret · twilio_account_sid · twilio_auth_token · twilio_from_number · chromium_path", {
x: 0.55, y: 5.8, w: 9, h: 0.45, fontSize: 8, fontFace: "Courier New", color: C.text, margin: 0,
});
// ═════════ SLIDE 3 — OPS modules with scripts/components ═════════
s = pres.addSlide();
header(s, "Modules OPS", "Pages & composants · apps/ops/src/");
const modules = [
{
title: "Clients",
color: C.purple,
files: [
"pages/ClientsPage.vue",
"pages/ClientDetailPage.vue",
"components/customer/*",
"components/shared/",
" detail-sections/",
" InvoiceDetail.vue",
" PaymentDetail.vue",
" SubscriptionDetail.vue",
" EquipmentDetail.vue",
" IssueDetail.vue",
],
},
{
title: "Dispatch",
color: C.amber,
files: [
"pages/DispatchPage.vue",
"components/dispatch/",
"pages/dispatch-styles.scss",
"mode sombre (exception)",
],
},
{
title: "Facturation",
color: C.green,
files: [
"shared/CreateInvoiceModal.vue",
"shared/detail-sections/",
" InvoiceDetail.vue",
" onglet Aperçu client",
" iframe → invoice_pdf",
"shared/detail-sections/",
" PaymentDetail.vue",
],
},
{
title: "Rapports",
color: C.indigo,
files: [
"pages/RapportsPage.vue",
"pages/ReportARPage.vue",
"pages/ReportRevenuPage.vue",
"pages/ReportTaxesPage.vue",
"pages/ReportVentesPage.vue",
],
},
{
title: "Network",
color: C.accent,
files: [
"pages/NetworkPage.vue",
"pages/TelephonyPage.vue",
"pages/OcrPage.vue",
],
},
{
title: "Équipe & Tickets",
color: C.red,
files: [
"pages/EquipePage.vue",
"pages/TicketsPage.vue",
"pages/AgentFlowsPage.vue",
"pages/DashboardPage.vue",
"pages/SettingsPage.vue",
],
},
];
const cols = 3, rows = 2;
const mW = 2.9, mH = 2.25;
const mGapX = 0.15, mGapY = 0.15;
const mStartX = (10 - (cols * mW + (cols - 1) * mGapX)) / 2;
const mStartY = 1.3;
modules.forEach((mod, i) => {
const cx = mStartX + (i % cols) * (mW + mGapX);
const cy = mStartY + Math.floor(i / cols) * (mH + mGapY);
s.addShape(pres.shapes.RECTANGLE, {
x: cx, y: cy, w: mW, h: mH,
fill: { color: C.cardBg }, line: { color: mod.color, width: 1.5 }, shadow: shadow(),
});
s.addShape(pres.shapes.RECTANGLE, { x: cx, y: cy, w: mW, h: 0.35, fill: { color: mod.color } });
s.addText(mod.title, { x: cx + 0.1, y: cy, w: mW - 0.2, h: 0.35, fontSize: 12, fontFace: "Arial Black", color: C.white, valign: "middle", margin: 0 });
s.addText(mod.files.map(f => "• " + f).join("\n"), {
x: cx + 0.1, y: cy + 0.42, w: mW - 0.2, h: mH - 0.5,
fontSize: 8.5, fontFace: "Courier New", color: C.text, valign: "top", margin: 0,
});
});
// Footer: deploy note
s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: mStartY + 2 * (mH + mGapY) + 0.1, w: 9.2, h: 0.5, fill: { color: "1a1a2e" }, line: { color: C.amber, width: 0.5 } });
s.addText("Déploiement: scp dist/spa/* → /opt/ops-app/ · nginx proxy API (token) · Authentik SSO · Traefik routing", {
x: 0.55, y: mStartY + 2 * (mH + mGapY) + 0.18, w: 9, h: 0.4, fontSize: 9, fontFace: "Calibri", color: C.text, margin: 0,
});
// ═════════ SLIDE 4 — Visuels : PDF facture + landing pay-public ═════════
s = pres.addSlide();
header(s, "Visuels", "PDF Facture TARGO (Chrome) + Landing /pay-public");
const invoiceMeta = await imgRatio("assets/screenshots/invoice-pdf.png");
const payMeta = await imgRatio("assets/screenshots/pay-public.png");
// Two equal 4.5" × 4.5" boxes, images fit maintaining ratio
const box = { w: 4.3, h: 4.4, y: 1.2 };
const leftX = 0.4;
const rightX = 5.3;
s.addShape(pres.shapes.RECTANGLE, { x: leftX, y: box.y, w: box.w, h: box.h, fill: { color: C.white }, line: { color: C.purple, width: 1 } });
if (invoiceMeta) {
const fit = fitImage(invoiceMeta, leftX + 0.05, box.y + 0.05, box.w - 0.1, box.h - 0.1);
s.addImage({ path: fit.path, x: fit.x, y: fit.y, w: fit.w, h: fit.h });
}
s.addText("PDF Facture TARGO", { x: leftX, y: box.y + box.h + 0.05, w: box.w, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.white, bold: true, align: "center", margin: 0 });
s.addText("Chromium --print-to-pdf · 1 page · ~1 s", { x: leftX, y: box.y + box.h + 0.32, w: box.w, h: 0.25, fontSize: 9, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 });
s.addShape(pres.shapes.RECTANGLE, { x: rightX, y: box.y, w: box.w, h: box.h, fill: { color: C.white }, line: { color: C.accent, width: 1 } });
if (payMeta) {
const fit = fitImage(payMeta, rightX + 0.05, box.y + 0.05, box.w - 0.1, box.h - 0.1);
s.addImage({ path: fit.path, x: fit.x, y: fit.y, w: fit.w, h: fit.h });
}
s.addText("Landing /pay-public", { x: rightX, y: box.y + box.h + 0.05, w: box.w, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.white, bold: true, align: "center", margin: 0 });
s.addText("Bypass Authentik · Stripe Checkout · Magic-link SMS/email", { x: rightX, y: box.y + box.h + 0.32, w: box.w, h: 0.25, fontSize: 9, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 });
// Save
const out = path.join(__dirname, "Gigafibre-Billing-Handoff.pptx");
await pres.writeFile({ fileName: out });
console.log("Wrote:", out);
}
build().catch(e => { console.error(e); process.exit(1); });