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

468 lines
19 KiB
JavaScript

/**
* Source-of-truth: portal-invitation email template.
*
* Auth templates manage their own full HTML (no shared emailLayout wrapper).
* All languages use the same CSS-class-based styled layout with the brand
* gradient header. English and Polish were originally styled in their own
* migrations; fr/es/de/nl/it were styled via migration 20251029100000.
*/
const TEMPLATE_NAME = 'portal-invitation';
const SUBTYPE_NAME = 'portal-invitation';
/* ------------------------------------------------------------------ */
/* Shared CSS for all portal-invitation templates */
/* ------------------------------------------------------------------ */
const PORTAL_INVITATION_CSS = `
body {
font-family: Inter, system-ui, sans-serif;
line-height: 1.6;
color: #0f172a;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f8fafc;
}
.header {
background: linear-gradient(135deg, #8A4DEA, #40CFF9);
color: white;
padding: 32px 24px;
border-radius: 12px 12px 0 0;
text-align: center;
}
.header h1 {
font-family: Poppins, system-ui, sans-serif;
font-weight: 700;
font-size: 28px;
margin: 0 0 8px 0;
color: white;
}
.header p {
margin: 0;
opacity: 1;
font-size: 16px;
color: rgba(255, 255, 255, 0.95);
}
.content {
background: #ffffff;
padding: 32px;
border: 1px solid #e2e8f0;
border-top: none;
border-bottom: none;
}
.footer {
background: #1e293b;
color: #cbd5e1;
padding: 24px;
border-radius: 0 0 12px 12px;
text-align: center;
font-size: 14px;
line-height: 1.6;
}
.footer p {
margin: 6px 0;
color: #cbd5e1;
}
.footer p:last-child {
color: #94a3b8;
font-size: 13px;
margin-top: 16px;
}
.info-box {
background: #faf8ff;
padding: 24px;
border-radius: 8px;
border: 1px solid #e9e5f5;
border-left: 4px solid #8a4dea;
margin: 24px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.info-box h3 {
color: #0f172a;
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
}
.info-box p {
margin: 8px 0;
color: #334155;
}
.action-button {
display: inline-block;
background: #8a4dea;
color: #ffffff !important;
padding: 14px 32px;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
margin: 24px 0;
font-family: Poppins, system-ui, sans-serif;
font-size: 16px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.action-button:hover {
background: #7c3aed;
color: #ffffff !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.warning {
background: #fffbeb;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
}
.warning h4 {
color: #92400e;
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
}
.warning p {
margin: 0;
color: #92400e;
}
.contact-info {
background: #f8fafc;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
border: 1px solid #e2e8f0;
}
.contact-info h4 {
color: #0f172a;
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
}
.contact-info p {
margin: 4px 0;
color: #334155;
font-size: 14px;
}
h2 {
color: #0f172a;
font-family: Poppins, system-ui, sans-serif;
font-size: 24px;
font-weight: 600;
margin: 0 0 16px 0;
}
p {
color: #334155;
margin: 0 0 16px 0;
}
a {
color: #8a4dea;
text-decoration: underline;
}
a:hover {
color: #7c3aed;
}
.tagline {
background: #faf8ff;
border-left: 3px solid #8a4dea;
padding: 20px 24px;
margin: 24px 0;
font-style: normal;
color: #334155;
border-radius: 6px;
line-height: 1.7;
}
.divider {
height: 1px;
background: #e2e8f0;
margin: 32px 0;
}
.link-text {
word-break: break-all;
font-size: 14px;
color: #64748b;
background: #f8fafc;
padding: 12px;
border-radius: 6px;
border: 1px solid #e2e8f0;
margin: 12px 0;
}`;
/* eslint-disable max-len */
/* ------------------------------------------------------------------ */
/* Per-language copy */
/* ------------------------------------------------------------------ */
const COPY = {
en: {
subject: 'Portal Invitation - {{clientName}}',
title: 'Portal Access Invitation',
headerTitle: 'Welcome to Your Customer Portal',
headerSubtitle: "You're invited to access your account",
greeting: 'Hello {{contactName}},',
intro: "Great news! You've been invited to access the customer portal for <strong>{{clientName}}</strong>. This secure portal gives you instant access to:",
infoBoxTitle: '\ud83c\udfaf What You Can Access',
feature1: '\u2713 View and track your support tickets',
feature2: '\u2713 Review project updates and documentation',
feature3: '\u2713 Communicate directly with your support team',
tagline: 'Experience seamless service management with our intuitive portal. Everything you need to stay informed and connected, all in one secure location.',
buttonLabel: 'Set Up Your Portal Access',
copyLinkHint: 'Or copy and paste this link into your browser:',
warningTitle: '\u23f0 Time-Sensitive Invitation',
warningText: 'This invitation link will expire in <strong>{{expirationTime}}</strong>. Please complete your account setup before then to ensure uninterrupted access.',
contactTitle: 'Need Assistance?',
phoneLabel: 'Phone',
contactHelp: 'Our support team is ready to help you get started.',
footerSent: 'This email was sent to {{contactName}} as part of your portal access setup.',
footerUnexpected: "If you didn't expect this invitation, please contact us at {{clientLocationEmail}}.",
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. All rights reserved.',
},
fr: {
subject: 'Invitation au portail client - {{clientName}}',
title: 'Invitation au portail',
headerTitle: 'Bienvenue sur votre portail client',
headerSubtitle: 'Vous \u00eates invit\u00e9 \u00e0 acc\u00e9der \u00e0 votre compte',
greeting: 'Bonjour {{contactName}},',
intro: "Bonne nouvelle\u00a0! Vous avez \u00e9t\u00e9 invit\u00e9 \u00e0 acc\u00e9der au portail client de <strong>{{clientName}}</strong>. Ce portail s\u00e9curis\u00e9 vous donne un acc\u00e8s instantan\u00e9 \u00e0\u00a0:",
infoBoxTitle: '\ud83c\udfaf Ce \u00e0 quoi vous avez acc\u00e8s',
feature1: '\u2713 Consulter et suivre vos tickets d\u2019assistance',
feature2: '\u2713 Voir les mises \u00e0 jour de projets et la documentation',
feature3: '\u2713 Communiquer directement avec votre \u00e9quipe support',
tagline: "D\u00e9couvrez une gestion de services simplifi\u00e9e gr\u00e2ce \u00e0 notre portail intuitif. Tout ce dont vous avez besoin pour rester inform\u00e9 et connect\u00e9, au m\u00eame endroit.",
buttonLabel: 'Configurer votre acc\u00e8s au portail',
copyLinkHint: 'Ou copiez et collez ce lien dans votre navigateur\u00a0:',
warningTitle: '\u23f0 Invitation \u00e0 dur\u00e9e limit\u00e9e',
warningText: "Ce lien d\u2019invitation expirera dans <strong>{{expirationTime}}</strong>. Veuillez terminer la configuration de votre compte avant cette date pour garantir un acc\u00e8s ininterrompu.",
contactTitle: "Besoin d\u2019aide\u00a0?",
phoneLabel: 'T\u00e9l\u00e9phone',
contactHelp: "Notre \u00e9quipe support est pr\u00eate \u00e0 vous aider \u00e0 d\u00e9marrer.",
footerSent: 'Cet e-mail a \u00e9t\u00e9 envoy\u00e9 \u00e0 {{contactName}} dans le cadre de la configuration de votre acc\u00e8s au portail.',
footerUnexpected: "Si vous n\u2019attendiez pas cette invitation, veuillez nous contacter \u00e0 {{clientLocationEmail}}.",
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. Tous droits r\u00e9serv\u00e9s.',
},
es: {
subject: 'Invitaci\u00f3n al portal del cliente - {{clientName}}',
title: 'Invitaci\u00f3n al portal',
headerTitle: 'Bienvenido a tu portal de cliente',
headerSubtitle: 'Has sido invitado a acceder a tu cuenta',
greeting: 'Hola {{contactName}},',
intro: '\u00a1Buenas noticias! Has sido invitado a acceder al portal de cliente de <strong>{{clientName}}</strong>. Este portal seguro te da acceso instant\u00e1neo a:',
infoBoxTitle: '\ud83c\udfaf A qu\u00e9 puedes acceder',
feature1: '\u2713 Ver y dar seguimiento a tus tickets de soporte',
feature2: '\u2713 Revisar actualizaciones de proyectos y documentaci\u00f3n',
feature3: '\u2713 Comunicarte directamente con tu equipo de soporte',
tagline: 'Disfruta de una gesti\u00f3n de servicios fluida con nuestro portal intuitivo. Todo lo que necesitas para estar informado y conectado, en un solo lugar seguro.',
buttonLabel: 'Configurar tu acceso al portal',
copyLinkHint: 'O copia y pega este enlace en tu navegador:',
warningTitle: '\u23f0 Invitaci\u00f3n con tiempo limitado',
warningText: 'Este enlace de invitaci\u00f3n expirar\u00e1 en <strong>{{expirationTime}}</strong>. Completa la configuraci\u00f3n de tu cuenta antes de esa fecha para garantizar un acceso ininterrumpido.',
contactTitle: '\u00bfNecesitas ayuda?',
phoneLabel: 'Tel\u00e9fono',
contactHelp: 'Nuestro equipo de soporte est\u00e1 listo para ayudarte a comenzar.',
footerSent: 'Este correo fue enviado a {{contactName}} como parte de la configuraci\u00f3n de tu acceso al portal.',
footerUnexpected: 'Si no esperabas esta invitaci\u00f3n, cont\u00e1ctanos en {{clientLocationEmail}}.',
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. Todos los derechos reservados.',
},
de: {
subject: 'Kundenportal-Einladung - {{clientName}}',
title: 'Portal-Einladung',
headerTitle: 'Willkommen in Ihrem Kundenportal',
headerSubtitle: 'Sie sind eingeladen, auf Ihr Konto zuzugreifen',
greeting: 'Hallo {{contactName}},',
intro: 'Gute Nachrichten! Sie wurden eingeladen, auf das Kundenportal von <strong>{{clientName}}</strong> zuzugreifen. Dieses sichere Portal bietet Ihnen sofortigen Zugang zu:',
infoBoxTitle: '\ud83c\udfaf Worauf Sie Zugriff haben',
feature1: '\u2713 Ihre Support-Tickets einsehen und verfolgen',
feature2: '\u2713 Projektaktualisierungen und Dokumentation \u00fcberpr\u00fcfen',
feature3: '\u2713 Direkt mit Ihrem Support-Team kommunizieren',
tagline: 'Erleben Sie nahtloses Service-Management mit unserem intuitiven Portal. Alles, was Sie brauchen, um informiert und verbunden zu bleiben \u2013 an einem sicheren Ort.',
buttonLabel: 'Portalzugang einrichten',
copyLinkHint: 'Oder kopieren Sie diesen Link in Ihren Browser:',
warningTitle: '\u23f0 Zeitlich begrenzte Einladung',
warningText: 'Dieser Einladungslink l\u00e4uft in <strong>{{expirationTime}}</strong> ab. Bitte schlie\u00dfen Sie die Einrichtung Ihres Kontos vorher ab, um einen unterbrechungsfreien Zugang zu gew\u00e4hrleisten.',
contactTitle: 'Brauchen Sie Hilfe?',
phoneLabel: 'Telefon',
contactHelp: 'Unser Support-Team steht bereit, um Ihnen den Einstieg zu erleichtern.',
footerSent: 'Diese E-Mail wurde an {{contactName}} im Rahmen der Einrichtung Ihres Portal-Zugangs gesendet.',
footerUnexpected: 'Wenn Sie diese Einladung nicht erwartet haben, kontaktieren Sie uns bitte unter {{clientLocationEmail}}.',
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. Alle Rechte vorbehalten.',
},
nl: {
subject: 'Uitnodiging voor klantenportaal - {{clientName}}',
title: 'Portaaluitnodiging',
headerTitle: 'Welkom bij uw klantenportaal',
headerSubtitle: 'U bent uitgenodigd om toegang te krijgen tot uw account',
greeting: 'Hallo {{contactName}},',
intro: 'Goed nieuws! U bent uitgenodigd om toegang te krijgen tot het klantenportaal van <strong>{{clientName}}</strong>. Dit beveiligde portaal geeft u direct toegang tot:',
infoBoxTitle: '\ud83c\udfaf Waar u toegang toe hebt',
feature1: '\u2713 Uw supporttickets bekijken en volgen',
feature2: '\u2713 Projectupdates en documentatie bekijken',
feature3: '\u2713 Direct communiceren met uw supportteam',
tagline: 'Ervaar naadloos servicebeheer met ons intu\u00eftieve portaal. Alles wat u nodig hebt om ge\u00efnformeerd en verbonden te blijven, op \u00e9\u00e9n veilige plek.',
buttonLabel: 'Uw portaaltoegang instellen',
copyLinkHint: 'Of kopieer en plak deze link in uw browser:',
warningTitle: '\u23f0 Tijdgebonden uitnodiging',
warningText: 'Deze uitnodigingslink verloopt over <strong>{{expirationTime}}</strong>. Voltooi uw accountconfiguratie v\u00f3\u00f3r die tijd om ononderbroken toegang te garanderen.',
contactTitle: 'Hulp nodig?',
phoneLabel: 'Telefoon',
contactHelp: 'Ons supportteam staat klaar om u op weg te helpen.',
footerSent: 'Deze e-mail is verzonden naar {{contactName}} als onderdeel van uw portaaltoegang.',
footerUnexpected: 'Als u deze uitnodiging niet verwachtte, neem dan contact met ons op via {{clientLocationEmail}}.',
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. Alle rechten voorbehouden.',
},
it: {
subject: 'Invito al portale clienti - {{clientName}}',
title: 'Invito al portale',
headerTitle: 'Benvenuto nel tuo portale clienti',
headerSubtitle: 'Sei stato invitato ad accedere al tuo account',
greeting: 'Ciao {{contactName}},',
intro: 'Ottime notizie! Sei stato invitato ad accedere al portale clienti di <strong>{{clientName}}</strong>. Questo portale sicuro ti d\u00e0 accesso immediato a:',
infoBoxTitle: '\ud83c\udfaf A cosa puoi accedere',
feature1: '\u2713 Visualizzare e monitorare i tuoi ticket di assistenza',
feature2: '\u2713 Consultare aggiornamenti sui progetti e documentazione',
feature3: '\u2713 Comunicare direttamente con il tuo team di supporto',
tagline: 'Sperimenta una gestione dei servizi senza interruzioni con il nostro portale intuitivo. Tutto ci\u00f2 di cui hai bisogno per restare informato e connesso, in un unico luogo sicuro.',
buttonLabel: 'Configura il tuo accesso al portale',
copyLinkHint: 'Oppure copia e incolla questo link nel tuo browser:',
warningTitle: '\u23f0 Invito a tempo limitato',
warningText: "Questo link di invito scadr\u00e0 tra <strong>{{expirationTime}}</strong>. Completa la configurazione del tuo account prima di tale scadenza per garantire un accesso ininterrotto.",
contactTitle: 'Hai bisogno di assistenza?',
phoneLabel: 'Telefono',
contactHelp: 'Il nostro team di supporto \u00e8 pronto ad aiutarti a iniziare.',
footerSent: "Questa e-mail \u00e8 stata inviata a {{contactName}} nell'ambito della configurazione dell'accesso al portale.",
footerUnexpected: 'Se non ti aspettavi questo invito, contattaci all\'indirizzo {{clientLocationEmail}}.',
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. Tutti i diritti riservati.',
},
pl: {
subject: 'Zaproszenie do portalu klienta{{#if clientName}} - {{clientName}}{{/if}}',
title: 'Zaproszenie do portalu',
headerTitle: 'Witamy w portalu klienta',
headerSubtitle: 'Tw\u00f3j dost\u0119p do zarz\u0105dzania us\u0142ugami jest gotowy',
greeting: 'Witaj {{contactName}},',
intro: 'Zosta\u0142e\u015b(a\u015b) zaproszony(a) do portalu klienta {{clientName}}. Ten bezpieczny portal daje Ci natychmiastowy dost\u0119p do:',
infoBoxTitle: 'Tw\u00f3j dost\u0119p obejmuje:',
feature1: '\u2713 Przegl\u0105danie i \u015bledzenie Twoich zg\u0142osze\u0144 serwisowych',
feature2: '\u2713 Przegl\u0105d aktualizacji projekt\u00f3w i dokumentacji',
feature3: '\u2713 Bezpo\u015brednia komunikacja z zespo\u0142em wsparcia',
tagline: null,
buttonLabel: 'Skonfiguruj dost\u0119p do portalu',
copyLinkHint: 'Lub skopiuj i wklej ten link do przegl\u0105darki:',
warningTitle: '\u23f0 Zaproszenie ograniczone czasowo',
warningText: 'Ten link zaproszeniowy wyga\u015bnie za {{expirationTime}}. Doko\u0144cz konfiguracj\u0119 konta przed tym terminem, aby zapewni\u0107 nieprzerwany dost\u0119p.',
contactTitle: 'Potrzebujesz pomocy?',
phoneLabel: 'Telefon',
contactHelp: 'Nasz zesp\u00f3\u0142 wsparcia jest gotowy, aby pom\u00f3c Ci rozpocz\u0105\u0107.',
footerSent: 'Ta wiadomo\u015b\u0107 zosta\u0142a wys\u0142ana do {{contactName}} w ramach konfiguracji dost\u0119pu do portalu.',
footerUnexpected: 'Je\u015bli nie spodziewale\u015b(a\u015b) si\u0119 tego zaproszenia, skontaktuj si\u0119 z nami pod adresem {{clientLocationEmail}}.',
footerCopyright: '\u00a9 {{currentYear}} {{clientName}}. Wszelkie prawa zastrze\u017cone.',
},
};
/* eslint-enable max-len */
/* ------------------------------------------------------------------ */
/* Shared HTML builder */
/* ------------------------------------------------------------------ */
function buildStyledHtml(c) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${c.title}</title>
<style>${PORTAL_INVITATION_CSS}
</style>
</head>
<body>
<div class="header">
<h1>${c.headerTitle}</h1>
<p>${c.headerSubtitle}</p>
</div>
<div class="content">
<h2>${c.greeting}</h2>
<p>${c.intro}</p>
<div class="info-box">
<h3>${c.infoBoxTitle}</h3>
<p>${c.feature1}</p>
<p>${c.feature2}</p>
<p>${c.feature3}</p>
</div>
${c.tagline ? `
<div class="tagline">
${c.tagline}
</div>
` : ''}
<div style="text-align: center;">
<a href="{{portalLink}}" class="action-button">${c.buttonLabel}</a>
</div>
<p style="text-align: center; color: #64748b; font-size: 14px;">
${c.copyLinkHint}
</p>
<div class="link-text">{{portalLink}}</div>
<div class="warning">
<h4>${c.warningTitle}</h4>
<p>${c.warningText}</p>
</div>
<div class="divider"></div>
<div class="contact-info">
<h4>${c.contactTitle}</h4>
<p><strong>Email:</strong> {{clientLocationEmail}}</p>
<p><strong>${c.phoneLabel}:</strong> {{clientLocationPhone}}</p>
<p style="margin-top: 12px; font-size: 13px; color: #64748b;">${c.contactHelp}</p>
</div>
</div>
<div class="footer">
<p>${c.footerSent}</p>
<p>${c.footerUnexpected}</p>
<p>${c.footerCopyright}</p>
</div>
</body>
</html>`;
}
function buildText(c) {
return `${c.headerTitle}
${c.greeting}
${c.intro.replace(/<[^>]+>/g, '')}
${c.feature1}
${c.feature2}
${c.feature3}
${c.tagline ? `\n${c.tagline}\n` : ''}
${c.buttonLabel}: {{portalLink}}
${c.warningTitle}
${c.warningText.replace(/<[^>]+>/g, '')}
${c.contactTitle}
Email: {{clientLocationEmail}}
${c.phoneLabel}: {{clientLocationPhone}}
${c.contactHelp}
---
${c.footerSent}
${c.footerUnexpected}
${c.footerCopyright}`;
}
function getTemplate() {
return {
templateName: TEMPLATE_NAME,
subtypeName: SUBTYPE_NAME,
translations: Object.entries(COPY).map(([lang, copy]) => ({
language: lang,
subject: copy.subject,
htmlContent: buildStyledHtml(copy),
textContent: buildText(copy),
})),
};
}
module.exports = { TEMPLATE_NAME, SUBTYPE_NAME, getTemplate };