mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 21:41:16 +00:00
added stammdaten form
This commit is contained in:
parent
378d9b9546
commit
a9fdcf9fe5
18 changed files with 1249 additions and 6 deletions
22
modules/shared/actions/acceptTerms.ts
Normal file
22
modules/shared/actions/acceptTerms.ts
Normal 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
|
||||
}
|
||||
}
|
||||
178
modules/shared/actions/stammdaten.ts
Normal file
178
modules/shared/actions/stammdaten.ts
Normal 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 };
|
||||
}
|
||||
14
modules/shared/data/stammdaten-data.ts
Normal file
14
modules/shared/data/stammdaten-data.ts
Normal 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.",
|
||||
];
|
||||
25
modules/shared/queries/stammdaten.ts
Normal file
25
modules/shared/queries/stammdaten.ts
Normal 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 }
|
||||
}
|
||||
42
modules/shared/ui/components/stammdaten-panel.tsx
Normal file
42
modules/shared/ui/components/stammdaten-panel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
43
modules/shared/ui/components/terms-dialog.tsx
Normal file
43
modules/shared/ui/components/terms-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue