Phase6: Customer Billing details
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user