mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
310 lines
11 KiB
TypeScript
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;
|