3cf2e10740
* feat(reminders): per-company reminder text overrides with per-field reset Add company_settings.reminder_text_overrides (JSONB, migration 20260830100000): optional subject/body per reminder level, storing only diffs from the defaults. Reminder templates now express their defaults as placeholder patterns and render stock and override mails through one substitution pipeline (placeholders, HTML escaping, subject sanitizing), so the settings prefill is exactly the sent mail. The level 3 default is strengthened into an explicit inkassovarning (8 days, handover to inkasso, costs per lag (1981:739)); text only, no fee or interest math changes. New ReminderEmailTextsSettings editor (per-level tabs, effective value prefilled, per-field reset, placeholder legend) mounted in the invoicing settings, strings in sv + en, and reminder_text_overrides added to UpdateSettingsSchema with schema and template tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zGah8Yy49esAwpnKGxiGy * chore(migrations): bump reminder_text_overrides to 20260830120000 Main gained 20260830101500_seed_agent_atom_bodies after this branch cut its version, so the file moves to a fresh later timestamp to keep remote migration history append-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zGah8Yy49esAwpnKGxiGy * fix(reminders): serialize override saves and fix Swedish hint grammar CodeRabbit review: queue the whole-object PUTs in ReminderEmailTextsSettings so an older in-flight snapshot cannot replace a newer edit, and start the level 3 hint with "Den slutliga paminnelsen". The NOT VALID suggestion on the migration CHECK is declined: company_settings is one row per company, migration files run in a single transaction, and the invoice_email_texts precedent shipped the identical constraint shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zGah8Yy49esAwpnKGxiGy --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
90 lines
4.2 KiB
TypeScript
90 lines
4.2 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslations } from 'next-intl'
|
|
import { InvoiceSettingsForm } from '@/components/settings/InvoiceSettingsForm'
|
|
import { InvoicePaymentLinkSettings } from '@/components/settings/InvoicePaymentLinkSettings'
|
|
import { PeppolReceiveSettings } from '@/components/settings/PeppolReceiveSettings'
|
|
import { InvoicePaymentAccountsSettings } from '@/components/settings/InvoicePaymentAccountsSettings'
|
|
import { InvoiceEmailTextsSettings } from '@/components/settings/InvoiceEmailTextsSettings'
|
|
import { ReminderEmailTextsSettings } from '@/components/settings/ReminderEmailTextsSettings'
|
|
import { InvoiceEmailRecipientsSettings } from '@/components/settings/InvoiceEmailRecipientsSettings'
|
|
import { InvoiceSenderDomainSettings } from '@/components/settings/InvoiceSenderDomainSettings'
|
|
import { InvoicePreviewCard } from '@/components/settings/InvoicePreviewCard'
|
|
import { PdfPrintSettings } from '@/components/settings/PdfPrintSettings'
|
|
import { SettingsFormWrapper } from '@/components/settings/SettingsFormWrapper'
|
|
import { SettingsLoadError } from '@/components/settings/SettingsLoadError'
|
|
import { SettingsLoadingSkeleton } from '@/components/settings/SettingsLoadingSkeleton'
|
|
import { SettingsSectionHeader } from '@/components/settings/SettingsRows'
|
|
import { useSettings } from '@/components/settings/useSettings'
|
|
import type { CompanySettings } from '@/types'
|
|
|
|
export function InvoicingSettingsContent() {
|
|
const tNav = useTranslations('settings_nav')
|
|
const tIntro = useTranslations('settings_intro')
|
|
const { settings, isLoading, updateSettings, refetch } = useSettings()
|
|
|
|
if (isLoading) return <SettingsLoadingSkeleton />
|
|
if (!settings) return <SettingsLoadError onRetry={refetch} />
|
|
|
|
function handleSave(formData: FormData) {
|
|
const updates: Record<string, unknown> = {
|
|
invoice_prefix: (formData.get('invoice_prefix') as string) || null,
|
|
next_invoice_number: parseInt(formData.get('next_invoice_number') as string) || 1,
|
|
next_arrival_number: parseInt(formData.get('next_arrival_number') as string) || 1,
|
|
invoice_default_days: parseInt(formData.get('invoice_default_days') as string) || 30,
|
|
invoice_default_notes: (formData.get('invoice_default_notes') as string) || null,
|
|
default_our_reference: (formData.get('default_our_reference') as string) || null,
|
|
reminder_days_level_1:
|
|
Number.parseInt(formData.get('reminder_days_level_1') as string) || 15,
|
|
reminder_days_level_2:
|
|
Number.parseInt(formData.get('reminder_days_level_2') as string) || 30,
|
|
reminder_days_level_3:
|
|
Number.parseInt(formData.get('reminder_days_level_3') as string) || 45,
|
|
send_invoice_reminders: formData.get('send_invoice_reminders') === 'true',
|
|
}
|
|
return {
|
|
updates,
|
|
onSuccess: (data: Record<string, unknown>) => {
|
|
updateSettings(data as Partial<CompanySettings>)
|
|
},
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<SettingsSectionHeader
|
|
title={tNav('invoicing')}
|
|
intro={tIntro('invoicing')}
|
|
action={<InvoicePreviewCard settings={settings} />}
|
|
/>
|
|
|
|
{/* Owner/admin only: the component gates itself on role. */}
|
|
<InvoicePaymentAccountsSettings settings={settings} onUpdate={updateSettings} />
|
|
|
|
<SettingsFormWrapper onSave={handleSave}>
|
|
<InvoiceSettingsForm settings={settings} />
|
|
</SettingsFormWrapper>
|
|
|
|
{/* Payment link opt-in: saves individually via toggle switch */}
|
|
<InvoicePaymentLinkSettings settings={settings} onUpdate={updateSettings} />
|
|
|
|
{/* Peppol receiving: one switch that publishes the company's Peppol id */}
|
|
<PeppolReceiveSettings />
|
|
|
|
{/* PDF settings: saves individually via toggle switches */}
|
|
<PdfPrintSettings settings={settings} onUpdate={updateSettings} />
|
|
|
|
{/* Fixed invoice email recipients: explicit save (owner/admin only) */}
|
|
<InvoiceEmailRecipientsSettings settings={settings} onUpdate={updateSettings} />
|
|
|
|
<InvoiceSenderDomainSettings companyName={settings.company_name ?? null} />
|
|
|
|
{/* Invoice email texts: autosaves on blur */}
|
|
<InvoiceEmailTextsSettings settings={settings} onUpdate={updateSettings} />
|
|
|
|
{/* Reminder email texts per level: autosaves on blur */}
|
|
<ReminderEmailTextsSettings settings={settings} onUpdate={updateSettings} />
|
|
</div>
|
|
)
|
|
}
|