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">
{t("title")}
</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 className="animate-in animate-in-delay-1">
<BillingSettingsForm initial={existing} />
<BillingSettingsForm
initial={existing}
isPersonal={user.isPersonal}
/>
</div>
</main>
);

View File

@@ -8,6 +8,16 @@ import type { OrgBilling } from "@/types";
interface Props {
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
* 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 router = useRouter();
const [form, setForm] = useState({
@@ -78,7 +88,10 @@ export function BillingSettingsForm({ initial }: Props) {
postalCode: form.postalCode.trim(),
city: form.city.trim(),
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(),
notes: form.notes.trim() || null,
}),
@@ -99,7 +112,10 @@ export function BillingSettingsForm({ initial }: Props) {
return (
<Card>
<div className="space-y-4">
<Field label={t("companyNameLabel")} required>
<Field
label={isPersonal ? t("fullNameLabel") : t("companyNameLabel")}
required
>
<input
type="text"
value={form.companyName}
@@ -155,6 +171,7 @@ export function BillingSettingsForm({ initial }: Props) {
/>
</Field>
</div>
{!isPersonal && (
<Field label={t("vatNumberLabel")} hint={t("vatNumberHint")}>
<input
type="text"
@@ -165,6 +182,7 @@ export function BillingSettingsForm({ initial }: Props) {
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 label={t("billingEmailLabel")} required hint={t("billingEmailHint")}>
<input
type="email"

View File

@@ -502,7 +502,9 @@
"saved": "Gespeichert.",
"missingRequired": "Bitte alle Pflichtfelder ausfüllen.",
"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": {
"title": "Support",

View File

@@ -502,7 +502,9 @@
"saved": "Saved.",
"missingRequired": "Please fill in all required fields.",
"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": {
"title": "Support",

View File

@@ -502,7 +502,9 @@
"saved": "Enregistré.",
"missingRequired": "Veuillez remplir tous les champs obligatoires.",
"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": {
"title": "Support",

View File

@@ -502,7 +502,9 @@
"saved": "Salvato.",
"missingRequired": "Compila tutti i campi obbligatori.",
"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": {
"title": "Supporto",