A6-bis: testi riquadro operativo editabili (segnaposto) + Impostazioni in tab

- 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
This commit is contained in:
2026-07-07 16:35:12 +02:00
parent a6a1900287
commit a550d8ff2e
5 changed files with 395 additions and 262 deletions

View File

@@ -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<string, string> = {
returnAddress:
op.returnAddress && op.returnAddress.trim()
? `<strong>${escHtml(op.returnAddress.trim())}</strong>`
: "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()
? `<br>${nl2br(escHtml(op.instructions.trim()))}`
: "";
inner = `Il prodotto risulta ${stateWord}. Per ottenere il rimborso, restituisci la merce integra a: ${addr}. ${spese}${instr}<br>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 `<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,
@@ -141,7 +167,7 @@ export function renderReceiptHtml(
<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) : ""}
${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>

View File

@@ -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 = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">${html}</table>`;
return (
<iframe
title="Anteprima riquadro"
srcDoc={doc}
style={{
width: "100%",
height: `${height}px`,
border: "1px solid #e1e1e1",
borderRadius: "8px",
background: "#fff",
}}
/>
);
}
export default function SettingsPage() {
const data = useLoaderData<typeof loader>();
const d = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const nav = useNavigation();
const submit = useSubmit();
const [subject, setSubject] = useState(data.subject);
const [intro, setIntro] = useState(data.intro);
const [note, setNote] = useState(data.note);
const [notifyEnabled, setNotifyEnabled] = useState(data.notifyEnabled);
const [notifyEmail, setNotifyEmail] = useState(data.notifyEmail);
const [tagEnabled, setTagEnabled] = useState(data.tagEnabled);
const [enforceWindow, setEnforceWindow] = useState(data.enforceWindow);
const [windowDays, setWindowDays] = useState(String(data.windowDays));
const [enforceExclusions, setEnforceExclusions] = useState(
data.enforceExclusions,
);
const [stateAwareEmail, setStateAwareEmail] = useState(data.stateAwareEmail);
const [tab, setTab] = useState(0);
const [subject, setSubject] = useState(d.subject);
const [intro, setIntro] = useState(d.intro);
const [note, setNote] = useState(d.note);
const [notifyEnabled, setNotifyEnabled] = useState(d.notifyEnabled);
const [notifyEmail, setNotifyEmail] = useState(d.notifyEmail);
const [tagEnabled, setTagEnabled] = useState(d.tagEnabled);
const [enforceWindow, setEnforceWindow] = useState(d.enforceWindow);
const [windowDays, setWindowDays] = useState(String(d.windowDays));
const [enforceExclusions, setEnforceExclusions] = useState(d.enforceExclusions);
const [stateAwareEmail, setStateAwareEmail] = useState(d.stateAwareEmail);
const [autoCancelUnfulfilled, setAutoCancelUnfulfilled] = useState(
data.autoCancelUnfulfilled,
d.autoCancelUnfulfilled,
);
const [returnAtCustomerExpense, setReturnAtCustomerExpense] = useState(
data.returnAtCustomerExpense,
);
const [returnAddress, setReturnAddress] = useState(data.returnAddress);
const [returnInstructions, setReturnInstructions] = useState(
data.returnInstructions,
d.returnAtCustomerExpense,
);
const [returnAddress, setReturnAddress] = useState(d.returnAddress);
const [opTextUnfulfilled, setOpTextUnfulfilled] = useState(d.opTextUnfulfilled);
const [opTextShipped, setOpTextShipped] = useState(d.opTextShipped);
const [showSaved, setShowSaved] = useState(false);
const saving = nav.state === "submitting";
@@ -136,6 +159,34 @@ export default function SettingsPage() {
() => renderReceiptHtml(SAMPLE_VARS, intro, note),
[intro, note],
);
const previewOpUnf = useMemo(
() =>
renderOperationalPreview(
{
state: "unfulfilled",
textUnfulfilled: opTextUnfulfilled,
textShipped: opTextShipped,
returnAddress,
atCustomerExpense: returnAtCustomerExpense,
},
SAMPLE_VARS,
),
[opTextUnfulfilled, opTextShipped, returnAddress, returnAtCustomerExpense],
);
const previewOpShip = useMemo(
() =>
renderOperationalPreview(
{
state: "shipped",
textUnfulfilled: opTextUnfulfilled,
textShipped: opTextShipped,
returnAddress,
atCustomerExpense: returnAtCustomerExpense,
},
SAMPLE_VARS,
),
[opTextShipped, opTextUnfulfilled, returnAddress, returnAtCustomerExpense],
);
const handleSave = () => {
setShowSaved(false);
@@ -153,20 +204,36 @@ export default function SettingsPage() {
fd.set("autoCancelUnfulfilled", String(autoCancelUnfulfilled));
fd.set("returnAtCustomerExpense", String(returnAtCustomerExpense));
fd.set("returnAddress", returnAddress);
fd.set("returnInstructions", returnInstructions);
fd.set("opTextUnfulfilled", opTextUnfulfilled);
fd.set("opTextShipped", opTextShipped);
submit(fd, { method: "post" });
};
const handleReset = () => {
const resetEmail = () => {
setSubject(DEFAULT_SUBJECT);
setIntro(DEFAULT_INTRO);
setNote(DEFAULT_NOTE);
setShowSaved(false);
};
const saveBtn = (
<div>
<Button variant="primary" loading={saving} onClick={handleSave}>
Salva
</Button>
</div>
);
const tabs = [
{ id: "email", content: "Email" },
{ id: "notifiche", content: "Notifiche" },
{ id: "regole", content: "Regole recesso" },
{ id: "reso", content: "Reso e stato ordine" },
];
return (
<Page>
<TitleBar title="Email di ricevuta" />
<TitleBar title="Impostazioni recesso" />
<Layout>
<Layout.Section>
<BlockStack gap="400">
@@ -176,199 +243,233 @@ export default function SettingsPage() {
</Banner>
) : null}
<Card>
<Tabs tabs={tabs} selected={tab} onSelect={setTab} />
{tab === 0 ? (
<BlockStack gap="400">
<BlockStack gap="100">
<Card>
<BlockStack gap="400">
<BlockStack gap="100">
<Text as="h2" variant="headingMd">
Testi dell'email
</Text>
<Text as="p" tone="subdued">
Oggetto e testi. Dettagli ordine, dichiarazione, data/ora,
avviso di legge e layout sono fissi. Segnaposto:
</Text>
<InlineStack gap="200" wrap>
{TEXT_PLACEHOLDERS.map((p) => (
<Badge key={p}>{`{{${p}}}`}</Badge>
))}
</InlineStack>
</BlockStack>
<TextField
label="Oggetto"
value={subject}
onChange={setSubject}
autoComplete="off"
/>
<TextField
label="Introduzione"
value={intro}
onChange={setIntro}
autoComplete="off"
multiline={4}
/>
<TextField
label="Nota aggiuntiva (opzionale)"
value={note}
onChange={setNote}
autoComplete="off"
multiline={3}
helpText="Riquadro in fondo alla email. Vuoto = nascosto."
/>
<ButtonGroup>
<Button
variant="primary"
loading={saving}
onClick={handleSave}
>
Salva
</Button>
<Button onClick={resetEmail}>Ripristina default</Button>
</ButtonGroup>
</BlockStack>
</Card>
<Card>
<BlockStack gap="200">
<Text as="h2" variant="headingMd">
Anteprima
</Text>
<Text as="p" tone="subdued">
Oggetto: {previewSubject}
</Text>
<iframe
title="Anteprima email"
srcDoc={previewHtml}
style={{
width: "100%",
height: "640px",
border: "1px solid #e1e1e1",
borderRadius: "8px",
background: "#fff",
}}
/>
</BlockStack>
</Card>
</BlockStack>
) : null}
{tab === 1 ? (
<Card>
<BlockStack gap="400">
<Text as="h2" variant="headingMd">
Testi dell'email
Notifiche al merchant
</Text>
<Text as="p" tone="subdued">
Personalizza oggetto e testi. Il resto (dettagli ordine,
dichiarazione, data/ora, avviso di legge, layout) è fisso e
sempre conforme. Segnaposto disponibili:
</Text>
<InlineStack gap="200" wrap>
{TEXT_PLACEHOLDERS.map((p) => (
<Badge key={p}>{`{{${p}}}`}</Badge>
))}
</InlineStack>
<Checkbox
label="Invia email di notifica a ogni recesso"
checked={notifyEnabled}
onChange={setNotifyEnabled}
/>
<TextField
label="Email notifiche"
type="email"
value={notifyEmail}
onChange={setNotifyEmail}
autoComplete="off"
disabled={!notifyEnabled}
helpText="Dove ricevere le notifiche. Senza indirizzo l'email non parte."
placeholder="ordini@tuonegozio.it"
/>
<Checkbox
label="Aggiungi il tag 'Recesso' all'ordine"
checked={tagEnabled}
onChange={setTagEnabled}
helpText="Rende l'ordine filtrabile nella lista ordini."
/>
{saveBtn}
</BlockStack>
</Card>
) : null}
<TextField
label="Oggetto"
value={subject}
onChange={setSubject}
autoComplete="off"
/>
<TextField
label="Introduzione"
value={intro}
onChange={setIntro}
autoComplete="off"
multiline={4}
helpText="Saluto e frase iniziale della email."
/>
<TextField
label="Nota aggiuntiva (opzionale)"
value={note}
onChange={setNote}
autoComplete="off"
multiline={3}
placeholder="Es. Per il reso, spedisci il pacco a..."
helpText="Riquadro in fondo alla email. Lascia vuoto per non mostrarlo."
/>
<ButtonGroup>
<Button variant="primary" loading={saving} onClick={handleSave}>
Salva
</Button>
<Button onClick={handleReset}>Ripristina default</Button>
</ButtonGroup>
</BlockStack>
</Card>
<Card>
<BlockStack gap="400">
<Text as="h2" variant="headingMd">
Notifiche al merchant
</Text>
<Checkbox
label="Invia email di notifica a ogni recesso"
checked={notifyEnabled}
onChange={setNotifyEnabled}
/>
<TextField
label="Email notifiche"
type="email"
value={notifyEmail}
onChange={setNotifyEmail}
autoComplete="off"
disabled={!notifyEnabled}
helpText="Dove ricevere le notifiche. Senza indirizzo l'email non parte."
placeholder="ordini@tuonegozio.it"
/>
<Checkbox
label="Aggiungi il tag 'Recesso' all'ordine"
checked={tagEnabled}
onChange={setTagEnabled}
helpText="Rende l'ordine filtrabile nella lista ordini."
/>
<div>
<Button variant="primary" loading={saving} onClick={handleSave}>
Salva
</Button>
</div>
</BlockStack>
</Card>
<Card>
<BlockStack gap="400">
<BlockStack gap="100">
<Text as="h2" variant="headingMd">
Regole di recesso
</Text>
<Text as="p" tone="subdued">
Attiva questi controlli solo dopo aver verificato i dati. Da
spenti, il recesso è sempre accettato.
</Text>
{tab === 2 ? (
<Card>
<BlockStack gap="400">
<BlockStack gap="100">
<Text as="h2" variant="headingMd">
Regole di recesso
</Text>
<Text as="p" tone="subdued">
Attiva questi controlli solo dopo aver verificato i dati. Da
spenti, il recesso è sempre accettato. Le regole di esclusione
si gestiscono nella pagina Esclusioni.
</Text>
</BlockStack>
<Checkbox
label="Blocca i recessi oltre il termine"
checked={enforceWindow}
onChange={setEnforceWindow}
helpText="Calcolato dalla data di consegna + i giorni sotto."
/>
<TextField
label="Giorni di recesso"
type="number"
value={windowDays}
onChange={setWindowDays}
autoComplete="off"
min={1}
max={365}
disabled={!enforceWindow}
/>
<Checkbox
label="Blocca i prodotti esclusi (Art. 59)"
checked={enforceExclusions}
onChange={setEnforceExclusions}
helpText="Blocca se l'intero ordine è escluso. Regole nella pagina Esclusioni."
/>
{saveBtn}
</BlockStack>
<Checkbox
label="Blocca i recessi oltre il termine"
checked={enforceWindow}
onChange={setEnforceWindow}
helpText="Calcolato dalla data di evasione (o d'ordine) + i giorni sotto."
/>
<TextField
label="Giorni di recesso"
type="number"
value={windowDays}
onChange={setWindowDays}
autoComplete="off"
min={1}
max={365}
disabled={!enforceWindow}
/>
<Checkbox
label="Blocca i prodotti esclusi (Art. 59)"
checked={enforceExclusions}
onChange={setEnforceExclusions}
helpText="Usa le regole nella pagina Esclusioni. Blocca se l'intero ordine è escluso."
/>
<div>
<Button variant="primary" loading={saving} onClick={handleSave}>
Salva
</Button>
</div>
</BlockStack>
</Card>
</Card>
) : null}
<Card>
{tab === 3 ? (
<BlockStack gap="400">
<Text as="h2" variant="headingMd">
Reso e stato ordine
</Text>
<Checkbox
label="Adatta la ricevuta allo stato dell'ordine"
checked={stateAwareEmail}
onChange={setStateAwareEmail}
helpText="Non evaso → annullo+rimborso; spedito/consegnato → istruzioni di reso."
/>
<Checkbox
label="Annulla automaticamente gli ordini non ancora spediti"
checked={autoCancelUnfulfilled}
onChange={setAutoCancelUnfulfilled}
helpText="Al recesso, se l'ordine non è evaso: annullo + rimborso automatici. Irreversibile."
/>
<Checkbox
label="Spese di restituzione a carico del cliente (Art. 57)"
checked={returnAtCustomerExpense}
onChange={setReturnAtCustomerExpense}
/>
<TextField
label="Indirizzo per il reso"
value={returnAddress}
onChange={setReturnAddress}
autoComplete="off"
multiline={2}
placeholder="Via ..., CAP Città (PR)"
/>
<TextField
label="Istruzioni di reso (opzionale)"
value={returnInstructions}
onChange={setReturnInstructions}
autoComplete="off"
multiline={3}
helpText="Testo aggiuntivo mostrato nella ricevuta per ordini spediti/consegnati."
/>
<div>
<Button variant="primary" loading={saving} onClick={handleSave}>
Salva
</Button>
</div>
<Card>
<BlockStack gap="400">
<BlockStack gap="100">
<Text as="h2" variant="headingMd">
Reso e stato ordine
</Text>
<Text as="p" tone="subdued">
Il riquadro nella ricevuta cambia in base allo stato.
Segnaposto nei testi:
</Text>
<InlineStack gap="200" wrap>
{OP_PLACEHOLDERS.map((p) => (
<Badge key={p}>{`{{${p}}}`}</Badge>
))}
</InlineStack>
</BlockStack>
<Checkbox
label="Adatta la ricevuta allo stato dell'ordine"
checked={stateAwareEmail}
onChange={setStateAwareEmail}
/>
<Checkbox
label="Annulla automaticamente gli ordini non ancora spediti"
checked={autoCancelUnfulfilled}
onChange={setAutoCancelUnfulfilled}
helpText="Al recesso, se l'ordine non è evaso: annullo + rimborso automatici. Irreversibile."
/>
<Checkbox
label="Spese di restituzione a carico del cliente (Art. 57)"
checked={returnAtCustomerExpense}
onChange={setReturnAtCustomerExpense}
helpText="Determina il valore di {{returnCost}}."
/>
<TextField
label="Indirizzo per il reso"
value={returnAddress}
onChange={setReturnAddress}
autoComplete="off"
multiline={2}
placeholder="Via ..., CAP Città (PR)"
helpText="Valore di {{returnAddress}}."
/>
<TextField
label="Testo - ordine non evaso"
value={opTextUnfulfilled}
onChange={setOpTextUnfulfilled}
autoComplete="off"
multiline={3}
/>
<TextField
label="Testo - ordine spedito/consegnato"
value={opTextShipped}
onChange={setOpTextShipped}
autoComplete="off"
multiline={4}
/>
{saveBtn}
</BlockStack>
</Card>
<Card>
<BlockStack gap="300">
<Text as="h2" variant="headingMd">
Anteprima riquadro
</Text>
<Text as="p" tone="subdued">
Ordine non evaso
</Text>
{opFrame(previewOpUnf)}
<Text as="p" tone="subdued">
Ordine spedito/consegnato
</Text>
{opFrame(previewOpShip)}
</BlockStack>
</Card>
</BlockStack>
</Card>
<Card>
<BlockStack gap="200">
<Text as="h2" variant="headingMd">
Anteprima
</Text>
<Text as="p" tone="subdued">
Oggetto: {previewSubject}
</Text>
<iframe
title="Anteprima email"
srcDoc={previewHtml}
style={{
width: "100%",
height: "640px",
border: "1px solid #e1e1e1",
borderRadius: "8px",
background: "#fff",
}}
/>
</BlockStack>
</Card>
) : null}
</BlockStack>
</Layout.Section>
</Layout>

View File

@@ -375,9 +375,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
(state === "unfulfilled" || state === "shipped" || state === "delivered")
? {
state,
textUnfulfilled: settings?.opTextUnfulfilled,
textShipped: settings?.opTextShipped,
returnAddress: settings?.returnAddress,
atCustomerExpense: settings?.returnAtCustomerExpense ?? true,
instructions: settings?.returnInstructions,
}
: null;
const receipt = await sendWithdrawalReceipt({

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "opTextShipped" TEXT,
ADD COLUMN "opTextUnfulfilled" TEXT;

View File

@@ -60,7 +60,9 @@ model Settings {
stateAwareEmail Boolean @default(true)
autoCancelUnfulfilled Boolean @default(false)
returnAtCustomerExpense Boolean @default(true)
returnInstructions String?
returnInstructions String? // deprecato: sostituito da opTextShipped
opTextUnfulfilled String?
opTextShipped String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt