since3-portal/modules/shared/actions/stammdaten.ts
2026-09-29 13:46:50 +02:00

226 lines
8.6 KiB
TypeScript

"use server";
import * as z from "zod";
import { revalidatePath } from "next/cache";
import { createAdminClient } from "@/lib/supabase/admin";
import { verifySession } from "@/lib/supabase/dal";
import { getClientId } from "@/modules/shared/queries/profile";
const IBAN_REGEX = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/;
const StammdatenSchema = z
.object({
profileId: z.string().uuid(),
companyName: z.string().min(1, { error: "Firmenname ist erforderlich." }),
legalForm: z.string().min(1, { error: "Rechtsform ist erforderlich." }),
industry: z.string().optional(),
vatId: z.string().optional(),
taxNumber: z.string().optional(),
commercialRegisterNumber: z.string().optional(),
street: z.string().min(1, { error: "Straße ist erforderlich." }),
postalCode: z.string().min(1, { error: "PLZ ist erforderlich." }),
city: z.string().min(1, { error: "Stadt ist erforderlich." }),
country: z.string().length(2).default("DE"),
billingStreet: z.string().optional(),
billingPostalCode: z.string().optional(),
billingCity: z.string().optional(),
billingCountry: z.string().optional(),
contactName: z.string().min(1, { error: "Ansprechpartner ist erforderlich." }),
contactPosition: z.string().optional(),
contactEmail: z.email({ error: "Bitte eine gültige Email eingeben." }),
contactPhone: z.string().optional(),
paymentMethod: z.enum(["invoice", "sepa_debit", "card"]).default("invoice"),
accountHolder: z.string().optional(),
iban: z.string().optional(),
bic: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.paymentMethod === "sepa_debit") {
if (!data.accountHolder) {
ctx.addIssue({
code: "custom",
path: ["accountHolder"],
message: "Kontoinhaber ist bei SEPA-Lastschrift erforderlich.",
});
}
if (!data.iban) {
ctx.addIssue({
code: "custom",
path: ["iban"],
message: "IBAN ist bei SEPA-Lastschrift erforderlich.",
});
} else if (!IBAN_REGEX.test(data.iban)) {
ctx.addIssue({
code: "custom",
path: ["iban"],
message: "Bitte eine gültige IBAN eingeben.",
});
}
}
const billingFields = [
data.billingStreet,
data.billingPostalCode,
data.billingCity,
data.billingCountry,
];
const filledCount = billingFields.filter(Boolean).length;
if (filledCount > 0 && filledCount < billingFields.length) {
ctx.addIssue({
code: "custom",
path: ["billingStreet"],
message: "Bitte alle Felder der Rechnungsadresse ausfüllen oder alle leer lassen.",
});
}
});
export type StammdatenState = { error: string } | { success: true } | undefined;
function emptyToUndefined(value: FormDataEntryValue | null) {
const stringValue = String(value ?? "").trim();
return stringValue === "" ? undefined : stringValue;
}
export async function saveStammdaten(
_prevState: StammdatenState,
formData: FormData
): Promise<StammdatenState> {
const session = await verifySession();
const profileId = String(formData.get("profileId") ?? "");
if (!session.isAdmin && session.userId !== profileId) {
return { error: "Keine Berechtigung." };
}
const validatedFields = StammdatenSchema.safeParse({
profileId,
companyName: emptyToUndefined(formData.get("companyName")),
legalForm: emptyToUndefined(formData.get("legalForm")),
industry: emptyToUndefined(formData.get("industry")),
vatId: emptyToUndefined(formData.get("vatId")),
taxNumber: emptyToUndefined(formData.get("taxNumber")),
commercialRegisterNumber: emptyToUndefined(formData.get("commercialRegisterNumber")),
street: emptyToUndefined(formData.get("street")),
postalCode: emptyToUndefined(formData.get("postalCode")),
city: emptyToUndefined(formData.get("city")),
country: emptyToUndefined(formData.get("country")) ?? "DE",
billingStreet: emptyToUndefined(formData.get("billingStreet")),
billingPostalCode: emptyToUndefined(formData.get("billingPostalCode")),
billingCity: emptyToUndefined(formData.get("billingCity")),
billingCountry: emptyToUndefined(formData.get("billingCountry")),
contactName: emptyToUndefined(formData.get("contactName")),
contactPosition: emptyToUndefined(formData.get("contactPosition")),
contactEmail: emptyToUndefined(formData.get("contactEmail")),
contactPhone: emptyToUndefined(formData.get("contactPhone")),
paymentMethod: emptyToUndefined(formData.get("paymentMethod")) ?? "invoice",
accountHolder: emptyToUndefined(formData.get("accountHolder")),
iban: emptyToUndefined(formData.get("iban")),
bic: emptyToUndefined(formData.get("bic")),
});
if (!validatedFields.success) {
const { fieldErrors } = z.flattenError(validatedFields.error);
const firstError = Object.values(fieldErrors).flat()[0];
return { error: firstError ?? "Bitte alle Pflichtfelder ausfüllen." };
}
const {
billingStreet,
billingPostalCode,
billingCity,
billingCountry,
paymentMethod,
accountHolder,
iban,
bic,
...data
} = validatedFields.data;
const supabase = createAdminClient();
const clientPayload = {
company_name: data.companyName,
legal_form: data.legalForm,
industry: data.industry ?? null,
vat_id: data.vatId ?? null,
tax_number: data.taxNumber ?? null,
commercial_register_number: data.commercialRegisterNumber ?? null,
street: data.street,
postal_code: data.postalCode,
city: data.city,
country: data.country,
billing_street: billingStreet ?? null,
billing_postal_code: billingPostalCode ?? null,
billing_city: billingCity ?? null,
billing_country: billingCountry ?? null,
contact_name: data.contactName,
contact_position: data.contactPosition ?? null,
contact_email: data.contactEmail,
contact_phone: data.contactPhone ?? null,
};
let clientId: string | null;
try {
clientId = await getClientId(data.profileId);
} catch {
return { error: "Profil konnte nicht geladen werden." };
}
if (clientId) {
const { error } = await supabase.from("clients").update(clientPayload).eq("id", clientId);
if (error) return { error: "Stammdaten konnten nicht gespeichert werden." };
} else {
const { data: newClientId, error } = await supabase.rpc("create_client_and_link_profile", {
p_profile_id: data.profileId,
p_company_name: clientPayload.company_name,
p_legal_form: clientPayload.legal_form,
p_industry: clientPayload.industry,
p_vat_id: clientPayload.vat_id,
p_tax_number: clientPayload.tax_number,
p_commercial_register_number: clientPayload.commercial_register_number,
p_street: clientPayload.street,
p_postal_code: clientPayload.postal_code,
p_city: clientPayload.city,
p_country: clientPayload.country,
p_billing_street: clientPayload.billing_street,
p_billing_postal_code: clientPayload.billing_postal_code,
p_billing_city: clientPayload.billing_city,
p_billing_country: clientPayload.billing_country,
p_contact_name: clientPayload.contact_name,
p_contact_position: clientPayload.contact_position,
p_contact_email: clientPayload.contact_email,
p_contact_phone: clientPayload.contact_phone,
});
if (error || !newClientId) return { error: "Stammdaten konnten nicht gespeichert werden." };
clientId = newClientId;
}
const { error: paymentError } = await supabase.from("client_payment_details").upsert({
client_id: clientId,
payment_method: paymentMethod,
account_holder: accountHolder ?? null,
iban: iban ?? null,
bic: bic ?? null,
});
if (paymentError) {
return { error: "Zahlungsdaten konnten nicht gespeichert werden." };
}
revalidatePath("/client/data");
revalidatePath(`/admin/clients/${data.profileId}/data`);
return { success: true };
}