Phase6: Customer Billing details

This commit is contained in:
2026-05-25 12:15:48 +02:00
parent fadfdd3435
commit 7feeb6cc02
6 changed files with 50 additions and 19 deletions

View File

@@ -33,10 +33,15 @@ export default async function BillingSettingsPage() {
<h1 className="font-display text-2xl font-semibold accent-rule"> <h1 className="font-display text-2xl font-semibold accent-rule">
{t("title")} {t("title")}
</h1> </h1>
<p className="text-sm text-text-secondary mt-3">{t("subtitle")}</p> <p className="text-sm text-text-secondary mt-3">
{user.isPersonal ? t("subtitlePersonal") : t("subtitle")}
</p>
</div> </div>
<div className="animate-in animate-in-delay-1"> <div className="animate-in animate-in-delay-1">
<BillingSettingsForm initial={existing} /> <BillingSettingsForm
initial={existing}
isPersonal={user.isPersonal}
/>
</div> </div>
</main> </main>
); );

View File

@@ -8,6 +8,16 @@ import type { OrgBilling } from "@/types";
interface Props { interface Props {
initial: OrgBilling | null; initial: OrgBilling | null;
/**
* Personal-account (individual customer) flag from the session.
* Individuals get a "Full name" field instead of "Company name",
* and the VAT input is hidden entirely — they don't have one and
* showing the field would only confuse. The underlying column is
* still `company_name` in the DB and the invoice PDF; for an
* individual that field carries their full name, which is
* exactly what should print on the invoice.
*/
isPersonal: boolean;
} }
/** /**
@@ -22,7 +32,7 @@ interface Props {
* On success we router.refresh() the page so the server component * On success we router.refresh() the page so the server component
* re-fetches and any "create now" -> "edit" wording flips. * re-fetches and any "create now" -> "edit" wording flips.
*/ */
export function BillingSettingsForm({ initial }: Props) { export function BillingSettingsForm({ initial, isPersonal }: Props) {
const t = useTranslations("settingsBilling"); const t = useTranslations("settingsBilling");
const router = useRouter(); const router = useRouter();
const [form, setForm] = useState({ const [form, setForm] = useState({
@@ -78,7 +88,10 @@ export function BillingSettingsForm({ initial }: Props) {
postalCode: form.postalCode.trim(), postalCode: form.postalCode.trim(),
city: form.city.trim(), city: form.city.trim(),
country: form.country.trim().toUpperCase(), country: form.country.trim().toUpperCase(),
vatNumber: form.vatNumber.trim() || null, // Personal accounts never have a VAT number — force null
// regardless of stale state, in case a value was stored
// before the account got flagged as personal.
vatNumber: isPersonal ? null : form.vatNumber.trim() || null,
billingEmail: form.billingEmail.trim(), billingEmail: form.billingEmail.trim(),
notes: form.notes.trim() || null, notes: form.notes.trim() || null,
}), }),
@@ -99,7 +112,10 @@ export function BillingSettingsForm({ initial }: Props) {
return ( return (
<Card> <Card>
<div className="space-y-4"> <div className="space-y-4">
<Field label={t("companyNameLabel")} required> <Field
label={isPersonal ? t("fullNameLabel") : t("companyNameLabel")}
required
>
<input <input
type="text" type="text"
value={form.companyName} value={form.companyName}
@@ -155,16 +171,18 @@ export function BillingSettingsForm({ initial }: Props) {
/> />
</Field> </Field>
</div> </div>
<Field label={t("vatNumberLabel")} hint={t("vatNumberHint")}> {!isPersonal && (
<input <Field label={t("vatNumberLabel")} hint={t("vatNumberHint")}>
type="text" <input
value={form.vatNumber} type="text"
onChange={set("vatNumber")} value={form.vatNumber}
maxLength={40} onChange={set("vatNumber")}
placeholder="CHE-123.456.789 MWST" maxLength={40}
className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border focus:border-accent focus:outline-none text-sm font-mono" placeholder="CHE-123.456.789 MWST"
/> className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border focus:border-accent focus:outline-none text-sm font-mono"
</Field> />
</Field>
)}
<Field label={t("billingEmailLabel")} required hint={t("billingEmailHint")}> <Field label={t("billingEmailLabel")} required hint={t("billingEmailHint")}>
<input <input
type="email" type="email"

View File

@@ -502,7 +502,9 @@
"saved": "Gespeichert.", "saved": "Gespeichert.",
"missingRequired": "Bitte alle Pflichtfelder ausfüllen.", "missingRequired": "Bitte alle Pflichtfelder ausfüllen.",
"invalidCountry": "Ländercode muss aus 2 Buchstaben bestehen (z.B. CH).", "invalidCountry": "Ländercode muss aus 2 Buchstaben bestehen (z.B. CH).",
"invalidEmail": "Bitte eine gültige E-Mail-Adresse eingeben." "invalidEmail": "Bitte eine gültige E-Mail-Adresse eingeben.",
"fullNameLabel": "Vor- und Nachname",
"subtitlePersonal": "Ihre Rechnungsadresse und Rechnungskontakt. Erforderlich, bevor Rechnungen ausgestellt werden können."
}, },
"support": { "support": {
"title": "Support", "title": "Support",

View File

@@ -502,7 +502,9 @@
"saved": "Saved.", "saved": "Saved.",
"missingRequired": "Please fill in all required fields.", "missingRequired": "Please fill in all required fields.",
"invalidCountry": "Country code must be 2 letters (e.g. CH).", "invalidCountry": "Country code must be 2 letters (e.g. CH).",
"invalidEmail": "Please enter a valid email address." "invalidEmail": "Please enter a valid email address.",
"fullNameLabel": "Full name",
"subtitlePersonal": "Your billing address and invoice contact. Required before invoices can be issued."
}, },
"support": { "support": {
"title": "Support", "title": "Support",

View File

@@ -502,7 +502,9 @@
"saved": "Enregistré.", "saved": "Enregistré.",
"missingRequired": "Veuillez remplir tous les champs obligatoires.", "missingRequired": "Veuillez remplir tous les champs obligatoires.",
"invalidCountry": "Le code pays doit comporter 2 lettres (p. ex. CH).", "invalidCountry": "Le code pays doit comporter 2 lettres (p. ex. CH).",
"invalidEmail": "Veuillez saisir une adresse e-mail valide." "invalidEmail": "Veuillez saisir une adresse e-mail valide.",
"fullNameLabel": "Nom et prénom",
"subtitlePersonal": "Votre adresse de facturation et votre contact. Requis avant l'émission de toute facture."
}, },
"support": { "support": {
"title": "Support", "title": "Support",

View File

@@ -502,7 +502,9 @@
"saved": "Salvato.", "saved": "Salvato.",
"missingRequired": "Compila tutti i campi obbligatori.", "missingRequired": "Compila tutti i campi obbligatori.",
"invalidCountry": "Il codice paese deve essere di 2 lettere (es. CH).", "invalidCountry": "Il codice paese deve essere di 2 lettere (es. CH).",
"invalidEmail": "Inserisci un indirizzo e-mail valido." "invalidEmail": "Inserisci un indirizzo e-mail valido.",
"fullNameLabel": "Nome e cognome",
"subtitlePersonal": "Il tuo indirizzo di fatturazione e contatto. Necessari prima che possano essere emesse fatture."
}, },
"support": { "support": {
"title": "Supporto", "title": "Supporto",