since3-portal/modules/loginPage/ui/components/stamdaten-form.tsx
2026-09-29 22:56:37 +02:00

310 lines
11 KiB
TypeScript

"use client"
import { useActionState, useEffect, useState } from "react";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { saveStammdaten, StammdatenState } from "@/modules/shared/actions/stammdaten";
import {
Building2,
CreditCard,
FileText,
MapPin,
User,
type LucideIcon,
} from "lucide-react";
import { LEGAL_FORMS } from "@/modules/shared/data/stammdaten-data";
type Client = {
company_name: string;
legal_form: string;
industry: string | null;
vat_id: string | null;
tax_number: string | null;
commercial_register_number: string | null;
street: string;
postal_code: string;
city: string;
country: string;
billing_street: string | null;
billing_postal_code: string | null;
billing_city: string | null;
billing_country: string | null;
contact_name: string;
contact_position: string | null;
contact_email: string;
contact_phone: string | null;
} | null;
type PaymentDetails = {
payment_method: "invoice" | "sepa_debit" | "card";
account_holder: string | null;
iban: string | null;
bic: string | null;
} | null;
const selectClassName =
"flex h-10 w-full rounded-md border border-input bg-background px-3 text-body outline-none transition-colors placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50";
function FormSection({
icon: Icon,
title,
children,
}: {
icon: LucideIcon;
title: string;
children: React.ReactNode;
}) {
return (
<div className="rounded-lg border border-border bg-surface/50 p-4 sm:p-5">
<div className="mb-4 flex items-center gap-2 sm:mb-5">
<Icon className="size-4.5 text-muted-foreground" />
<h2 className="text-base font-semibold">{title}</h2>
</div>
<FieldGroup className="@container grid grid-cols-1 gap-4 @md:grid-cols-2">
{children}
</FieldGroup>
</div>
);
}
function TextField({
name,
label,
defaultValue,
type = "text",
required,
disabled,
}: {
name: string;
label: string;
defaultValue?: string | null;
type?: string;
required?: boolean;
disabled?: boolean;
}) {
return (
<Field>
<FieldLabel htmlFor={name}>
{label} {required && <span className="text-destructive">*</span>}
</FieldLabel>
<Input
id={name}
name={name}
type={type}
defaultValue={defaultValue ?? ""}
required={required}
disabled={disabled}
className="focus-visible:ring-0"
/>
</Field>
);
}
function SelectField({
name,
label,
value,
defaultValue,
onChange,
options,
required,
disabled,
}: {
name: string;
label: string;
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
options: { value: string; label: string }[];
required?: boolean;
disabled?: boolean;
}) {
return (
<Field>
<FieldLabel htmlFor={name}>
{label} {required && <span className="text-destructive">*</span>}
</FieldLabel>
<select
id={name}
name={name}
value={value}
defaultValue={defaultValue}
onChange={onChange ? (event) => onChange(event.target.value) : undefined}
disabled={disabled}
required={required}
className={selectClassName}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</Field>
);
}
const StammdatenForm = ({
profileId,
client,
paymentDetails,
isEditing,
onDone,
}: {
profileId: string;
client: Client;
paymentDetails: PaymentDetails;
isEditing: boolean;
onDone: () => void;
}) => {
const [state, formAction, pending] = useActionState<StammdatenState, FormData>(
saveStammdaten,
undefined
);
const [paymentMethod, setPaymentMethod] = useState(paymentDetails?.payment_method ?? "invoice");
const [hasDifferentBillingAddress, setHasDifferentBillingAddress] = useState(
Boolean(client?.billing_street)
);
useEffect(() => {
if (state && "success" in state && state.success) {
onDone();
}
}, [state, onDone]);
const disabled = !isEditing || pending;
return (
<form action={formAction} className="flex flex-col gap-6">
<input type="hidden" name="profileId" value={profileId} />
<fieldset disabled={disabled} className="flex flex-col gap-6">
<FormSection icon={Building2} title="Firma">
<TextField name="companyName" label="Firmenname" defaultValue={client?.company_name} required />
<SelectField
name="legalForm"
label="Rechtsform"
defaultValue={client?.legal_form ?? LEGAL_FORMS[0]}
disabled={disabled}
required
options={LEGAL_FORMS.map((form) => ({ value: form, label: form }))}
/>
<TextField name="industry" label="Branche" defaultValue={client?.industry} />
<TextField name="vatId" label="USt-IdNr" defaultValue={client?.vat_id} />
<TextField name="taxNumber" label="Steuernummer" defaultValue={client?.tax_number} />
<TextField
name="commercialRegisterNumber"
label="Handelsregisternummer"
defaultValue={client?.commercial_register_number}
/>
</FormSection>
<FormSection icon={MapPin} title="Firmenadresse">
<TextField name="street" label="Straße & Hausnummer" defaultValue={client?.street} required />
<TextField name="postalCode" label="PLZ" defaultValue={client?.postal_code} required />
<TextField name="city" label="Stadt" defaultValue={client?.city} required />
<SelectField
name="country"
label="Land"
defaultValue={client?.country ?? "DE"}
disabled={disabled}
options={[
{ value: "DE", label: "Deutschland" },
{ value: "AT", label: "Österreich" },
{ value: "CH", label: "Schweiz" },
]}
/>
</FormSection>
<div className="rounded-lg border border-border bg-surface/50 p-4 sm:p-5">
<label className="mb-4 flex items-center gap-2 has-disabled:opacity-50 sm:mb-5">
<input
type="checkbox"
checked={hasDifferentBillingAddress}
onChange={(event) => setHasDifferentBillingAddress(event.target.checked)}
disabled={disabled}
className="size-4 rounded border-input accent-primary"
/>
<FileText className="size-4.5 text-muted-foreground" />
<span className="text-base font-semibold">Abweichende Rechnungsadresse</span>
</label>
{hasDifferentBillingAddress && (
<FieldGroup className="@container grid grid-cols-1 gap-4 @md:grid-cols-2">
<TextField
name="billingStreet"
label="Straße & Hausnummer"
defaultValue={client?.billing_street}
required
/>
<TextField
name="billingPostalCode"
label="PLZ"
defaultValue={client?.billing_postal_code}
required
/>
<TextField name="billingCity" label="Stadt" defaultValue={client?.billing_city} required />
<TextField
name="billingCountry"
label="Land"
defaultValue={client?.billing_country}
required
/>
</FieldGroup>
)}
</div>
<FormSection icon={User} title="Ansprechpartner">
<TextField name="contactName" label="Name" defaultValue={client?.contact_name} required />
<TextField name="contactPosition" label="Position" defaultValue={client?.contact_position} />
<TextField name="contactEmail" label="Email" type="email" defaultValue={client?.contact_email} required />
<TextField name="contactPhone" label="Telefon" defaultValue={client?.contact_phone} />
</FormSection>
<FormSection icon={CreditCard} title="Zahlungsdaten">
<SelectField
name="paymentMethod"
label="Zahlungsart"
value={paymentMethod}
onChange={(value) => setPaymentMethod(value as typeof paymentMethod)}
disabled={disabled}
options={[
{ value: "invoice", label: "Rechnung" },
{ value: "sepa_debit", label: "SEPA-Lastschrift" },
{ value: "card", label: "Kreditkarte" },
]}
/>
{paymentMethod === "sepa_debit" && (
<>
<TextField
name="accountHolder"
label="Kontoinhaber"
defaultValue={paymentDetails?.account_holder}
required
/>
<TextField name="iban" label="IBAN" defaultValue={paymentDetails?.iban} required />
<TextField name="bic" label="BIC" defaultValue={paymentDetails?.bic} />
</>
)}
</FormSection>
</fieldset>
{state && "error" in state && (
<p className="text-sm text-destructive">{state.error}</p>
)}
{isEditing && (
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button type="button" variant="outline" onClick={onDone} disabled={pending}>
Abbrechen
</Button>
<Button type="submit" disabled={pending}>
{pending ? "Wird gespeichert..." : "Speichern"}
</Button>
</div>
)}
</form>
);
};
export default StammdatenForm;