Merge pull request #354 from Luca-Timo/fix/email-language-and-payment-confirm
fix(email,ui): billing emails follow customer language + readable pay…
This commit is contained in:
@@ -726,9 +726,12 @@ async function sendTemplateEmail(to, templateKey, variables) {
|
|||||||
throw new Error('Email configuration not found');
|
throw new Error('Email configuration not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine recipient language (pass eventId if available in variables)
|
// Determine recipient language. An explicit `__language` in the email data
|
||||||
const language = await getRecipientLanguage(to, variables.eventId || null);
|
// wins (CRM/billing emails set it to the customer/invoice language so a
|
||||||
|
// gallery event's language can't override a dunning notice — see #760);
|
||||||
|
// otherwise fall back to the event-first recipient resolution.
|
||||||
|
const language = variables.__language || await getRecipientLanguage(to, variables.eventId || null);
|
||||||
|
|
||||||
// Process template with variables
|
// Process template with variables
|
||||||
const { subject, htmlBody, textBody } = await processTemplate(template, variables, language);
|
const { subject, htmlBody, textBody } = await processTemplate(template, variables, language);
|
||||||
|
|
||||||
@@ -783,7 +786,7 @@ async function sendTemplateEmail(to, templateKey, variables) {
|
|||||||
async function renderQueuedEmail(templateKey, variables = {}, to = '') {
|
async function renderQueuedEmail(templateKey, variables = {}, to = '') {
|
||||||
const template = await db('email_templates').where('template_key', templateKey).first();
|
const template = await db('email_templates').where('template_key', templateKey).first();
|
||||||
if (!template) return null;
|
if (!template) return null;
|
||||||
const language = await getRecipientLanguage(to, variables.eventId || null);
|
const language = variables.__language || await getRecipientLanguage(to, variables.eventId || null);
|
||||||
const { subject, htmlBody } = await processTemplate(template, variables, language);
|
const { subject, htmlBody } = await processTemplate(template, variables, language);
|
||||||
return { subject, html: htmlBody };
|
return { subject, html: htmlBody };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ async function applyReminder(invoice, lineItems, level, adminId) {
|
|||||||
const rawDaysOverdue = Math.floor((Date.now() - new Date(invoice.due_date).getTime()) / 86400000);
|
const rawDaysOverdue = Math.floor((Date.now() - new Date(invoice.due_date).getTime()) / 86400000);
|
||||||
const daysOverdue = Math.max(1, rawDaysOverdue);
|
const daysOverdue = Math.max(1, rawDaysOverdue);
|
||||||
const templateKey = level === 1 ? 'invoice_reminder_first' : 'invoice_reminder_second';
|
const templateKey = level === 1 ? 'invoice_reminder_first' : 'invoice_reminder_second';
|
||||||
const locale = ctx.locale || invoice.language || 'de';
|
const locale = ctx.locale || customer.preferred_language || invoice.language || 'de';
|
||||||
const outstandingMinor = Math.max(0, newTotal - Number(invoice.paid_amount_minor || 0));
|
const outstandingMinor = Math.max(0, newTotal - Number(invoice.paid_amount_minor || 0));
|
||||||
|
|
||||||
// Attach the (unchanged) original invoice PDF + the new Mahnung.
|
// Attach the (unchanged) original invoice PDF + the new Mahnung.
|
||||||
@@ -154,6 +154,8 @@ async function applyReminder(invoice, lineItems, level, adminId) {
|
|||||||
const { to: reminderTo, cc: reminderCc } = resolveBillingRecipients(customer, invoice.cc_pdf_email);
|
const { to: reminderTo, cc: reminderCc } = resolveBillingRecipients(customer, invoice.cc_pdf_email);
|
||||||
try {
|
try {
|
||||||
await emailProcessor.queueEmail(invoice.event_id || null, reminderTo, templateKey, {
|
await emailProcessor.queueEmail(invoice.event_id || null, reminderTo, templateKey, {
|
||||||
|
// Render in the customer/invoice language, not the gallery event's (#760).
|
||||||
|
__language: locale,
|
||||||
invoice_number: invoice.invoice_number,
|
invoice_number: invoice.invoice_number,
|
||||||
customer_name: customer.display_name || customer.first_name || customer.email.split('@')[0],
|
customer_name: customer.display_name || customer.first_name || customer.email.split('@')[0],
|
||||||
total_amount: formatMajor(invoice.total_amount_minor, invoice.currency, locale),
|
total_amount: formatMajor(invoice.total_amount_minor, invoice.currency, locale),
|
||||||
|
|||||||
@@ -4074,7 +4074,7 @@
|
|||||||
"noEvents": "Noch keinem Event zugewiesen. Fügen Sie diesen Kunden über das Event-Formular hinzu.",
|
"noEvents": "Noch keinem Event zugewiesen. Fügen Sie diesen Kunden über das Event-Formular hinzu.",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"preferredLanguage": "Bevorzugte Sprache",
|
"preferredLanguage": "Bevorzugte Sprache",
|
||||||
"preferredLanguageHint": "Steuert die Portal-Sprache sowie die Sprache von Angebots- und Rechnungs-PDFs. Neue Kunden erben standardmässig die Sprache aus dem Geschäftsprofil ({{lang}}); hier kann pro Kunde überschrieben werden.",
|
"preferredLanguageHint": "Steuert die Portal-Sprache, Angebots-/Rechnungs-PDFs sowie Rechnungs-E-Mails (Erinnerungen/Mahnungen). Neue Kunden erben standardmässig die Sprache aus dem Geschäftsprofil ({{lang}}); hier kann pro Kunde überschrieben werden.",
|
||||||
"salutation": "Anrede",
|
"salutation": "Anrede",
|
||||||
"salutationNone": "—",
|
"salutationNone": "—",
|
||||||
"firstName": "Vorname",
|
"firstName": "Vorname",
|
||||||
|
|||||||
@@ -4074,7 +4074,7 @@
|
|||||||
"noEvents": "Not assigned to any events yet. Add this customer to an event from the event form.",
|
"noEvents": "Not assigned to any events yet. Add this customer to an event from the event form.",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"preferredLanguage": "Preferred language",
|
"preferredLanguage": "Preferred language",
|
||||||
"preferredLanguageHint": "Drives portal UI and quote/invoice PDF locale. New customers default to the business-profile language ({{lang}}); override here per customer.",
|
"preferredLanguageHint": "Drives portal UI, quote/invoice PDFs, and billing emails (reminders/dunning). New customers default to the business-profile language ({{lang}}); override here per customer.",
|
||||||
"salutation": "Salutation",
|
"salutation": "Salutation",
|
||||||
"salutationNone": "—",
|
"salutationNone": "—",
|
||||||
"firstName": "First name",
|
"firstName": "First name",
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export const CustomerDetailPage: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
<p className="text-xs text-neutral-500 mt-1">
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
{t('customers.detail.preferredLanguageHint',
|
{t('customers.detail.preferredLanguageHint',
|
||||||
'Drives portal UI and quote/invoice PDF locale. New customers default to the business-profile language ({{lang}}); override here per customer.',
|
'Drives portal UI, quote/invoice PDFs, and billing emails (reminders/dunning). New customers default to the business-profile language ({{lang}}); override here per customer.',
|
||||||
{ lang: LOCALE_LABELS[profileDefaultLocale] || profileDefaultLocale.toUpperCase() })}
|
{ lang: LOCALE_LABELS[profileDefaultLocale] || profileDefaultLocale.toUpperCase() })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -373,15 +373,18 @@ const ResultBox: React.FC<{
|
|||||||
<div
|
<div
|
||||||
className="rounded-lg border p-6"
|
className="rounded-lg border p-6"
|
||||||
style={{
|
style={{
|
||||||
borderColor: '#bbf7d0',
|
// Theme-adaptive success card — a light-green tint on light surfaces,
|
||||||
backgroundColor: 'color-mix(in srgb, #dcfce7 50%, var(--color-surface))',
|
// a dark-green tint on dark ones (was a hardcoded light-green mix +
|
||||||
|
// dark-green title that went unreadable in dark mode, #759).
|
||||||
|
borderColor: 'color-mix(in srgb, #16a34a 35%, var(--color-surface))',
|
||||||
|
backgroundColor: 'color-mix(in srgb, #16a34a 12%, var(--color-surface))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="w-10 h-10 mb-3" style={{ color: '#16a34a' }} />
|
<CheckCircle2 className="w-10 h-10 mb-3" style={{ color: '#16a34a' }} />
|
||||||
<h1 className="text-lg font-bold mb-1" style={{ color: '#166534' }}>
|
<h1 className="text-lg font-bold mb-1" style={{ color: 'var(--color-text)' }}>
|
||||||
{t('paymentCheck.result.title', 'Action recorded')}
|
{t('paymentCheck.result.title', 'Action recorded')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm">
|
<p className="text-sm" style={{ color: 'var(--color-text)' }}>
|
||||||
{result.applied === 'paid_full' && t('paymentCheck.result.paid',
|
{result.applied === 'paid_full' && t('paymentCheck.result.paid',
|
||||||
'Invoice {{n}} marked as paid in full.', { n: inv.invoiceNumber })}
|
'Invoice {{n}} marked as paid in full.', { n: inv.invoiceNumber })}
|
||||||
{result.applied === 'paid_with_skonto' && t('paymentCheck.result.paidSkonto',
|
{result.applied === 'paid_with_skonto' && t('paymentCheck.result.paidSkonto',
|
||||||
|
|||||||
Reference in New Issue
Block a user