added stammdaten form

This commit is contained in:
Rajpreet0 2026-09-28 15:53:52 +02:00
parent 378d9b9546
commit a9fdcf9fe5
18 changed files with 1249 additions and 6 deletions

View file

@ -0,0 +1,22 @@
"use server";
import { createAdminClient } from "@/lib/supabase/admin";
import { verifySession } from "@/lib/supabase/dal";
export async function acceptTerms() {
const session = await verifySession();
const supabase = createAdminClient();
const { error } = await supabase
.from("profiles")
.update({ terms_accepted_at: new Date().toISOString() })
.eq("id", session.userId)
if (error) return {
error: "Zustimmung konnte nicht gespeichert werden."
};
return {
success: true
}
}

View file

@ -0,0 +1,178 @@
"use server";
import * as z from "zod";
import { revalidatePath } from "next/cache";
import { createAdminClient } from "@/lib/supabase/admin";
import { verifySession } from "@/lib/supabase/dal";
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(),
});
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 { data: profile, error: profileError } = await supabase
.from("profiles")
.select("client_id")
.eq("id", data.profileId)
.single();
if (profileError || !profile) {
return { error: "Profil konnte nicht geladen werden." };
}
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 = profile.client_id;
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: newClient, error } = await supabase
.from("clients")
.insert(clientPayload)
.select("id")
.single();
if (error || !newClient) return { error: "Stammdaten konnten nicht gespeichert werden." };
clientId = newClient.id;
const { error: linkError } = await supabase
.from("profiles")
.update({ client_id: clientId })
.eq("id", data.profileId);
if (linkError) return { error: "Verknüpfung mit Profil fehlgeschlagen." };
}
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 };
}

View file

@ -0,0 +1,14 @@
export const LEGAL_FORMS = [
"Einzelunternehmen",
"GbR",
"GmbH",
"UG (haftungsbeschränkt)",
"AG",
"OHG",
"KG",
"GmbH & Co. KG",
"e.K.",
"e.V.",
];

View file

@ -0,0 +1,25 @@
import "server-only"
import { createAdminClient } from "@/lib/supabase/admin"
export async function getStammdaten(profileId: string) {
const supabase = createAdminClient()
const { data: profile, error: profileError } = await supabase
.from("profiles")
.select("client_id")
.eq("id", profileId)
.single()
if (profileError || !profile) return null
if (!profile.client_id) {
return { client: null, paymentDetails: null }
}
const [{ data: client }, { data: paymentDetails }] = await Promise.all([
supabase.from("clients").select("*").eq("id", profile.client_id).single(),
supabase.from("client_payment_details").select("*").eq("client_id", profile.client_id).single(),
])
return { client: client ?? null, paymentDetails: paymentDetails ?? null }
}

View file

@ -0,0 +1,42 @@
"use client"
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Pen } from "lucide-react";
import StammdatenForm from "@/modules/loginPage/ui/components/stamdaten-form";
type Client = React.ComponentProps<typeof StammdatenForm>["client"];
type PaymentDetails = React.ComponentProps<typeof StammdatenForm>["paymentDetails"];
export function StammdatenPanel({
profileId,
client,
paymentDetails,
}: {
profileId: string;
client: Client;
paymentDetails: PaymentDetails;
}) {
const [isEditing, setIsEditing] = useState(false);
return (
<div className="flex flex-col gap-8 w-full">
<div className="w-full flex flex-row justify-between items-center">
<h1 className="text-3xl font-semibold">Stammdaten</h1>
{!isEditing && (
<Button variant="outline" onClick={() => setIsEditing(true)}>
<Pen className="w-4 h-4 mr-2" /> Edit
</Button>
)}
</div>
<StammdatenForm
key={JSON.stringify({ client, paymentDetails })}
profileId={profileId}
client={client}
paymentDetails={paymentDetails}
isEditing={isEditing}
onDone={() => setIsEditing(false)}
/>
</div>
);
}

View file

@ -0,0 +1,43 @@
"use client";
import { useState } from "react";
import { acceptTerms } from "../../actions/acceptTerms";
import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export function TermsDialog() {
const [open, setOpen] = useState(true);
const [pending, setPending] = useState(false);
const router = useRouter();
async function handleAccept() {
setPending(true);
const result = await acceptTerms();
if (result && "success" in result) {
setOpen(false);
router.refresh();
}
setPending(false);
}
return (
<Dialog open={open} onOpenChange={() => {}}>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Nutzungsbedingungen</DialogTitle>
<DialogDescription>
Bitte akzeptiere unsere Nutzungsbedingungen, um fortzufahren.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={handleAccept} disabled={pending}>
{pending ? "..." : "Akzeptieren"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}