gigafibre-fsm/apps/ops/src/config/nav.js
louispaulb 611f4ed5a6 feat(ops/campaigns): UI module for gift campaigns + GrapesJS template editor
New /campaigns section in the ops SPA, gated by manage_users (proxy until a
dedicated manage_campaigns capability is added).

Pages (apps/ops/src/modules/campaigns/pages/):

- CampaignsListPage: table of all campaigns with status chip + progress
  bar (sent/total, with fail count), "Nouvelle campagne" + "Éditer le
  template" buttons. Empty state with onboarding copy.

- CampaignNewPage: 3-step Quasar Stepper wizard.
  Step 1 — upload Map CSV + Giftbit CSV, configure params (name, amount,
           commitment_months, sender, throttle, multi-email handling).
  Step 2 — preview the matched send list from POST /campaigns/parse, with
           counters (matched/unmatched/excluded), per-row match-method
           chip, and exclude/include toggle. Banner warns when CSVs are
           mis-aligned (leftover gifts or contacts).
  Step 3 — confirmation recap with estimated send duration, then fire
           POST /campaigns + POST /campaigns/:id/send and redirect to the
           live detail page.

- CampaignDetailPage: per-recipient table with status chips updated live
  via EventSource on the campaign:<id> SSE topic. Counters bar
  (envoyés / cliqués / queued / échecs / non envoyés), progress bar,
  per-row customer-link badge with deep-link into /clients/<id>.
  Auto-subscribes to SSE when status is draft|sending; "Lancer l'envoi"
  button for draft campaigns.

- TemplateEditorPage: GrapesJS-based visual editor for the campaign
  templates. Three view modes (Visuel / HTML / Aperçu) — the HTML mode
  is the fallback for our table-heavy hand-crafted template that
  GrapesJS-preset-newsletter may parse imperfectly. Aperçu mode calls
  POST /campaigns/templates/:name/preview on the hub for live variable
  substitution. Custom GrapesJS blocks under "Variables" category for
  drag-drop insertion of {{firstname}}, {{amount}}, {{gift_url}},
  {{description}}, {{expiry}}, {{commitment_months}}. Saves via PUT
  with hub-side backup of the previous version.

Wiring:

- api/campaigns.js: hubFetch wrapper, exports parseCsvs / createCampaign
  / listCampaigns / getCampaign / updateCampaign / sendCampaign +
  campaignSseUrl(id) for EventSource subscription, + listTemplates /
  getTemplate / saveTemplate / previewTemplate for the editor.

- router/index.js: three new routes under /campaigns. The
  /campaigns/templates/:name? route is positioned ABOVE /campaigns/:id
  to prevent the wildcard from catching template paths.

- config/nav.js + layouts/MainLayout.vue: "Campagnes" sidebar entry with
  Lucide Gift icon.

- package.json: grapesjs + grapesjs-preset-newsletter dependencies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:08:04 -04:00

33 lines
1.6 KiB
JavaScript

// Ops sidebar navigation + search filter options
// `requires` = capability needed to see this nav item (null = always visible)
export const navItems = [
{ path: '/', icon: 'LayoutDashboard', label: 'Tableau de bord', requires: 'view_dashboard_kpi' },
{ path: '/clients', icon: 'Users', label: 'Clients', requires: 'view_clients' },
{ path: '/dispatch', icon: 'Truck', label: 'Dispatch', requires: 'view_all_jobs' },
{ path: '/tickets', icon: 'Ticket', label: 'Tickets', requires: 'view_all_tickets' },
{ path: '/equipe', icon: 'UsersRound', label: 'Équipe', requires: 'manage_users' },
{ path: '/rapports', icon: 'BarChart3', label: 'Rapports', requires: 'view_dashboard_kpi' },
{ path: '/campaigns', icon: 'Gift', label: 'Campagnes', requires: 'manage_users' },
{ path: '/settings', icon: 'Settings', label: 'Paramètres', requires: 'view_settings' },
]
export const territoryOptions = [
{ label: 'Gatineau', value: 'Gatineau' },
{ label: 'Ottawa', value: 'Ottawa' },
{ label: 'Aylmer', value: 'Aylmer' },
{ label: 'Hull', value: 'Hull' },
{ label: 'Buckingham', value: 'Buckingham' },
{ label: 'Masson-Angers', value: 'Masson-Angers' },
]
export const statusOptions = [
{ label: 'Actif', value: 'Active' },
{ label: 'Inactif', value: 'Inactive' },
{ label: 'En attente', value: 'Pending' },
]
export const customerTypeOptions = [
{ label: 'Individu', value: 'Individual' },
{ label: 'Entreprise', value: 'Company' },
]