diff --git a/src/modules/courses/actions/course-actions.ts b/src/modules/courses/actions/course-actions.ts index 5d4d0d0..70e3fba 100644 --- a/src/modules/courses/actions/course-actions.ts +++ b/src/modules/courses/actions/course-actions.ts @@ -10,7 +10,7 @@ import { prisma } from "@/src/lib/prisma"; import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser"; -import { courseSchema, type CourseFormData } from "../schemas/course-schema"; +import { courseSchema, publishedCourseSchema, type CourseFormData } from "../schemas/course-schema"; import { revalidatePath } from "next/cache"; import { Prisma } from "@prisma/client"; @@ -48,7 +48,8 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ } // Schritt 4: Formulardaten mit Zod-Schema validieren - const validation = courseSchema.safeParse(data); + const schema = status === "published" ? publishedCourseSchema : courseSchema; + const validation = schema.safeParse(data); if (!validation.success) { return { @@ -70,17 +71,17 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ // Schritt 6: Kurs in der Datenbank erstellen const course = await prisma.course.create({ data: { - name: validatedData.name, - sport: validatedData.sport, + name: validatedData.name || "", + sport: validatedData.sport || [], level: validatedData.level || "any", - date: new Date(validatedData.date), - timeFrom: validatedData.timeFrom, - timeTo: validatedData.timeTo, - trainers: validatedData.trainers, - room: validatedData.room, - description: validatedData.description, - maxParticipants: validatedData.maxParticipants, - price: validatedData.price, + date: new Date(validatedData.date || new Date()), + timeFrom: validatedData.timeFrom || "", + timeTo: validatedData.timeTo || "", + trainers: validatedData.trainers || [], + room: validatedData.room || "", + description: validatedData.description || "", + maxParticipants: validatedData.maxParticipants || 0, + price: validatedData.price ?? 0, isStandingOrder: validatedData.isStandingOrder, frequency: validatedData.frequency || null, weekdays: validatedData.weekdays || [], @@ -361,7 +362,8 @@ export async function updateCourse( } // Schritt 8: Formulardaten mit Zod-Schema validieren - const validation = courseSchema.safeParse(data); + const schema = status === "published" ? publishedCourseSchema : courseSchema; + const validation = schema.safeParse(data); if (!validation.success) { return { @@ -377,17 +379,17 @@ export async function updateCourse( await prisma.course.update({ where: { id: courseId }, data: { - name: validatedData.name, - sport: validatedData.sport, + name: validatedData.name || "", + sport: validatedData.sport || [], level: validatedData.level || "any", - date: new Date(validatedData.date), - timeFrom: validatedData.timeFrom, - timeTo: validatedData.timeTo, - trainers: validatedData.trainers, - room: validatedData.room, - description: validatedData.description, - maxParticipants: validatedData.maxParticipants, - price: validatedData.price, + date: new Date(validatedData.date || new Date()), + timeFrom: validatedData.timeFrom || "", + timeTo: validatedData.timeTo || "", + trainers: validatedData.trainers || [], + room: validatedData.room || "", + description: validatedData.description || "", + maxParticipants: validatedData.maxParticipants || 0, + price: validatedData.price ?? 0, isStandingOrder: validatedData.isStandingOrder, frequency: validatedData.frequency || null, weekdays: validatedData.weekdays || [], diff --git a/src/modules/courses/schemas/course-schema.ts b/src/modules/courses/schemas/course-schema.ts index 082932c..56d1296 100644 --- a/src/modules/courses/schemas/course-schema.ts +++ b/src/modules/courses/schemas/course-schema.ts @@ -1,101 +1,25 @@ -/** - * Course Schema - Validierungsschema für Kurs-Formulare - * - * Diese Datei definiert das Zod-Schema zur Validierung von Kursdaten. - * Es stellt sicher, dass alle erforderlichen Felder korrekt ausgefüllt werden - * und die Daten den definierten Regeln entsprechen. - */ - import { z } from "zod"; -/** - * Haupt-Validierungsschema für Kurse - * - * Dieses Schema validiert: - * - Grundlegende Kursinformationen (Name, Sportart, Datum, Zeit) - * - Trainer und Raum-Zuweisungen - * - Teilnehmerbegrenzungen (1-100 Personen) - * - Dauerauftrag-Einstellungen mit Häufigkeit und Wochentagen - */ +// Basis-Schema mit allen Feldern optional — für Entwürfe export const courseSchema = z .object({ - // Kursname (Pflichtfeld, min. 1 Zeichen) - name: z.string().min(1, "Kursname ist erforderlich"), - - // Sportarten (Pflichtfeld, z.B. Fußball, Basketball) - sport: z.array(z.string()).min(1, "Mindestens eine Sportart ist erforderlich"), - - // Niveau (optional, Standard: "any") + name: z.string().min(1, "Kursname ist erforderlich").optional().or(z.literal("")), + sport: z.array(z.string()).optional(), level: z.string().optional(), - - // Kursdatum als String im Format YYYY-MM-DD - date: z.string().min(1, "Datum ist erforderlich"), - - // Anfangszeit als String im Format HH:MM - timeFrom: z.string().min(1, "Anfangszeit ist erforderlich"), - - // Endzeit als String im Format HH:MM - timeTo: z.string().min(1, "Endzeit ist erforderlich"), - - // Array von Trainer-IDs (mindestens ein Trainer erforderlich) - trainers: z.array(z.string()).min(1, "Mindestens ein Trainer muss ausgewählt werden"), - - // Raum-ID für den Kurs (z.B. "Sporthalle 1") - room: z.string().min(1, "Raum ist erforderlich"), - - // Rich-Text Beschreibung mit HTML-Inhalt - description: z.string().min(1, "Beschreibung ist erforderlich"), - - // Maximale Anzahl der Teilnehmer (1-100) - maxParticipants: z - .number() - .min(1, "Mindestens 1 Teilnehmer erforderlich") - .max(100, "Maximal 100 Teilnehmer erlaubt"), - - // Preis in Euro (0-9999.99) - price: z - .number() - .min(0, "Preis muss eine gültige positive Zahl sein") - .max(9999.99, "Preis darf maximal 9.999,99 € betragen"), - - // Gibt an, ob der Kurs ein Dauerauftrag ist (regelmäßig wiederkehrend) + date: z.string().optional().or(z.literal("")), + timeFrom: z.string().optional().or(z.literal("")), + timeTo: z.string().optional().or(z.literal("")), + trainers: z.array(z.string()).optional(), + room: z.string().optional().or(z.literal("")), + description: z.string().optional(), + maxParticipants: z.number().max(100).optional(), + price: z.number().max(9999.99).optional(), isStandingOrder: z.boolean(), - - // Häufigkeit des Dauerauftrags (täglich, wöchentlich, etc.) - optional frequency: z.string().optional(), - - // Ausgewählte Wochentage für individuelle Häufigkeit - optional weekdays: z.array(z.string()).optional(), }) .refine( (data) => { - // Validierungsregel: Wenn Dauerauftrag aktiviert ist, muss eine Häufigkeit ausgewählt werden - if (data.isStandingOrder && !data.frequency) { - return false; - } - return true; - }, - { - message: "Häufigkeit ist erforderlich bei Daueraufträgen", - path: ["frequency"], - } - ) - .refine( - (data) => { - // Validierungsregel: Bei individueller Häufigkeit müssen Wochentage ausgewählt werden - if (data.frequency === "custom" && (!data.weekdays || data.weekdays.length === 0)) { - return false; - } - return true; - }, - { - message: "Mindestens ein Tag muss ausgewählt werden bei individueller Häufigkeit", - path: ["weekdays"], - } - ) - .refine( - (data) => { - // Validierungsregel: Anfangszeit muss vor der Endzeit liegen if (data.timeFrom && data.timeTo && data.timeFrom >= data.timeTo) { return false; } @@ -107,8 +31,54 @@ export const courseSchema = z } ); -/** - * TypeScript-Typ für Kursdaten - * Wird automatisch aus dem Zod-Schema generiert - */ +// Strenges Schema für veröffentlichte Kurse — alle Pflichtfelder erforderlich +export const publishedCourseSchema = z + .object({ + name: z.string().min(1, "Kursname ist erforderlich"), + sport: z.array(z.string()).min(1, "Mindestens eine Sportart ist erforderlich"), + level: z.string().optional(), + date: z.string().min(1, "Datum ist erforderlich"), + timeFrom: z.string().min(1, "Anfangszeit ist erforderlich"), + timeTo: z.string().min(1, "Endzeit ist erforderlich"), + trainers: z.array(z.string()).min(1, "Mindestens ein Trainer muss ausgewählt werden"), + room: z.string().min(1, "Raum ist erforderlich"), + description: z.string().optional(), + maxParticipants: z + .number() + .min(1, "Mindestens 1 Teilnehmer erforderlich") + .max(100, "Maximal 100 Teilnehmer erlaubt"), + price: z + .number() + .min(0, "Preis muss eine gültige positive Zahl sein") + .max(9999.99, "Preis darf maximal 9.999,99 € betragen"), + isStandingOrder: z.boolean(), + frequency: z.string().optional(), + weekdays: z.array(z.string()).optional(), + }) + .refine( + (data) => { + if (data.isStandingOrder && !data.frequency) return false; + return true; + }, + { message: "Häufigkeit ist erforderlich bei Daueraufträgen", path: ["frequency"] } + ) + .refine( + (data) => { + if (data.frequency === "custom" && (!data.weekdays || data.weekdays.length === 0)) + return false; + return true; + }, + { + message: "Mindestens ein Tag muss ausgewählt werden bei individueller Häufigkeit", + path: ["weekdays"], + } + ) + .refine( + (data) => { + if (data.timeFrom && data.timeTo && data.timeFrom >= data.timeTo) return false; + return true; + }, + { message: "Endzeit muss nach der Anfangszeit liegen", path: ["timeTo"] } + ); + export type CourseFormData = z.infer; diff --git a/src/modules/courses/ui/components/CourseFilter.tsx b/src/modules/courses/ui/components/CourseFilter.tsx index e9b836b..e8ef8ca 100644 --- a/src/modules/courses/ui/components/CourseFilter.tsx +++ b/src/modules/courses/ui/components/CourseFilter.tsx @@ -161,7 +161,11 @@ const CourseFilter: React.FC = ({ return ( {children} - + e.preventDefault()} + > Kurse Filtern @@ -177,7 +181,7 @@ const CourseFilter: React.FC = ({ type="date" value={localDateFrom} onChange={(e) => setLocalDateFrom(e.target.value)} - className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition" + className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none [&::-webkit-datetime-edit]:p-0 [&::-webkit-datetime-edit-fields-wrapper]:p-0" /> @@ -188,7 +192,7 @@ const CourseFilter: React.FC = ({ type="date" value={localDateTo} onChange={(e) => setLocalDateTo(e.target.value)} - className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition" + className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none [&::-webkit-datetime-edit]:p-0 [&::-webkit-datetime-edit-fields-wrapper]:p-0" /> @@ -301,7 +305,7 @@ const CourseFilter: React.FC = ({ {/* Uhrzeit */} -

