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