mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
Merge pull request #79 from Since-3/refactor/fixes
some ui mobile fixes and course validation fixes
This commit is contained in:
commit
873160e2a2
4 changed files with 110 additions and 124 deletions
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue