From 08ff89702da2033a52cf57646a3006598644898a Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Sun, 23 Nov 2025 17:33:16 +0100 Subject: [PATCH] added registriation of manager and business --- src/app/(main)/dashboard/page.tsx | 154 ++++++--- src/app/api/auth/register/admin/route.ts | 109 +++++++ src/hooks/useAuth.ts | 62 +++- src/lib/auth/getUser.ts | 82 ++++- .../auth/ui/views/register-admin-view.tsx | 291 +++++++++++++++++- .../auth/validation/adminRegisterSchema.ts | 46 +++ src/types/auth.ts | 17 + 7 files changed, 706 insertions(+), 55 deletions(-) create mode 100644 src/app/api/auth/register/admin/route.ts diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index 689a704..ebaea4e 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -1,16 +1,24 @@ import { LogoutButton } from "@/src/components/layout/LogoutButton"; -import { requireAuthWithData } from "@/src/lib/auth/getUser"; +import { requireAuthWithData, isManager, isUser } from "@/src/lib/auth/getUser"; export default async function DashboardPage() { const userData = await requireAuthWithData(); + // Get display name based on user type + const displayName = isManager(userData) + ? `${userData.firstName} ${userData.lastName}` + : userData.name || userData.email; + return (

Willkommen zurück!

-

- Schön, dich wiederzusehen, {userData?.name || userData.email} -

+

Schön, dich wiederzusehen, {displayName}

+ {isManager(userData) && ( + + Manager + + )}
@@ -19,10 +27,20 @@ export default async function DashboardPage() {

Verwalte deine persönlichen Daten

-
-

Trainings

-

Deine gebuchten Trainingseinheiten

-
+ {isManager(userData) ? ( +
+

Meine Businesses

+

+ {userData.businesses.length}{" "} + {userData.businesses.length === 1 ? "Business" : "Businesses"} +

+
+ ) : ( +
+

Trainings

+

Deine gebuchten Trainingseinheiten

+
+ )}

Einstellungen

@@ -30,41 +48,93 @@ export default async function DashboardPage() {
-
-

Benutzerdaten

-
-
-
Name
-
{userData?.name || "—"}
+ {isUser(userData) && ( +
+

Benutzerdaten

+
+
+
Name
+
{userData.name || "—"}
+
+
+
E-Mail
+
{userData.email}
+
+
+
Geburtsdatum
+
+ {userData.birthday ? new Date(userData.birthday).toLocaleDateString("de-DE") : "—"} +
+
+
+
Wohnort
+
+ {userData.plz && userData.city ? `${userData.plz} ${userData.city}` : "—"} +
+
+
+
Geschlecht
+
{userData.gender || "—"}
+
+
+
Registriert seit
+
+ {userData.createdAt + ? new Date(userData.createdAt).toLocaleDateString("de-DE") + : "—"} +
+
+
+
+ )} + + {isManager(userData) && ( +
+

Manager-Daten

+
+
+
Vorname
+
{userData.firstName}
+
+
+
Nachname
+
{userData.lastName}
+
+
+
E-Mail
+
{userData.email}
+
+
+
Telefon
+
{userData.tel || "—"}
+
+
+
Registriert seit
+
+ {userData.createdAt + ? new Date(userData.createdAt).toLocaleDateString("de-DE") + : "—"} +
+
+
+
+ )} + + {isManager(userData) && userData.businesses.length > 0 && ( +
+

Meine Businesses

+
+ {userData.businesses.map((business) => ( +
+

{business.name}

+

{business.address}

+

{business.email}

+
+ ))}
-
-
E-Mail
-
{userData.email}
-
-
-
Geburtsdatum
-
- {userData?.birthday ? new Date(userData.birthday).toLocaleDateString("de-DE") : "—"} -
-
-
-
Wohnort
-
- {userData?.plz && userData?.city ? `${userData.plz} ${userData.city}` : "—"} -
-
-
-
Geschlecht
-
{userData?.gender || "—"}
-
-
-
Registriert seit
-
- {userData?.createdAt ? new Date(userData.createdAt).toLocaleDateString("de-DE") : "—"} -
-
-
-
+ + )} + ); diff --git a/src/app/api/auth/register/admin/route.ts b/src/app/api/auth/register/admin/route.ts new file mode 100644 index 0000000..15200e5 --- /dev/null +++ b/src/app/api/auth/register/admin/route.ts @@ -0,0 +1,109 @@ +// app/api/auth/register/admin/route.ts +import { createClient } from "@/src/lib/supabase/server"; +import { prisma } from "@/src/lib/prisma"; +import { NextResponse } from "next/server"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { + email, + password, + firstName, + lastName, + tel, + plz, + city, + street, + companyName, + companyPlace, + companyPLZ, + companyStreet, + companyMail, + companyNumber, + } = body; + + // Validate required fields + if (!email || !password || !firstName || !lastName || !companyName) { + return NextResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 }); + } + + const supabase = await createClient(); + + // Create Manager in Supabase Auth + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"}/login`, + data: { + firstName, + lastName, + role: "manager", + }, + }, + }); + + if (error) { + console.error("❌ Supabase Auth Error:", error.message); + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + const supabaseUser = data.user; + if (!supabaseUser) { + return NextResponse.json({ error: "Manager konnte nicht erstellt werden" }, { status: 500 }); + } + + // Check if Manager already exists in DB + const existingManager = await prisma.manager.findUnique({ + where: { email }, + }); + + if (existingManager) { + return NextResponse.json({ manager: existingManager }, { status: 200 }); + } + + // Build business address + const businessAddress = `${companyStreet}, ${companyPLZ} ${companyPlace}`; + const managerAddress = plz && city && street ? `${street}, ${plz} ${city}` : ""; + + // Create Manager and Business in a transaction + const result = await prisma.$transaction(async (tx) => { + // Create Manager + const newManager = await tx.manager.create({ + data: { + id: supabaseUser.id, + firstName, + lastName, + tel: tel || null, + email, + }, + }); + + // Create Business + const newBusiness = await tx.business.create({ + data: { + managerId: newManager.id, + name: companyName, + address: businessAddress, + email: companyMail || email, // Use manager email if company email is not provided + title: companyName, // Using company name as title + }, + }); + + return { manager: newManager, business: newBusiness }; + }); + + return NextResponse.json( + { + manager: result.manager, + business: result.business, + message: "Manager und Business erfolgreich erstellt", + }, + { status: 201 } + ); + } catch (error) { + console.error("Error in /api/auth/register/admin:", error); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 2271085..b8146e4 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -8,7 +8,8 @@ import { userLoginSchema } from "../modules/auth/validation/userLoginSchema"; import { createClient } from "@/src/lib/supabase/client"; import { useAuthStore } from "@/src/store/authStore"; import { useRouter } from "next/navigation"; -import { RegisterFormData } from "../types/auth"; +import { RegisterFormData, AdminRegisterFormData } from "../types/auth"; +import { adminRegisterSchema } from "../modules/auth/validation/adminRegisterSchema"; export const useAuth = () => { const [loading, setLoading] = useState(false); @@ -112,10 +113,69 @@ export const useAuth = () => { } }; + const registerAdmin = async (formData: AdminRegisterFormData) => { + setErrors({}); + setLoading(true); + + try { + adminRegisterSchema.parse(formData); + + const [firstName, ...lastNameParts] = formData.fullName.split(" "); + const lastName = lastNameParts.join(" ") || firstName; + + const res = await fetch("/api/auth/register/admin", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: formData.email, + password: formData.password, + firstName, + lastName, + tel: formData.tel, + plz: formData.plz, + city: formData.place, + street: formData.street, + companyName: formData.companyName, + companyPlace: formData.companyPlace, + companyPLZ: formData.companyPLZ, + companyStreet: formData.companyStreet, + companyMail: formData.companyMail, + companyNumber: formData.companyNumber, + }), + }); + + const data = await res.json(); + + if (!res.ok) { + setErrors({ email: data.error || "Registrierung fehlgeschlagen" }); + toast.error(data.error || "Registrierung fehlgeschlagen"); + return false; + } + + toast.success("Manager und Business erfolgreich registriert! Bitte bestätige deine E-Mail."); + return true; + } catch (err) { + if (err instanceof ZodError) { + const fieldErrors: Record = {}; + err.issues.forEach((issue) => { + const path = issue.path[0]; + if (typeof path === "string") fieldErrors[path] = issue.message; + }); + setErrors(fieldErrors); + return false; + } + toast.error("Ein unbekannter Fehler ist aufgetreten."); + return false; + } finally { + setLoading(false); + } + }; + return { loading, errors, registerUser, + registerAdmin, loginUser, setErrors, }; diff --git a/src/lib/auth/getUser.ts b/src/lib/auth/getUser.ts index 0373097..1663e88 100644 --- a/src/lib/auth/getUser.ts +++ b/src/lib/auth/getUser.ts @@ -2,7 +2,7 @@ import { createClient } from "@/src/lib/supabase/server"; import { prisma } from "@/src/lib/prisma"; import { cache } from "react"; -// Define the user data type +// Define the user data types export type UserData = { id: string; name: string | null; @@ -15,11 +15,31 @@ export type UserData = { gender: string | null; pbSrc: string | null; createdAt: Date | null; + role: "user"; }; +export type ManagerData = { + id: string; + firstName: string; + lastName: string; + email: string; + tel: string | null; + pbSrc: string | null; + createdAt: Date | null; + role: "manager"; + businesses: Array<{ + id: string; + name: string; + address: string; + email: string; + }>; +}; + +export type AuthUserData = UserData | ManagerData; + // Cache entry type type CacheEntry = { - data: UserData; + data: AuthUserData; timestamp: number; }; @@ -40,7 +60,7 @@ export const getUser = cache(async () => { }); // Cache user data from database with time-based cache -export const getUserData = cache(async (): Promise => { +export const getUserData = cache(async (): Promise => { userDataCallCount++; console.log(`📊 [DB] getUserData called (count: ${userDataCallCount})`); @@ -60,6 +80,7 @@ export const getUserData = cache(async (): Promise => { console.log("🗄️ [DB] Fetching user data from Prisma for:", user.email); + // Try to find in User table first const userData = await prisma.user.findUnique({ where: { id: user.id }, select: { @@ -77,14 +98,47 @@ export const getUserData = cache(async (): Promise => { }, }); - console.log("✅ [DB] User data fetched:", userData?.name); - - // Store in cache if (userData) { - userDataCache.set(user.id, { data: userData, timestamp: now }); + console.log("✅ [DB] User data fetched:", userData.name); + const userWithRole: UserData = { ...userData, role: "user" }; + userDataCache.set(user.id, { data: userWithRole, timestamp: now }); + return userWithRole; } - return userData; + // If not found in User table, try Manager table + const managerData = await prisma.manager.findUnique({ + where: { id: user.id }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + tel: true, + pbSrc: true, + createdAt: true, + businesses: { + select: { + id: true, + name: true, + address: true, + email: true, + }, + }, + }, + }); + + if (managerData) { + console.log( + "✅ [DB] Manager data fetched:", + `${managerData.firstName} ${managerData.lastName}` + ); + const managerWithRole: ManagerData = { ...managerData, role: "manager" }; + userDataCache.set(user.id, { data: managerWithRole, timestamp: now }); + return managerWithRole; + } + + console.log("⚠️ [DB] No user or manager found for:", user.email); + return null; }); export async function requireAuth() { @@ -95,7 +149,7 @@ export async function requireAuth() { export async function requireAuthWithData() { const userData = await getUserData(); if (!userData) { - throw new Error("User not found"); + throw new Error("User or Manager not found"); } return userData; } @@ -107,3 +161,13 @@ export async function requireGuest() { redirect("/dashboard"); } } + +// Helper function to check if user is a manager +export function isManager(userData: AuthUserData): userData is ManagerData { + return userData.role === "manager"; +} + +// Helper function to check if user is a regular user +export function isUser(userData: AuthUserData): userData is UserData { + return userData.role === "user"; +} diff --git a/src/modules/auth/ui/views/register-admin-view.tsx b/src/modules/auth/ui/views/register-admin-view.tsx index ffeff70..3797091 100644 --- a/src/modules/auth/ui/views/register-admin-view.tsx +++ b/src/modules/auth/ui/views/register-admin-view.tsx @@ -1,7 +1,292 @@ "use client"; +import { useState } from "react"; +import AuthSidebarComponent from "../components/AuthSidebarComponent"; +import InputComponent from "@/src/components/layout/InputComponent"; +import { Button } from "@/src/components/ui/button"; +import { useAuthFormStore } from "../../store/authFormState"; +import { useAuth } from "@/src/hooks/useAuth"; +import { ZodError } from "zod"; +import { + adminRegisterStep1Schema, + adminRegisterStep2Schema, + adminRegisterStep3Schema, +} from "../../validation/adminRegisterSchema"; +import { Spinner } from "@/src/components/ui/spinner"; +import { useRouter } from "next/navigation"; -const RegisterAdminView = () => { - return
Register Admin View
; +const RegisterAdmin = () => { + const [step, setStep] = useState(1); + const { registerAdmin, loading, errors, setErrors } = useAuth(); + const router = useRouter(); + const formData = useAuthFormStore(); + + const handleChange = (field: string, value: string) => { + formData.setField(field, value); + }; + + const handleNext = () => { + setErrors({}); + try { + if (step === 1) { + adminRegisterStep1Schema.parse({ + fullName: formData.fullName, + email: formData.email, + tel: formData.tel, + password: formData.password, + passwordConfirm: formData.passwordConfirm, + }); + setStep(2); + } else if (step === 2) { + adminRegisterStep2Schema.parse({ + plz: formData.plz, + place: formData.place, + street: formData.street, + }); + setStep(3); + } + } catch (err) { + if (err instanceof ZodError) { + const fieldErrors: Record = {}; + err.issues.forEach((issue) => { + const path = issue.path[0]; + if (typeof path === "string") { + fieldErrors[path] = issue.message; + } + }); + setErrors(fieldErrors); + } + } + }; + + const handleRegister = async () => { + setErrors({}); + try { + adminRegisterStep3Schema.parse({ + companyName: formData.companyName, + companyPlace: formData.companyPlace, + companyPLZ: formData.companyPLZ, + companyStreet: formData.companyStreet, + companyMail: formData.companyMail, + companyNumber: formData.companyNumber, + }); + + const success = await registerAdmin({ + fullName: formData.fullName, + email: formData.email, + tel: formData.tel, + password: formData.password, + passwordConfirm: formData.passwordConfirm, + plz: formData.plz, + place: formData.place, + street: formData.street, + companyName: formData.companyName, + companyPlace: formData.companyPlace, + companyPLZ: formData.companyPLZ, + companyStreet: formData.companyStreet, + companyMail: formData.companyMail, + companyNumber: formData.companyNumber, + }); + + if (success) { + formData.resetForm(); + router.push("/login"); + } + } catch (err) { + if (err instanceof ZodError) { + const fieldErrors: Record = {}; + err.issues.forEach((issue) => { + const path = issue.path[0]; + if (typeof path === "string") { + fieldErrors[path] = issue.message; + } + }); + setErrors(fieldErrors); + } + } + }; + + return ( +
+
+
+ +
+
+ +
+
+

Sign up for free

+ + {step === 1 && ( + <> + handleChange("fullName", e.target.value)} + error={errors.fullName} + /> + handleChange("email", e.target.value)} + error={errors.email} + /> + handleChange("tel", e.target.value)} + error={errors.tel} + /> + handleChange("password", e.target.value)} + error={errors.password} + /> + handleChange("passwordConfirm", e.target.value)} + error={errors.passwordConfirm} + /> + + + + )} + + {step === 2 && ( + <> + handleChange("plz", e.target.value)} + error={errors.plz} + /> + + handleChange("place", e.target.value)} + error={errors.place} + /> + handleChange("street", e.target.value)} + error={errors.street} + /> + + + + )} + + {step === 3 && ( + <> + handleChange("companyName", e.target.value)} + error={errors.companyName} + /> + handleChange("companyPlace", e.target.value)} + error={errors.companyPlace} + /> + + handleChange("companyPLZ", e.target.value)} + error={errors.companyPLZ} + /> + handleChange("companyStreet", e.target.value)} + error={errors.companyStreet} + /> + handleChange("companyMail", e.target.value)} + error={errors.companyMail} + /> + handleChange("companyNumber", e.target.value)} + error={errors.companyNumber} + /> + + + + )} + +
+

Ich habe schon einen Account

+

Login

+
+
+
+
+ ); }; -export default RegisterAdminView; +export default RegisterAdmin; diff --git a/src/modules/auth/validation/adminRegisterSchema.ts b/src/modules/auth/validation/adminRegisterSchema.ts index e69de29..e42049a 100644 --- a/src/modules/auth/validation/adminRegisterSchema.ts +++ b/src/modules/auth/validation/adminRegisterSchema.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +// Step 1: Personal Info +export const adminRegisterStep1Schema = z + .object({ + fullName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"), + email: z.string().email("Bitte eine gültige E-Mail-Adresse eingeben"), + tel: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein"), + password: z.string().min(8, "Passwort muss mindestens 8 Zeichen lang sein"), + passwordConfirm: z.string(), + }) + .refine((data) => data.password === data.passwordConfirm, { + message: "Passwürter müssen übereinstimmen", + path: ["passwordConfirm"], + }); + +// Step 2: Address Info +export const adminRegisterStep2Schema = z.object({ + plz: z.string().min(5, "PLZ muss 5 Zeichen lang sein").max(5, "PLZ muss 5 Zeichen lang sein"), + place: z.string().min(2, "Wohnort muss mindestens 2 Zeichen lang sein"), + street: z.string().min(3, "Straße und Hausnummer müssen mindestens 3 Zeichen lang sein"), +}); + +// Step 3: Business Info +export const adminRegisterStep3Schema = z.object({ + companyName: z.string().min(2, "Unternehmensname muss mindestens 2 Zeichen lang sein"), + companyPlace: z.string().min(2, "Unternehmenssitz muss mindestens 2 Zeichen lang sein"), + companyPLZ: z + .string() + .min(5, "PLZ muss 5 Zeichen lang sein") + .max(5, "PLZ muss 5 Zeichen lang sein"), + companyStreet: z.string().min(3, "Straße und Hausnummer müssen mindestens 3 Zeichen lang sein"), + companyMail: z + .string() + .email("Bitte eine gültige E-Mail-Adresse eingeben") + .optional() + .or(z.literal("")), + companyNumber: z.string().optional().or(z.literal("")), +}); + +// Full schema for complete validation +export const adminRegisterSchema = adminRegisterStep1Schema + .merge(adminRegisterStep2Schema) + .merge(adminRegisterStep3Schema); + +export type AdminRegisterFormData = z.infer; diff --git a/src/types/auth.ts b/src/types/auth.ts index b1521f0..4adf2c8 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -10,6 +10,23 @@ export interface RegisterFormData { gender: string; } +export interface AdminRegisterFormData { + fullName: string; + email: string; + tel: string; + password: string; + passwordConfirm: string; + plz: string; + place: string; + street: string; + companyName: string; + companyPlace: string; + companyPLZ: string; + companyStreet: string; + companyMail: string; + companyNumber: string; +} + export interface LoginFormData { email: string; password: string;