diff --git a/app/app/lib/emailTemplate.ts b/app/app/lib/emailTemplate.ts index 2fd13e4..0c7571c 100644 --- a/app/app/lib/emailTemplate.ts +++ b/app/app/lib/emailTemplate.ts @@ -61,39 +61,65 @@ export function renderSubject(subjectTpl: string | null | undefined, vars: Email 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; - instructions?: string | null; } -/** Blocco operativo per stato ordine (A6-bis): annullo (non evaso) o istruzioni reso. */ -function renderOperationalBlock(op: OperationalConfig): string { - let inner: string; - if (op.state === "unfulfilled") { - inner = - "Il tuo ordine non risultava ancora spedito: procederemo all'annullamento e al rimborso. Non devi restituire nulla."; - } else { - const stateWord = op.state === "delivered" ? "consegnato" : "spedito"; - const addr = +/** 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 = { + returnAddress: op.returnAddress && op.returnAddress.trim() - ? `${escHtml(op.returnAddress.trim())}` - : "l'indirizzo che ti comunicheremo"; - const spese = op.atCustomerExpense - ? "Le spese di restituzione sono a tuo carico." - : "Le spese di restituzione sono a nostro carico."; - const instr = - op.instructions && op.instructions.trim() - ? `
${nl2br(escHtml(op.instructions.trim()))}` - : ""; - inner = `Il prodotto risulta ${stateWord}. Per ottenere il rimborso, restituisci la merce integra a: ${addr}. ${spese}${instr}
Il rimborso sara' disposto dopo il rientro della merce.`; - } + ? 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 `
${inner}
`; } +/** 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, @@ -141,7 +167,7 @@ export function renderReceiptHtml(

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.

-${operational ? renderOperationalBlock(operational) : ""} +${operational ? renderOperationalBlock(operational, vars) : ""} ${note}

Ti invieremo separatamente le istruzioni per l'eventuale reso e i tempi di rimborso.

diff --git a/app/app/routes/app.settings.tsx b/app/app/routes/app.settings.tsx index a40e91f..2fb37df 100644 --- a/app/app/routes/app.settings.tsx +++ b/app/app/routes/app.settings.tsx @@ -10,6 +10,7 @@ import { Page, Layout, Card, + Tabs, TextField, Button, ButtonGroup, @@ -28,59 +29,67 @@ import { DEFAULT_INTRO, DEFAULT_NOTE, DEFAULT_SUBJECT, + DEFAULT_OP_SHIPPED, + DEFAULT_OP_UNFULFILLED, + OP_PLACEHOLDERS, SAMPLE_VARS, TEXT_PLACEHOLDERS, + renderOperationalPreview, renderReceiptHtml, renderSubject, } from "../lib/emailTemplate"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); - const settings = await db.settings.findUnique({ where: { shop: session.shop } }); + const s = await db.settings.findUnique({ where: { shop: session.shop } }); return { - subject: settings?.emailSubject ?? DEFAULT_SUBJECT, - intro: settings?.emailIntro ?? DEFAULT_INTRO, - note: settings?.emailNote ?? DEFAULT_NOTE, - notifyEnabled: settings?.notifyEnabled ?? true, - notifyEmail: settings?.notifyEmail ?? "", - tagEnabled: settings?.tagEnabled ?? true, - enforceWindow: settings?.enforceWindow ?? false, - windowDays: settings?.defaultWindowDays ?? 14, - enforceExclusions: settings?.enforceExclusions ?? false, - stateAwareEmail: settings?.stateAwareEmail ?? true, - autoCancelUnfulfilled: settings?.autoCancelUnfulfilled ?? false, - returnAtCustomerExpense: settings?.returnAtCustomerExpense ?? true, - returnAddress: settings?.returnAddress ?? "", - returnInstructions: settings?.returnInstructions ?? "", + subject: s?.emailSubject ?? DEFAULT_SUBJECT, + intro: s?.emailIntro ?? DEFAULT_INTRO, + note: s?.emailNote ?? DEFAULT_NOTE, + notifyEnabled: s?.notifyEnabled ?? true, + notifyEmail: s?.notifyEmail ?? "", + tagEnabled: s?.tagEnabled ?? true, + enforceWindow: s?.enforceWindow ?? false, + windowDays: s?.defaultWindowDays ?? 14, + enforceExclusions: s?.enforceExclusions ?? false, + stateAwareEmail: s?.stateAwareEmail ?? true, + autoCancelUnfulfilled: s?.autoCancelUnfulfilled ?? false, + returnAtCustomerExpense: s?.returnAtCustomerExpense ?? true, + returnAddress: s?.returnAddress ?? "", + opTextUnfulfilled: s?.opTextUnfulfilled ?? DEFAULT_OP_UNFULFILLED, + opTextShipped: s?.opTextShipped ?? DEFAULT_OP_SHIPPED, }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.admin(request); - const form = await request.formData(); - const subject = String(form.get("subject") ?? "").trim(); - const intro = String(form.get("intro") ?? "").trim(); - const note = String(form.get("note") ?? "").trim(); + const f = await request.formData(); + const subject = String(f.get("subject") ?? "").trim(); + const intro = String(f.get("intro") ?? "").trim(); + const note = String(f.get("note") ?? "").trim(); + const opUnf = String(f.get("opTextUnfulfilled") ?? "").trim(); + const opShip = String(f.get("opTextShipped") ?? "").trim(); const data = { emailSubject: subject && subject !== DEFAULT_SUBJECT ? subject : null, emailIntro: intro && intro !== DEFAULT_INTRO ? intro : null, emailNote: note || null, - notifyEnabled: form.get("notifyEnabled") === "true", - notifyEmail: String(form.get("notifyEmail") ?? "").trim() || null, - tagEnabled: form.get("tagEnabled") === "true", - enforceWindow: form.get("enforceWindow") === "true", + notifyEnabled: f.get("notifyEnabled") === "true", + notifyEmail: String(f.get("notifyEmail") ?? "").trim() || null, + tagEnabled: f.get("tagEnabled") === "true", + enforceWindow: f.get("enforceWindow") === "true", defaultWindowDays: Math.min( 365, - Math.max(1, Number(form.get("windowDays")) || 14), + Math.max(1, Number(f.get("windowDays")) || 14), ), - enforceExclusions: form.get("enforceExclusions") === "true", - stateAwareEmail: form.get("stateAwareEmail") === "true", - autoCancelUnfulfilled: form.get("autoCancelUnfulfilled") === "true", - returnAtCustomerExpense: form.get("returnAtCustomerExpense") === "true", - returnAddress: String(form.get("returnAddress") ?? "").trim() || null, - returnInstructions: - String(form.get("returnInstructions") ?? "").trim() || null, + enforceExclusions: f.get("enforceExclusions") === "true", + stateAwareEmail: f.get("stateAwareEmail") === "true", + autoCancelUnfulfilled: f.get("autoCancelUnfulfilled") === "true", + returnAtCustomerExpense: f.get("returnAtCustomerExpense") === "true", + returnAddress: String(f.get("returnAddress") ?? "").trim() || null, + opTextUnfulfilled: + opUnf && opUnf !== DEFAULT_OP_UNFULFILLED ? opUnf : null, + opTextShipped: opShip && opShip !== DEFAULT_OP_SHIPPED ? opShip : null, }; await db.settings.upsert({ @@ -88,38 +97,52 @@ export const action = async ({ request }: ActionFunctionArgs) => { create: { shop: session.shop, ...data }, update: data, }); - return { ok: true }; }; +function opFrame(html: string, height = 130) { + const doc = `${html}
`; + return ( +