feat(email): WYSIWYG template editor with variable substitution
- Add emailSubject{De,En} + emailBodyHtml{De,En} to ShopSettings
- New RichTextEditor component (TipTap) with toolbar + variable insert
- Settings UI: Email templates section per language
- email.server.ts: substitute {{var}} placeholders, fall back to defaults
- Default vars: invoiceNumber, customerName, customerFirstName, orderName,
totalGross, dueDate, companyName, ownerName
This commit is contained in:
@@ -165,8 +165,8 @@ export default function InvoicesPage() {
|
||||
</s-stack>
|
||||
|
||||
{isLoading ? (
|
||||
<s-stack direction="inline" gap="small" alignItems="center">
|
||||
<s-spinner size="small" accessibilityLabel="Loading orders" />
|
||||
<s-stack direction="inline" gap="base" alignItems="center">
|
||||
<s-spinner size="base" accessibilityLabel="Loading orders" />
|
||||
<s-text tone="neutral">Loading…</s-text>
|
||||
</s-stack>
|
||||
) : orders.length === 0 ? (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
deleteStoredLogo,
|
||||
storeUploadedLogo,
|
||||
} from "../services/invoice/logoCache.server";
|
||||
import { RichTextEditor } from "../components/RichTextEditor";
|
||||
|
||||
interface SettingsFieldErrors {
|
||||
vatId?: string;
|
||||
@@ -152,6 +153,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
smtpFromName: str("smtpFromName"),
|
||||
smtpFromEmail: str("smtpFromEmail"),
|
||||
smtpReplyTo: str("smtpReplyTo"),
|
||||
emailSubjectDe: str("emailSubjectDe"),
|
||||
emailBodyHtmlDe: str("emailBodyHtmlDe"),
|
||||
emailSubjectEn: str("emailSubjectEn"),
|
||||
emailBodyHtmlEn: str("emailBodyHtmlEn"),
|
||||
};
|
||||
|
||||
await db.shopSettings.upsert({
|
||||
@@ -361,6 +366,43 @@ export default function SettingsRoute() {
|
||||
</s-stack>
|
||||
</s-section>
|
||||
|
||||
<s-section heading="Email templates">
|
||||
<s-stack direction="block" gap="base">
|
||||
<s-paragraph>
|
||||
These templates are used when sending the invoice PDF by email.
|
||||
Leave a field empty to fall back to the built-in default.
|
||||
</s-paragraph>
|
||||
|
||||
<Field
|
||||
label="Subject (German)"
|
||||
name="emailSubjectDe"
|
||||
defaultValue={settings.emailSubjectDe}
|
||||
helpText="Variables like {{invoiceNumber}} are substituted at send time."
|
||||
/>
|
||||
<RichTextEditor
|
||||
name="emailBodyHtmlDe"
|
||||
label="Body (German)"
|
||||
defaultValue={settings.emailBodyHtmlDe}
|
||||
variables={EMAIL_VARS}
|
||||
minHeight={220}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label="Subject (English)"
|
||||
name="emailSubjectEn"
|
||||
defaultValue={settings.emailSubjectEn}
|
||||
helpText="Variables like {{invoiceNumber}} are substituted at send time."
|
||||
/>
|
||||
<RichTextEditor
|
||||
name="emailBodyHtmlEn"
|
||||
label="Body (English)"
|
||||
defaultValue={settings.emailBodyHtmlEn}
|
||||
variables={EMAIL_VARS}
|
||||
minHeight={220}
|
||||
/>
|
||||
</s-stack>
|
||||
</s-section>
|
||||
|
||||
<s-section>
|
||||
<s-stack direction="inline" gap="base" justifyContent="end" alignItems="center">
|
||||
{isSaving ? <s-text tone="neutral">Saving…</s-text> : null}
|
||||
@@ -374,6 +416,17 @@ export default function SettingsRoute() {
|
||||
);
|
||||
}
|
||||
|
||||
const EMAIL_VARS = [
|
||||
{ token: "{{invoiceNumber}}" },
|
||||
{ token: "{{customerName}}" },
|
||||
{ token: "{{customerFirstName}}" },
|
||||
{ token: "{{orderName}}" },
|
||||
{ token: "{{totalGross}}" },
|
||||
{ token: "{{dueDate}}" },
|
||||
{ token: "{{companyName}}" },
|
||||
{ token: "{{ownerName}}" },
|
||||
];
|
||||
|
||||
interface FieldProps {
|
||||
label: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user