a6cd6ee453
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a6cd6ee453 | fix(ops/campaigns): v-pre on translation dialog hint to avoid Vue parser crash on {{ '{{...}}' }} | ||
|
|
1b399f65eb |
feat(campaigns): AI template translator via Gemini Flash
New "Traduire (AI)" button in the template editor toolbar. One click
translates the current template's HTML to the opposite language
(detected from the -fr/-en suffix), writing the translated content as
the matching companion template.
Backend (lib/campaigns.js):
- New endpoint: POST /campaigns/templates/:name/translate-to/:targetName
- Reads source .html, calls lib/ai.js aiCall() with Gemini Flash
- System prompt enforces 7 strict preservation rules:
1. Byte-preserve all HTML tags/attributes/styles/Outlook conditionals
2. Don't translate Mustache {{vars}}
3. Preserve URLs/emails/phones/hex colors/CSS/brand names (TARGO,
Gigafibre, Giftbit, Amazon, IGA, Tim Hortons, etc.)
4. Preserve emojis (🎁 ⚡ 🤝 🪂 ✅ ⏭️ ⏰)
5. Keep the warm informal tone (tu in FR, you in EN)
6. Translate only visible text inside elements (paragraphs, buttons,
alt attributes, link text)
7. Output full HTML doc only, no markdown wrapping
- temperature=0.2 for stable output, maxTokens=32768 to fit ~35 KB HTML
- Sanity validates output isn't truncated (>50% of source size)
- Strips defensive markdown fences if AI ignored rule 7
- Auto-backs up existing target before overwrite
- Regenerates Unlayer design JSON from the translated HTML so the
editor can reload the translated template visually
- Requires { override: true } in body to overwrite existing target
(409 Conflict otherwise — protects against accidental clobber)
API client (apps/ops/src/api/campaigns.js):
- translateTemplate(srcName, targetName, { override })
Frontend (TemplateEditorPage.vue):
- "Traduire (AI)" button (purple, icon=translate) in toolbar — disabled
when current template has no -fr/-en suffix
- aiTranslateTargetName computed: detects source lang from suffix,
flips to opposite (-fr → -en, -en → -fr)
- Confirmation dialog:
• Shows source → target template names
• Info banner explaining what's preserved (HTML, vars, brands, emojis)
• Amber banner + toggle if target exists (must confirm override)
- On success: positive notification with byte counts +
"Open" action button to jump to the translated template
- Refreshes templates list after translation so the new file appears
in the selector dropdown
UX: replaces the previous manual translation workflow (where the user
or I had to maintain two parallel templates). One click now does the
whole round-trip. User reviews + adjusts wording in the EN editor if
the AI translation needs polish.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
73e4118901 |
feat(campaigns): create new templates from UI + enable Unlayer template library
Two improvements to the template editor:
1. "+ Nouveau" button + creation dialog
Users can now create new templates from the editor UI without us
re-deploying the hub. Click "Nouveau" next to the template selector,
pick a name + prefix + starter (blank or copy from existing), submit.
The hub PUTs the new template (existing endpoint, no new code needed
on the backend — just relaxed validation).
Form:
• Type (prefix): gift-email / newsletter / transactional
• Name suffix: lowercase letters/digits/dashes (e.g. summer-2026)
• Starter: "Vide" or "Copier depuis <existing template>"
On submit:
• If starter != blank: GET source template's html + design
• PUT new template name with that content
• Refresh templates list + switch editor to the new one
2. Backend: replace hardcoded EDITABLE_TEMPLATES allow-list with
regex-validated prefix matching + disk scan
• EDITABLE_TEMPLATE_PREFIXES = ['gift-email-', 'newsletter-',
'transactional-'] — bounds what categories users can create
• TEMPLATE_NAME_RE = /^[a-z0-9-]+$/ — prevents path traversal
• isValidTemplateName() validates both regex + prefix membership
• scanEditableTemplates() returns all matching .html/.mjml files
currently on disk (excludes .bak-* and .legacy-* variants)
• listEditableTemplates() now scans disk instead of a static list,
so newly-created templates appear automatically in the dropdown
3. Enable Unlayer's built-in panels
• templates: true — exposes Unlayer's template library (limited
free-tier selection but ~10-20 starters available without a
projectId)
• stockImages: true — Unsplash search built into image picker
• imageEditor: true — basic crop/resize on inserted images
• undoRedo: true — history navigation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4acb18c7df |
fix(ops/campaigns): drop loadBlank() call + force explicit editor dimensions
Two bugs from the first prod test of the Unlayer editor:
1. `editor.value.loadBlank is not a function` — the loadBlank() method
exists in newer Unlayer versions but NOT in vue-email-editor 2.2
which wraps an older Unlayer. When no design is stored yet, just let
the editor render its default empty state ("No content here. Drag
content from left.") and show a Quasar notification telling the
user how to start. No explicit load call needed.
2. Editor renders cramped/small — the EmailEditor component's nested
iframe doesn't inherit dimensions from Quasar's q-page wrapper.
Wrap the EmailEditor in an explicit-sized container:
<div style="height: calc(100vh - 60px); width: 100%; overflow: hidden;">
Plus pass style="height: 100%; width: 100%" to the EmailEditor itself.
This gives the editor a full viewport-minus-toolbar canvas.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9dcd32ef6a |
feat(ops/campaigns): group merge tags by category + add toolbar hint
Improvements to the variable insertion UX in the Unlayer editor:
1. Reorganized mergeTags from a flat object into 3 logical groups so
Unlayer's "Merge Tags" dropdown shows them under sub-headers
instead of a long flat list:
• Client (firstname, lastname, email, description)
• Offre (amount, gift_url, expiry, commitment_months)
• Système (year)
Format switched from { id: {name, value} } to grouped array
format (Unlayer accepts both, but groups give better UX).
2. Added `sample` field to each merge tag — Unlayer renders these
as the visible content while editing, so the canvas shows
"Louis Tremblay" / "60 $" / "https://gft.link/abc" instead of
literal "{{firstname}} {{lastname}}". Makes the live preview
look like real content during edit. Substitution still happens
server-side at send time via Mustache.
3. New toolbar hint button (code icon, grey) explaining where to
find merge tags in the Unlayer UI:
"Insertion : clic dans un texte → barre flottante → icône {}
Merge Tags. Marche aussi dans les champs URL (boutons, images,
mailto)."
This addresses a common discoverability issue: users don't
always realize variables work in URL fields too (e.g. setting
a button's "Action URL" to {{gift_url}} so each recipient gets
their own Giftbit link).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a11fe5a115 |
feat(ops/campaigns): pivot template editor to Unlayer (vue-email-editor)
After honest acknowledgment that easy-email-standard is abandoned and
limited (Chrome-only, no responsive preview, no AMP, no Unsplash, no
file manager), pivoted to Unlayer's vue-email-editor — a Vue 3 native
component giving all the features the user listed for free (internal
use; a small "Powered by Unlayer" badge shows in the sidebar but NOT
in sent emails).
Why drop MJML alongside:
• MJML was our SERVER-SIDE compilation step because we hand-wrote
templates. With a visual editor that outputs email-safe HTML
directly (responsive media queries, Outlook MSO fallbacks, AMP
where used), the compilation step is redundant.
• One fewer dependency on the hub (mjml package no longer needed).
• One fewer file format to persist (.mjml dropped, only .html
canonical + .json design).
Storage simplification:
Before: .mjml (source) + .html (compiled) + .json (editor state)
After: .html (canonical) + .json (Unlayer design tree)
The hub's send-worker reads .html as before — no changes to send
logic.
Architecture wins:
• Vue 3 native — zero iframe friction, no postMessage choreography
• No separate microservice — easy-email container decommissioned
(docker compose down, code kept under /opt/email-editor/ in case
of rollback)
• DNS editor.gigafibre.ca retained but unused — can be removed via
Cloudflare API cleanup later
• The editor's mergeTags option exposes our {{firstname}}, {{amount}},
{{gift_url}}, etc. in Unlayer's native "Merge tags" panel — same
pattern, more polished UI
• Features now native: responsive preview (mobile/tablet/desktop
breakpoints), Unsplash search, file manager, dark mode, design
history, undo/redo, layers panel, content blocks library
Frontend (TemplateEditorPage.vue):
• Imports EmailEditor from vue-email-editor
• onReady() callback: fetch template + loadDesign() to restore canvas
• saveTemplate(): exportHtml() → PUT { html, design } to hub
• Top bar kept: template selector, saved chip, preview, test-send,
save button
• Removed: iframe-related glue (postMessage listener, iframeKey,
EDITOR_BASE constant, Cmd-S handling that lived in the iframe)
API client (apps/ops/src/api/campaigns.js):
• saveTemplate() now accepts opts.design (Unlayer JSON tree) alongside
content. Legacy opts.format='mjml' still works for backward compat.
Hub (services/targo-hub/lib/campaigns.js):
• GET /campaigns/templates/:name unconditionally returns
{ name, format, html, design } (+ mjml when format=mjml for
legacy templates). The design field is null when no .json file
exists yet.
• PUT /campaigns/templates/:name HTML save path now accepts
body.design alongside body.html and persists both with backups.
• MJML save path (legacy) preserved for any callers using the old
contract.
Container decommissioned on prod: email-editor container stopped +
removed. The Vue editor lives inside the ops SPA, served from
erp.gigafibre.ca/ops as a normal route.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f9971e9113 |
feat(ops/campaigns): Phase 2 — switch editor page to easy-email iframe
Replace the broken GrapesJS-mjml integration with an iframe pointing to
the standalone email-editor microservice at editor.gigafibre.ca (created
in Phase 1).
What changed:
- Dropped all grapesjs* imports and ~250 lines of editor init/save/preview
glue code. That logic now lives in the React app on the other side of
the iframe.
- Page becomes a thin wrapper:
• Top bar: back button, template selector, "saved" chip,
"Aperçu inbox" button, "Envoyer un test" button, reload button.
• Below: full-height iframe to editor.gigafibre.ca/?name=<template-name>.
- Template switching: bumping iframeKey forces a fresh iframe load so the
new ?name= param takes effect. Route is updated via router.replace.
- postMessage listener: receives { type: 'email-editor:saved', ts }
from the editor iframe and shows a positive toast + updates the
"Sauvegardé · il y a Xs" chip. Origin-checked against EDITOR_BASE.
- Preview dialog: unchanged — fetches compiled HTML from hub's preview
endpoint and renders in srcdoc iframe.
- Test-send dialog: unchanged from previous version.
Removed (now handled inside the iframe):
- Visual / HTML / Aperçu view-mode toggle (editor.gigafibre.ca handles
all editing modes natively)
- "Vide" / "Réinitialiser" buttons (editor has its own)
- "Annuler" / "Enregistrer" buttons (editor saves itself on Cmd-S /
toolbar button)
- spell-check on textarea (editor handles it)
- GrapesJS asset manager wiring (editor will use its own image picker
in Phase 3)
DNS prerequisite handled separately: editor.gigafibre.ca → 96.125.196.67
created via Cloudflare API (proxied=false to match the existing pattern
that lets Traefik handle Let's Encrypt directly).
Container running on prod via /opt/email-editor/docker-compose.yml,
Traefik routing to Host(`editor.gigafibre.ca`). HTTPS verified live.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
79ae38db60 |
feat(campaigns): MJML canonical templates + test-send button
Two big moves:
1. Promote MJML to the canonical template format
- Move gift-email-fr-mjml.{mjml,html} → gift-email-fr.{mjml,html}
- Create gift-email-en.mjml (English translation of FR MJML)
- Compile EN MJML → gift-email-en.html
- Remove obsolete variants:
• gift-email-fr-simple.html (now replaced by MJML)
• gift-email-en-simple.html (same)
• gift-email-fr-mjml.* (renamed to canonical)
- The old gift-email-fr.html (rich-with-merchant-grid version) is
backed up as gift-email-fr.legacy-rich.html.bak — kept on disk
for reference but not in the editable list.
- EDITABLE_TEMPLATES is now just ['gift-email-fr', 'gift-email-en'],
both backed by .mjml source + .html auto-compiled output.
2. Add "Envoyer un test" feature
Backend:
- POST /campaigns/templates/:name/test-send accepts { to, vars,
from?, subject? }. Reads compiled .html, renders Mustache vars,
sends via Mailjet through email.sendEmail with X-MJ-CustomID
"test-send:<name>:<timestamp>" so webhook events for tests are
identifiable. Returns { sent, to, from, message_id, bytes }.
- Default vars are sensible: firstname="Louis", amount="60 $",
gift_url="https://gft.link/TEST123", etc. User overrides any
via the request body.
Frontend (TemplateEditorPage):
- Toolbar button "Envoyer un test" (orange) — opens a dialog.
- Dialog has email input + subject + 7 variable inputs
(firstname, lastname, amount, commitment_months, gift_url,
description, expiry) with sensible defaults.
- "Dirty" banner warning: if the user has unsaved changes, the
test will use the LAST SAVED version (so save first to test the
latest). Mentions explicitly in card footer.
- On send: live notification with the message_id + byte count.
Errors surface clearly.
Verified live in prod:
POST /campaigns/templates/gift-email-fr/test-send → 200, message_id
returned, ~32 KB rendered MJML→HTML output, sent from
TARGO <support@targointernet.com> (Mailjet-validated sender).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b37270c11d |
feat(campaigns/editor): MJML mode — proper email-focused visual builder
Pivot the template editor toward email-marketing-grade visual editing
by replacing grapesjs-preset-newsletter (permissive HTML, fails to parse
nested table structures) with grapesjs-mjml (the industry-standard
email markup language used by Mailchimp/Sendgrid/Twilio).
Why MJML: it was specifically designed to solve the "visual editor +
email-safe HTML" problem. You write semantic <mj-section>, <mj-column>,
<mj-button>, <mj-image> components — MJML compiles them to the gnarly
email-safe HTML with Outlook fallbacks + responsive media queries
auto-generated. Source is 3x more compact than hand-written HTML and
parses cleanly in visual editors.
Backend (lib/campaigns.js):
- Add `mjml` (v5, async) dependency. Compilation happens server-side
at SAVE time only; the send-worker reads pre-compiled .html (no
per-recipient compile cost).
- Each template can now be in 'mjml' or 'html' format. Detection by
file extension on disk: .mjml present → format='mjml', otherwise
format='html'. Source of truth for MJML templates = .mjml file;
.html is the auto-compiled output kept alongside for the send-worker.
- GET /campaigns/templates → returns { name, format, size } per template.
- GET /campaigns/templates/:name → returns { format, mjml?, html }
(mjml field present only when format=mjml; html always present).
- PUT /campaigns/templates/:name accepts:
{ mjml: "<mjml>..." } → compile to HTML, save both .mjml + .html
{ html: "..." } → save .html only (legacy path, unchanged)
Compilation errors return 400 with details (MJML validation soft mode).
Both files backed up as .bak-<ts>.<ext> before overwrite.
Frontend (TemplateEditorPage.vue):
- Detect format from API response on load.
- For format='mjml': swap grapesjs-preset-newsletter for grapesjs-mjml
plugin. Editor's getHtml() returns MJML source (not compiled HTML);
Save POSTs the MJML, hub compiles + persists both files.
- For format='html': existing behavior unchanged.
- Editor is destroyed + reinitialized when format changes (different
plugin sets).
- Custom variable blocks ({{firstname}}, {{amount}}, etc.) work for
both formats — they're text content, format-agnostic.
API client (apps/ops/src/api/campaigns.js):
- saveTemplate(name, content, { format }) routes to the right PUT body
shape based on format param.
Prototype: gift-email-fr-mjml — full MJML conversion of the simple
variant, ~7.5 KB MJML source compiling to ~32 KB email-safe HTML with
0 validation errors. All 6 Mustache variables preserved through
compilation (firstname, amount, gift_url, description, commitment_months,
year). User compares the MJML editor experience to the existing HTML
templates and decides whether to migrate the others.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4a4d145465 |
feat(campaigns/assets): self-hosted image upload + GrapesJS asset manager
Background: existing Mailjet-hosted brand logos in the gift email templates
stay as-is — those URLs are stable and live on Mailjet's CDN. This change
adds infrastructure for ADDITIONAL images the user wants to drop into the
editor going forward (event photos, custom illustrations, technician
photos for service campaigns, etc.) without uploading to Mailjet first.
Why self-hosted: avoids vendor lock-in for new assets, gives us control
over retention + immutable URLs, integrates natively with our GrapesJS
editor's AssetManager. The cost is ~5 MB max per image and one new bind
mount on the hub.
Backend (lib/campaigns.js):
- Storage at services/targo-hub/uploads/ (new bind mount, RW, mounted into
the container at /app/uploads). Files named by SHA-256 of content for:
• Automatic dedup (same image twice → same URL, no extra disk)
• Immutable URLs (content never changes for a given filename)
• Path-traversal defence (regex-locked filename pattern)
- POST /campaigns/assets/upload — accepts JSON { name, data } where data
is a data:image/...;base64,... URL. Decodes, validates MIME against
allow-list (png/jpg/gif/webp/svg), enforces 5 MB cap, hashes, persists,
returns { url, filename, size, content_type, data: [...] }. The `data`
array shape matches what GrapesJS' AssetManager expects on upload
success. Using base64-in-JSON avoids pulling a multipart parser
dependency — the ~33% encoding overhead is fine for ≤5 MB images.
- GET /campaigns/assets — list all uploaded assets with metadata
(filename, url, size, modified, content_type).
- GET /campaigns/assets/:hash.<ext> — serve image bytes with
Content-Type matching the extension + Cache-Control:
public, max-age=31536000, immutable. The 1-year cache is safe because
filename = content hash → URL never serves different bytes. Aligns
with how Gmail's image proxy and Outlook's caching work.
- DELETE /campaigns/assets/:hash.<ext> — admin removal from disk.
- Helpers (persistUpload / readUpload / deleteUpload) live at module
scope so they can call `path.join` (otherwise shadowed by the `path`
URL parameter inside handle()).
API client (apps/ops/src/api/campaigns.js):
- listAssets() → GET /campaigns/assets
- uploadAsset(file) → reads file via FileReader, posts base64 JSON
- deleteAsset(filename) → DELETE the hash-named file
GrapesJS editor (TemplateEditorPage.vue):
- assetManager config with custom uploadFile callback that bypasses
GrapesJS' built-in multipart uploader. Drag-drop or file-picker
triggers our base64 upload, on success the URL is added to the
AssetManager library so it appears in the editor sidebar for reuse.
- onMounted: preload all previously-uploaded assets via listAssets()
so the user sees their image library immediately when opening the
editor (no need to re-upload images used in past campaigns).
End-to-end verified live in prod:
POST /campaigns/assets/upload → 200 (with data URL JSON body)
GET /campaigns/assets → 200 (list)
GET /campaigns/assets/:hash → 200 (serves PNG bytes)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
d6096fe1f8 |
feat(campaigns): apply real TARGO brand + auto-route FR/EN by Customer.language
Brand audit against the official guide (Feb 2026 v1.0) caught several inconsistencies in the email template: - Wrong primary green: was #019547, should be #00C853 (Targo Green from brand palette). Globally replaced. - Wrong gradient: was #019547→#06a04d, should be 135deg #00C853→#005026 (the official Gradient Targo from the brand). Now using Outlook-safe background-image + bgcolor fallback for solid green on Outlook desktop. - Wrong contact info: facturation@targointernet.com / 514 242-1500 → support@targo.ca / 514 448-0773 / 1 855 888-2746 (per §11 of guide). - Wrong website: targointernet.com + gigafibre.ca → www.targo.ca. - Missing slogan + green dot: footer now ends with the trademark tagline "Services de confiance, tout-en-un, près de chez vous." with the obligatory green period (always FR — it's the trademark, not a marketing line, so stays untranslated in EN template too). - Missing brand fonts: added Space Grotesk (display) + Plus Jakarta Sans (body) via Google Fonts. Wrapped in MSO conditional comments so Outlook desktop skips the request and falls back to Helvetica via the explicit font-family stack on every element. - Wrong body bg / text colors: now #F5FAF7 (Muted) / #1B2E24 (Foreground) per brand semantic palette. - Wrong info-pill bg: was #f3f4f3 → #F5FAF7 (Muted). - Added official dark footer band #1C1E26 (Targo Dark) with white inverted wordmark, slogan, address, copyright. Multilang routing (FR/EN): - lib/campaigns.js matchCustomer now fetches Customer.language (14k FR / 1k EN distribution confirmed on prod). Default 'fr' for unmatched contacts. - New templateForLanguage(lang) helper picks gift-email-<lang>.html, falls back to FR. Resolves 'fr-CA' → 'fr' etc. - sendCampaignAsync pre-loads templates per recipient with an in-memory cache to avoid re-reading from disk on every send. - gift-email-en.html created — English translation of the full FR template, keeping the slogan in French (it's the trademark tagline). - year variable now injected (replaces hardcoded © year). UI (CampaignNewPage): - New "Langue" column in the Step 2 recipient table. Shows a clickable chip (FR primary green / EN blue-grey) that toggles language inline, so a campaign manager can override the ERPNext-resolved language per recipient. - Step 3 recap now shows "Répartition par langue: 145 × FR, 12 × EN" before confirming the send. Spell-check: - TemplateEditorPage HTML mode now has spellcheck="true" + dynamic lang attribute on the textarea, picked from the template name suffix (gift-email-fr → fr, gift-email-en → en). Browser's native dictionary flags typos in real time. AI-grade rewrites deferred to the future /campaigns/ai/rewrite endpoint discussed previously. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ff629a6a85 |
feat(campaigns): support Giftbit Link Order CSV + add blank-canvas editor mode
Two issues spotted during first real-data test:
1. parseGiftbitCsv only handled the Campaign-export format (header row
+ columns firstname/lastname/email/gift_url/uuid/...). The Link Order
product Giftbit ships when you pre-buy N links exports a different
format: headerless, one URL per line. Detect this by checking the
first non-empty line: if it starts with http(s):// and has no
comma/pipe/tab separators, treat the whole file as bare URLs. Each
URL maps to one recipient (row-order matching, same as before).
2. The template editor was hard-coded to load the existing
gift-email-fr.html into GrapesJS on mount. Hand-crafted email HTML
with deeply nested tables doesn't parse cleanly into GrapesJS
components, so the visual canvas often renders blank. Two new
toolbar actions to address this:
• "Vide" — clears the canvas to a minimal table-based skeleton.
For composing brand-new templates from scratch in the visual
editor without inheriting the existing template's structure.
Confirms before resetting, then sets dirty=true so the next Save
overwrites the on-disk template (with hub-side backup).
• "Réinitialiser" — reloads the last on-disk version, discarding
any unsaved canvas state. Confirms if dirty.
Plus an amber banner in visual mode (auto-hidden when blank-canvas
is active) explaining that Visual mode is for new templates and
the existing template should be edited in HTML mode.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
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>
|