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>
33 lines
1.6 KiB
JavaScript
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' },
|
|
]
|