PSA/server/migrations/utils/templates/email/surveys/surveyTicketClosed.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

174 lines
7.2 KiB
JavaScript

/**
* Source-of-truth: survey-ticket-closed email template.
*
* This template has its own HTML structure (not using the shared emailLayout)
* because the survey email has a unique layout with inline rating buttons and
* a fallback survey link.
*
* Supports all 7 languages: en, fr, es, de, nl, it, pl.
*/
const TEMPLATE_NAME = 'SURVEY_TICKET_CLOSED';
const SUBTYPE_NAME = 'survey-ticket-closed';
const SURVEY_CATEGORY_NAME = 'Surveys';
const SUBJECTS = {
en: "We'd love your feedback on ticket {{ticket_number}}",
fr: 'Votre avis sur le ticket {{ticket_number}} nous int\u00e9resse',
es: 'Queremos conocer tu opini\u00f3n sobre el ticket {{ticket_number}}',
de: 'Wir freuen uns \u00fcber Ihr Feedback zu Ticket {{ticket_number}}',
nl: 'We horen graag uw feedback over ticket {{ticket_number}}',
it: 'Ci farebbe piacere il tuo feedback sul ticket {{ticket_number}}',
pl: 'Ch\u0119tnie poznamy Twoj\u0105 opini\u0119 o zg\u0142oszeniu {{ticket_number}}',
};
/* eslint-disable max-len */
const COPY = {
en: {
salutation: 'Hi {{contact_name}},',
summary: 'Ticket #{{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'Technician: {{technician_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: 'Choose a score below to let us know how we did:',
fallback: 'If the buttons do not load, open this secure survey link:',
thankYou: '{{thank_you_text}}',
},
fr: {
salutation: 'Bonjour {{contact_name}},',
summary: 'Ticket n\u00b0{{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'Technicien : {{technicien_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: 'S\u00e9lectionnez une note ci-dessous pour nous donner votre avis :',
fallback: "Si les boutons ne s'affichent pas correctement, utilisez ce lien s\u00e9curis\u00e9 :",
thankYou: '{{thank_you_text}}',
},
es: {
salutation: 'Hola {{contact_name}},',
summary: 'Ticket #{{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'T\u00e9cnico: {{technician_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: 'Elige una calificaci\u00f3n para contarnos c\u00f3mo fue tu experiencia:',
fallback: 'Si los botones no funcionan, abre este enlace seguro de la encuesta:',
thankYou: '{{thank_you_text}}',
},
de: {
salutation: 'Hallo {{contact_name}},',
summary: 'Ticket Nr. {{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'Techniker: {{technician_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: 'W\u00e4hlen Sie unten eine Bewertung, um uns Ihr Erlebnis mitzuteilen:',
fallback: 'Wenn die Schaltfl\u00e4chen nicht funktionieren, \u00f6ffnen Sie diesen sicheren Link:',
thankYou: '{{thank_you_text}}',
},
nl: {
salutation: 'Hallo {{contact_name}},',
summary: 'Ticket #{{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'Technicus: {{technician_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: 'Kies hieronder een score om te laten weten hoe wij het hebben gedaan:',
fallback: 'Werken de knoppen niet? Gebruik dan deze beveiligde link:',
thankYou: '{{thank_you_text}}',
},
it: {
salutation: 'Ciao {{contact_name}},',
summary: 'Ticket n. {{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'Tecnico: {{technician_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: "Scegli una valutazione qui sotto per dirci com\u2019\u00e8 andata:",
fallback: 'Se i pulsanti non funzionano, apri questo link sicuro:',
thankYou: '{{thank_you_text}}',
},
pl: {
salutation: 'Cze\u015b\u0107 {{contact_name}},',
summary: 'Zg\u0142oszenie #{{ticket_number}} \u00b7 {{ticket_subject}}',
technicianLine: 'Technik: {{technician_name}}',
ratingIntro: '{{prompt_text}}',
buttonHelp: 'Wybierz ocen\u0119 poni\u017cej, aby da\u0107 nam zna\u0107, jak nam posz\u0142o:',
fallback: 'Je\u015bli przyciski si\u0119 nie za\u0142aduj\u0105, otw\u00f3rz ten bezpieczny link do ankiety:',
thankYou: '{{thank_you_text}}',
},
};
/* eslint-enable max-len */
function buildBodyHtml(lang, c, subject) {
return `<!DOCTYPE html>
<html lang="${lang}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${subject}</title>
</head>
<body style="margin:0;padding:0;background-color:#f8fafc;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#0f172a;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f8fafc;padding:24px 0;">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:12px;box-shadow:0 10px 30px rgba(15,23,42,0.08);overflow:hidden;">
<tr>
<td style="background:linear-gradient(135deg,#8A4DEA,#40CFF9);padding:28px 32px;color:#ffffff;">
<h1 style="margin:0;font-size:24px;font-weight:600;">${subject}</h1>
<p style="margin:8px 0 0 0;font-size:14px;opacity:0.85;">${c.summary}</p>
<p style="margin:8px 0 0 0;font-size:14px;opacity:0.85;">${c.technicianLine}</p>
</td>
</tr>
<tr>
<td style="padding:32px;">
<p style="margin:0 0 16px 0;font-size:16px;">${c.salutation}</p>
<p style="margin:0 0 16px 0;font-size:16px;line-height:1.6;">${c.ratingIntro}</p>
<p style="margin:0 0 20px 0;font-size:15px;color:#475569;">${c.buttonHelp}</p>
<div style="text-align:center;margin:24px 0;">
{{rating_buttons_html}}
</div>
<div style="background-color:#f1f5f9;border-radius:10px;padding:16px 20px;margin:24px 0;">
<p style="margin:0;font-size:14px;color:#475569;">${c.fallback}</p>
<p style="margin:12px 0 0 0;font-size:14px;color:#2563eb;word-break:break-all;">
<a href="{{survey_url}}" style="color:#2563eb;text-decoration:none;">{{survey_url}}</a>
</p>
</div>
<p style="margin:0 0 20px 0;font-size:14px;color:#475569;white-space:pre-line;">{{rating_links_text}}</p>
<p style="margin:0;font-size:16px;line-height:1.6;">${c.thankYou}</p>
<p style="margin:20px 0 0 0;font-size:12px;color:#94a3b8;">
{{tenant_name}} \u00b7 Ticket #{{ticket_number}} \u00b7 {{ticket_closed_at}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
function buildText(c, subject) {
return `${subject}
${c.salutation}
${c.summary}
${c.technicianLine}
${c.ratingIntro}
${c.buttonHelp}
${c.fallback}
{{rating_links_text}}
${c.thankYou}
{{tenant_name}} \u00b7 Ticket #{{ticket_number}} \u00b7 {{ticket_closed_at}}`;
}
function getTemplate() {
return {
templateName: TEMPLATE_NAME,
subtypeName: SUBTYPE_NAME,
translations: Object.entries(COPY).map(([lang, copy]) => ({
language: lang,
subject: SUBJECTS[lang],
htmlContent: buildBodyHtml(lang, copy, SUBJECTS[lang]),
textContent: buildText(copy, SUBJECTS[lang]),
})),
};
}
module.exports = { TEMPLATE_NAME, SUBTYPE_NAME, SURVEY_CATEGORY_NAME, getTemplate };