some ui mobile fixes and course validation fixes

This commit is contained in:
Rajpreet0 2026-04-14 23:57:49 +02:00
parent b847e16758
commit c24df3d29b
4 changed files with 110 additions and 124 deletions

View file

@ -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 || [],

View file

@ -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<typeof courseSchema>;

View file

@ -161,7 +161,11 @@ const CourseFilter: React.FC<CourseFilterProps> = ({
return (
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
<SheetTrigger asChild>{children}</SheetTrigger>
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col">
<SheetContent
side="right"
className="w-full sm:max-w-md flex flex-col"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<SheetHeader>
<SheetTitle className="text-2xl">Kurse Filtern</SheetTitle>
</SheetHeader>
@ -177,7 +181,7 @@ const CourseFilter: React.FC<CourseFilterProps> = ({
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"
/>
</div>
@ -188,7 +192,7 @@ const CourseFilter: React.FC<CourseFilterProps> = ({
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"
/>
</div>
@ -301,7 +305,7 @@ const CourseFilter: React.FC<CourseFilterProps> = ({
</div>
{/* Uhrzeit */}
<h3 className=" font-medium mt-2">Uhrzeit</h3>
<h3 className="font-medium mt-2">Uhrzeit</h3>
{/* From */}
<div className="space-y-1">
@ -310,7 +314,7 @@ const CourseFilter: React.FC<CourseFilterProps> = ({
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"
/>
</div>
@ -321,7 +325,7 @@ const CourseFilter: React.FC<CourseFilterProps> = ({
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"
/>
</div>
</AccordionContent>

View file

@ -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<string, string> = {};
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,