Uhrzeit

+

Uhrzeit

{/* From */}
@@ -310,7 +314,7 @@ const CourseFilter: React.FC = ({ type="time" value={localTimeFrom} onChange={(e) => setLocalTimeFrom(e.target.value)} - className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition" + className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none [&::-webkit-datetime-edit]:p-0 [&::-webkit-datetime-edit-fields-wrapper]:p-0" />
@@ -321,7 +325,7 @@ const CourseFilter: React.FC = ({ type="time" value={localTimeTo} onChange={(e) => setLocalTimeTo(e.target.value)} - className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition" + className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none [&::-webkit-datetime-edit]:p-0 [&::-webkit-datetime-edit-fields-wrapper]:p-0" /> diff --git a/src/modules/courses/ui/views/course-create-view.tsx b/src/modules/courses/ui/views/course-create-view.tsx index 2543eee..345edec 100644 --- a/src/modules/courses/ui/views/course-create-view.tsx +++ b/src/modules/courses/ui/views/course-create-view.tsx @@ -171,7 +171,6 @@ const CourseCreateView = ({ if (selectedTrainers.length === 0) newErrors.trainers = "Mindestens ein Trainer muss ausgewählt werden"; if (!selectedRoom) newErrors.room = "Raum ist erforderlich"; - if (!description.trim()) newErrors.description = "Beschreibung ist erforderlich"; const maxPart = parseInt(maxParticipants); if (!maxParticipants || isNaN(maxPart) || maxPart < 1) { @@ -201,11 +200,22 @@ const CourseCreateView = ({ return Object.keys(newErrors).length === 0; }; + const validateDraft = () => { + const newErrors: Record = {}; + if (!courseName.trim()) newErrors.name = "Kursname ist erforderlich"; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + const handleSubmit = async (status: "draft" | "published") => { - if (!validateForm()) { + if (status === "published" && !validateForm()) { toast.error("Bitte füllen Sie alle erforderlichen Felder aus"); return; } + if (status === "draft" && !validateDraft()) { + toast.error("Bitte geben Sie mindestens einen Kursnamen ein"); + return; + } setIsSubmitting(true); @@ -220,8 +230,8 @@ const CourseCreateView = ({ trainers: selectedTrainers, room: selectedRoom, description, - maxParticipants: parseInt(maxParticipants), - price: parseFloat(price), + maxParticipants: maxParticipants ? parseInt(maxParticipants) : undefined, + price: price ? parseFloat(price) : undefined, isStandingOrder, frequency: selectedFrequency || undefined, weekdays: selectedWeekdays.length > 0 ? selectedWeekdays : undefined,