- opTextUnfulfilled / opTextShipped editabili dal merchant, con segnaposto
{{returnAddress}} {{returnCost}} {{orderState}} {{customerName}} {{orderName}}
{{shopName}}. Default = testi attuali. returnCost deriva da returnAtCustomerExpense
(a tuo/nostro carico, Art. 57). returnInstructions deprecato (assorbito da opTextShipped).
- Pagina Impostazioni riorganizzata in 4 tab (Email / Notifiche / Regole recesso /
Reso e stato ordine) + anteprima live del riquadro per stato (non evaso e spedito).
- emailTemplate: renderOperationalBlock ora usa i template + sostituzione segnaposto;
renderOperationalPreview esportato per l'admin. Migrazione op_texts (additiva).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
193 lines
8.9 KiB
TypeScript
193 lines
8.9 KiB
TypeScript
/**
|
|
* Email ricevuta recesso - layout FISSO (stile Shopify), con solo alcuni TESTI
|
|
* editabili dal merchant: oggetto, introduzione, nota opzionale. Le parti legali
|
|
* (dichiarazione, timestamp, avviso supporto durevole art. 54-bis), la struttura
|
|
* e il footer sono fissi -> sempre conforme e non rompibile.
|
|
*
|
|
* Modulo PURO (niente node/server): usato dal mailer e dall'anteprima admin.
|
|
* Segnaposto ammessi nei testi: {{customerName}} {{orderName}} {{shopName}}.
|
|
*/
|
|
|
|
export interface EmailVars {
|
|
shopName: string;
|
|
shopUrl: string;
|
|
orderName: string;
|
|
orderUrl: string;
|
|
customerName: string;
|
|
transmittedAt: string;
|
|
statementText: string;
|
|
}
|
|
|
|
// Segnaposto usabili nei campi di testo editabili.
|
|
export const TEXT_PLACEHOLDERS = ["customerName", "orderName", "shopName"] as const;
|
|
|
|
export const DEFAULT_SUBJECT =
|
|
"{{shopName}} - Ricevuta della tua richiesta di recesso - Ordine {{orderName}}";
|
|
|
|
export const DEFAULT_INTRO =
|
|
"Gentile {{customerName}},\n\nconfermiamo di aver ricevuto la tua dichiarazione di recesso relativa all'ordine {{orderName}}.";
|
|
|
|
export const DEFAULT_NOTE = "";
|
|
|
|
function escHtml(s: string): string {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
function nl2br(s: string): string {
|
|
return s.replace(/\n/g, "<br>");
|
|
}
|
|
|
|
/** Sostituzione segnaposto in testo semplice (valori grezzi). Per l'oggetto. */
|
|
function substPlain(tpl: string, vars: EmailVars): string {
|
|
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k: string) =>
|
|
String((vars as unknown as Record<string, string>)[k] ?? ""),
|
|
);
|
|
}
|
|
|
|
/** Testo del merchant -> HTML sicuro: escape del letterale + segnaposto (valori escaped) + a-capo. */
|
|
function richText(tpl: string, vars: EmailVars): string {
|
|
const escaped = escHtml(tpl);
|
|
const withVars = escaped.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k: string) =>
|
|
escHtml(String((vars as unknown as Record<string, string>)[k] ?? "")),
|
|
);
|
|
return nl2br(withVars);
|
|
}
|
|
|
|
export function renderSubject(subjectTpl: string | null | undefined, vars: EmailVars): string {
|
|
const tpl = (subjectTpl && subjectTpl.trim()) || DEFAULT_SUBJECT;
|
|
return substPlain(tpl, vars).trim() || substPlain(DEFAULT_SUBJECT, vars);
|
|
}
|
|
|
|
// Testi del riquadro operativo (A6-bis) - editabili dal merchant, con segnaposto.
|
|
export const OP_PLACEHOLDERS = [
|
|
"returnAddress",
|
|
"returnCost",
|
|
"orderState",
|
|
"customerName",
|
|
"orderName",
|
|
"shopName",
|
|
] as const;
|
|
|
|
export const DEFAULT_OP_UNFULFILLED =
|
|
"Il tuo ordine non risultava ancora spedito: procederemo all'annullamento e al rimborso. Non devi restituire nulla.";
|
|
|
|
export const DEFAULT_OP_SHIPPED =
|
|
"Il prodotto risulta {{orderState}}. Per ottenere il rimborso, restituisci la merce integra a: {{returnAddress}}. Le spese di restituzione sono {{returnCost}}. Il rimborso sarà disposto dopo il rientro della merce.";
|
|
|
|
export interface OperationalConfig {
|
|
state: "unfulfilled" | "shipped" | "delivered";
|
|
textUnfulfilled?: string | null;
|
|
textShipped?: string | null;
|
|
returnAddress?: string | null;
|
|
atCustomerExpense: boolean;
|
|
}
|
|
|
|
/** Blocco operativo per stato ordine (A6-bis): testo editabile + segnaposto. */
|
|
function renderOperationalBlock(op: OperationalConfig, vars: EmailVars): string {
|
|
const tpl =
|
|
op.state === "unfulfilled"
|
|
? (op.textUnfulfilled && op.textUnfulfilled.trim()) || DEFAULT_OP_UNFULFILLED
|
|
: (op.textShipped && op.textShipped.trim()) || DEFAULT_OP_SHIPPED;
|
|
const map: Record<string, string> = {
|
|
returnAddress:
|
|
op.returnAddress && op.returnAddress.trim()
|
|
? op.returnAddress.trim()
|
|
: "l'indirizzo che ti comunicheremo",
|
|
returnCost: op.atCustomerExpense ? "a tuo carico" : "a nostro carico",
|
|
orderState: op.state === "delivered" ? "consegnato" : "spedito",
|
|
customerName: vars.customerName,
|
|
orderName: vars.orderName,
|
|
shopName: vars.shopName,
|
|
};
|
|
const withVars = escHtml(tpl).replace(
|
|
/\{\{\s*(\w+)\s*\}\}/g,
|
|
(_m, k: string) => escHtml(map[k] ?? ""),
|
|
);
|
|
const inner = nl2br(withVars);
|
|
return `<tr><td style="padding:4px 32px 8px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:14px 16px;background:#fff7e6;border:1px solid #ffe2a8;border-radius:8px;font-size:14px;line-height:1.6;color:#6a5518;">${inner}</td></tr></table>
|
|
</td></tr>`;
|
|
}
|
|
|
|
/** Anteprima del riquadro operativo per un dato stato (per l'admin). */
|
|
export function renderOperationalPreview(
|
|
op: OperationalConfig,
|
|
vars: EmailVars,
|
|
): string {
|
|
return renderOperationalBlock(op, vars);
|
|
}
|
|
|
|
/** Corpo HTML fisso con intro/nota editabili inseriti. */
|
|
export function renderReceiptHtml(
|
|
vars: EmailVars,
|
|
introTpl: string | null | undefined,
|
|
noteTpl: string | null | undefined,
|
|
operational?: OperationalConfig | null,
|
|
): string {
|
|
const intro = richText((introTpl && introTpl.trim()) || DEFAULT_INTRO, vars);
|
|
const noteVal = noteTpl && noteTpl.trim() ? richText(noteTpl, vars) : "";
|
|
const note = noteVal
|
|
? `<tr><td style="padding:4px 32px 8px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:14px 16px;background:#f6f6f7;border-radius:8px;font-size:14px;line-height:1.6;color:#444;">${noteVal}</td></tr></table>
|
|
</td></tr>`
|
|
: "";
|
|
|
|
return `<!doctype html>
|
|
<html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"></head>
|
|
<body style="margin:0;padding:0;background:#f4f4f5;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;">
|
|
<tr><td align="center" style="padding:24px 12px;">
|
|
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="width:100%;max-width:560px;background:#ffffff;border:1px solid #e5e5e5;border-radius:12px;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
|
<tr><td style="height:4px;line-height:4px;font-size:0;background:#1a1a1a;"> </td></tr>
|
|
<tr><td style="padding:22px 32px 18px;border-bottom:1px solid #ececec;">
|
|
<a href="${escHtml(vars.shopUrl)}" style="text-decoration:none;color:#1a1a1a;font-size:17px;font-weight:600;">${escHtml(vars.shopName)}</a>
|
|
</td></tr>
|
|
<tr><td style="padding:26px 32px 6px;">
|
|
<h1 style="margin:0 0 14px;font-size:20px;line-height:1.3;color:#1a1a1a;">Ricevuta della richiesta di recesso</h1>
|
|
<p style="margin:0;font-size:15px;line-height:1.6;color:#3a3a3a;">${intro}</p>
|
|
</td></tr>
|
|
<tr><td style="padding:16px 32px 6px;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f6f7;border-radius:8px;">
|
|
<tr><td style="padding:14px 16px;font-size:14px;line-height:1.9;color:#444;">
|
|
<div><span style="color:#777;">Data e ora di trasmissione:</span> <strong>${escHtml(vars.transmittedAt)}</strong></div>
|
|
<div><span style="color:#777;">Ordine:</span> <strong>${escHtml(vars.orderName)}</strong></div>
|
|
<div><span style="color:#777;">Nome:</span> <strong>${escHtml(vars.customerName)}</strong></div>
|
|
</td></tr></table>
|
|
</td></tr>
|
|
<tr><td style="padding:14px 32px 6px;">
|
|
<div style="font-size:13px;font-weight:600;color:#555;margin-bottom:6px;">Dichiarazione trasmessa</div>
|
|
<div style="border-left:3px solid #d9d9d9;padding:8px 14px;font-size:14px;line-height:1.6;color:#555;font-style:italic;">${escHtml(vars.statementText)}</div>
|
|
</td></tr>
|
|
<tr><td style="padding:8px 32px 4px;">
|
|
<a href="${escHtml(vars.orderUrl)}" style="display:inline-block;padding:10px 18px;background:#1a1a1a;color:#ffffff;text-decoration:none;border-radius:8px;font-size:14px;font-weight:600;">Vedi il tuo ordine</a>
|
|
</td></tr>
|
|
<tr><td style="padding:12px 32px 4px;">
|
|
<p style="margin:0;font-size:12.5px;line-height:1.6;color:#8a8a8a;">Questa comunicazione costituisce la ricevuta su supporto durevole ai sensi dell'art. 54-bis del Codice del Consumo. La data e l'ora indicate attestano il momento della trasmissione.</p>
|
|
</td></tr>
|
|
${operational ? renderOperationalBlock(operational, vars) : ""}
|
|
${note}
|
|
<tr><td style="padding:18px 32px 24px;border-top:1px solid #ececec;">
|
|
<p style="margin:0 0 8px;font-size:12.5px;line-height:1.6;color:#999;">Ti invieremo separatamente le istruzioni per l'eventuale reso e i tempi di rimborso.</p>
|
|
<p style="margin:0 0 10px;font-size:12px;line-height:1.6;color:#aaa;">Questa funzione e' aggiuntiva: restano validi il modulo tipo (Allegato I, parte B) e la dichiarazione via email.</p>
|
|
<p style="margin:0;font-size:12px;line-height:1.6;color:#999;"><a href="${escHtml(vars.shopUrl)}" style="color:#999;text-decoration:underline;">${escHtml(vars.shopName)}</a></p>
|
|
</td></tr>
|
|
</table>
|
|
</td></tr></table>
|
|
</body></html>`;
|
|
}
|
|
|
|
/** Dati fittizi per l'anteprima admin. */
|
|
export const SAMPLE_VARS: EmailVars = {
|
|
shopName: "Il tuo negozio",
|
|
shopUrl: "https://esempio.myshopify.com",
|
|
orderName: "#1001",
|
|
orderUrl: "https://esempio.myshopify.com/orders/xyz",
|
|
customerName: "Mario Rossi",
|
|
transmittedAt: "07/07/2026, 11:32:07 CEST",
|
|
statementText:
|
|
"Con la presente comunico il recesso dal contratto di vendita relativo all'ordine #1001.",
|
|
};
|