79ae38db60
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
1af8b3a029 |
feat(campaigns/templates): add gift-email-{fr,en}-simple variants
Flat single-table-per-section structure (max 1 level of nesting) so that GrapesJS' preset-newsletter parser can recognize each section as an editable component. Same brand visuals + content as the rich variants, but: dropped the 12-logo merchant grid (heaviest part for the editor), compacted the three info pills into one consolidated card. Sections (top-level <table width="600">): 1. Header logo 2. Greeting + brand-line + offer intro 3. Compact info card (was 3 pills) 4. Option 1 chip 5. Big green CTA button 6. Prorata refund disclaimer 7. Option 2 chip + text 8. Optional expiry notice (Mustache conditional) 9. Signature 10. Contact info (outside card) 11. Dark footer band (logo + address + copyright) Each section is a standalone <table role="presentation" width="600"> sharing the same #ffffff background. The first and last get the rounded border-radius, middle sections have no rounding. Result: visually one unified card, structurally many editable blocks. Registered both new variants in EDITABLE_TEMPLATES whitelist so the ops UI editor picks them up. Rich variants gift-email-fr.html and gift-email-en.html are unchanged — both styles coexist. User compares in the editor and picks which to standardize on per campaign. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
bbd2b31761 |
feat(campaigns/templates): new opening line + logo image in dark footer
Per user feedback after seeing the rendered preview:
1. Opening line replaced:
FR: "Tu choisis local, on veut te remercier." →
"Comme toi, on aime les connexions stables et les relations durables."
EN: "You went local — we want to say thanks." →
"Just like you, we love stable connections and lasting relationships."
The new line ties the Internet service (stable connections) to the
relationship framing (lasting), which reads more naturally than the
previous "we want to thank you" phrasing.
2. Dark footer band cleanup:
• Removed the CSS-styled TARGO. wordmark (with green dot)
• Removed the official slogan line "Services de confiance, ..."
• Replaced with the actual TARGO logo image (img tag at 120px wide)
The wordmark is now ALWAYS the logo image, never a text styling —
keeps the brand mark consistent across header and footer.
TODO marker left in the HTML pointing to the white-variant logo: the
brand guide §1 specifies targo-logo-white.svg for dark backgrounds, but
we only have the green variant uploaded on Mailjet (UUID eed4d18c-...).
The green logo on the #1C1E26 Targo Dark bg is readable but not
pixel-perfect with the brand. To fix, upload the white variant via the
new /campaigns/assets/upload endpoint and swap the src in both
templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
d694d889a1 |
feat(campaigns/templates): replace placehold.co with real Mailjet logos for rows 2-3
User pasted the full HTML block from their Mailjet Passport editor — extracted the 8 missing CDN URLs for the merchant grid bottom rows and swapped them into both FR and EN templates. Final 12-logo grid is now 100% real Mailjet-hosted assets matching the user's brand-approved visuals (no more placehold.co rectangles): Row 1: Amazon, IGA, Tim Hortons, $1 Plus (already real) Row 2: Pizza Pizza, Home Depot, Best Buy, Walmart (NEW) Row 3: Petro-Canada, Esso, Home Hardware, Sobeys (NEW) URL pattern: https://xqy3m.mjt.lu/img2/xqy3m/<UUID>/content Width normalized to 95px (consistent with row 1) instead of the source template's 300px since our 600px-wide email card means each 25% column is ~140px effective — 95px image fits with proper margins. 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> |
||
|
|
9f2b37939d |
feat(campaigns): TARGO rebrand + Mustache sections + Mailjet webhook setup
- Template gift-email-fr.html: switch from Gigafibre indigo to TARGO green (#019547), use real Mailjet-hosted TARGO logo, adopt retention-offer layout from the latest mockup (tutoiement, Option 1/Option 2 split, prorata-refund disclaimer, "L'équipe TARGO" signature). Row 1 of the merchant grid uses real Mailjet logos (Amazon, IGA, Tim Hortons, $1 Plus); rows 2-3 are placehold.co until URLs are shared. - send_gift_campaign.js: add {{#var}}...{{/var}} Mustache section support to the renderer so the optional expiry block disappears cleanly when --expiry is omitted (was rendering literal tags before). Add new --commitment-months CLI flag (default 3) for the "Rester encore X mois ou +" wording. - setup_mailjet_webhook.js (new): one-shot Node script to register the Hub callback URL with Mailjet's /v3/REST/eventcallbackurl. Defaults to a safe event subset (open/click/spam/unsub) that doesn't conflict with the WP-Mail-SMTP integration already owning sent/bounce/blocked. --all forces full takeover with a conflict guard requiring --force-takeover to overwrite existing records. Supports --list and --delete for inspection / rollback. - package.json (new): nodemailer dependency for SMTP send. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
37896421c3 |
feat(campaigns): MVP gift campaign sender (Node CLI + FR email template)
User context: needs to send Giftbit gift cards to 203 customers with a
branded French email instead of Giftbit's English-only default delivery.
Giftbit's own UI/API can issue the gifts but its email is English; this
MVP bridges the gap by taking the gift URLs back from Giftbit, pairing
them with our contact CSV, and sending personalized FR emails through
the Mailjet SMTP that's already wired up for ERPNext invoice mail.
Three files in scripts/campaigns/:
1. send_gift_campaign.js — Node CLI. Two CSV inputs (gifts + contacts),
matches by row order (default) or email key, renders the HTML
template with mustache-style {{firstname}} / {{gift_url}} / etc.,
sends via nodemailer with configurable SMTP + throttle.
--dry-run writes per-recipient previews to disk for visual review
before flipping to live mode. Results CSV with per-row status
(sent / failed / dry-run) + error message + timestamp is written
next to the script for follow-up on failures.
2. templates/gift-email-fr.html — branded French email. Table-based
layout (the only thing that renders consistently in Gmail / Outlook /
iOS Mail / Apple Mail / Bell Sympatico). Indigo gradient header,
centered CTA button, contextual {{description}} line citing the
service address, support contact in the footer, no inline images
(defers to text + colour blocks to dodge image-blocking).
3. contacts_from_legacy.py — replaces the ad-hoc /tmp Python I ran
earlier with a proper repo'd version. Same multi-email handling
options (first / split / skip) as I offered the user; defaults to
"first" = 1 gift per household, which is what they chose. Title-
cases the address with French article rules (de / du / la / aux
stay lowercase, 1re / 2e ordinals stay lowercase too).
4. README.md — end-to-end usage with the actual SMTP env vars from
/opt/targo-hub/.env and the matching strategy decision matrix.
Validated end-to-end with a 5-row dry run: matching works, accents
preserved (Amélie, Geneviève, Marc-André), {{firstname}} interpolates,
gift URLs land in the rendered button href, address shows in the
contextual footer line. Previews written to disk for visual QA.
NOT in this MVP (out of scope, can come next if we end up running
gift campaigns regularly):
- No persistence to ERPNext doctype (no Gift Campaign / Recipient
records — pure CLI, results CSV is the audit trail)
- No click-tracking redirect (the gift_url goes verbatim to the
recipient; Giftbit's own API/dashboard reports redemption status,
which is the more relevant signal than "clicked the link")
- No ops UI page (CLI is fine for one-shot; if this becomes regular
we wrap it in services/targo-hub/lib/gift-campaign.js + a Vue page)
|