Merge pull request #18 from Since-3/feature/courses-edit

Able to edit the courses
This commit is contained in:
Rajpreet0 2025-12-14 18:06:58 +01:00 • committed by GitHub
commit e9f9400480
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 329 additions and 28 deletions

View file

@ -0,0 +1,8 @@
import CourseEditView from "@/src/modules/courses/ui/views/course-edit-view";
const CourseEdit = async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params;
return <CourseEditView courseId={id} />;
};
export default CourseEdit;

View file

@ -1,5 +0,0 @@
const MyCourses = () => {
return <div>myCourses</div>;
};
export default MyCourses;

View file

@ -1,5 +1,6 @@
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import { useEffect } from "react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Underline from "@tiptap/extension-underline";
@ -50,6 +51,13 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
},
});
// Sync editor content when value prop changes (for edit mode)
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value, { emitUpdate: false });
}
}, [editor, value]);
if (!editor) {
return null;
}

View file

@ -200,6 +200,172 @@ export async function getMyCourses(options?: { dateFrom?: Date; dateTo?: Date })
}
}
/**
* Ruft einen einzelnen Kurs nach ID ab
*
* Diese Funktion gibt einen Kurs zurück, wenn der eingeloggte Manager
* der Ersteller des Kurses ist.
*
* @param courseId - Die eindeutige ID des Kurses
* @returns Ein Objekt mit success-Flag und dem Kurs oder Fehler
*/
export async function getCourseById(courseId: string) {
try {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
if (!userData || !isManager(userData)) {
return {
success: false,
error: "Nur Manager können Kurse bearbeiten",
};
}
// Schritt 3: Kurs aus der Datenbank abrufen
const course = await prisma.course.findUnique({
where: { id: courseId },
});
// Schritt 4: Überprüfen, ob Kurs existiert
if (!course) {
return {
success: false,
error: "Kurs nicht gefunden",
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (course.createdBy !== userData.id) {
return {
success: false,
error: "Sie können nur Ihre eigenen Kurse bearbeiten",
};
}
return {
success: true,
course,
};
} catch (error) {
console.error("Error fetching course:", error);
return {
success: false,
error: "Fehler beim Laden des Kurses",
};
}
}
/**
* Aktualisiert einen bestehenden Kurs
*
* Diese Funktion validiert die Eingabedaten, überprüft die Berechtigungen des Benutzers
* und aktualisiert einen bestehenden Kurs. Nur der Manager, der den Kurs erstellt hat,
* kann ihn bearbeiten.
*
* @param courseId - Die eindeutige ID des zu aktualisierenden Kurses
* @param data - Die aktualisierten Kursdaten aus dem Formular
* @param status - Der Status des Kurses ("draft" für Entwurf oder "published" für veröffentlicht)
* @returns Ein Objekt mit success-Flag und Nachricht oder Fehler
*/
export async function updateCourse(
courseId: string,
data: CourseFormData,
status: "draft" | "published"
) {
try {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer angemeldet ist
if (!userData) {
return {
success: false,
error: "Sie müssen angemeldet sein, um einen Kurs zu bearbeiten",
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist
if (!isManager(userData)) {
return {
success: false,
error: "Nur Manager können Kurse bearbeiten",
};
}
// Schritt 4: Kurs aus der Datenbank abrufen
const existingCourse = await prisma.course.findUnique({
where: { id: courseId },
});
// Schritt 5: Überprüfen, ob Kurs existiert
if (!existingCourse) {
return {
success: false,
error: "Kurs nicht gefunden",
};
}
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (existingCourse.createdBy !== userData.id) {
return {
success: false,
error: "Sie können nur Ihre eigenen Kurse bearbeiten",
};
}
// Schritt 7: Formulardaten mit Zod-Schema validieren
const validation = courseSchema.safeParse(data);
if (!validation.success) {
return {
success: false,
error: "Validierungsfehler",
fieldErrors: validation.error.flatten().fieldErrors,
};
}
const validatedData = validation.data;
// Schritt 8: Kurs in der Datenbank aktualisieren
await prisma.course.update({
where: { id: courseId },
data: {
name: validatedData.name,
sport: validatedData.sport,
date: new Date(validatedData.date),
time: validatedData.time,
trainers: validatedData.trainers,
room: validatedData.room,
description: validatedData.description,
maxParticipants: validatedData.maxParticipants,
isStandingOrder: validatedData.isStandingOrder,
frequency: validatedData.frequency || null,
weekdays: validatedData.weekdays || [],
status,
},
});
// Schritt 9: Next.js Cache für /courses Seite invalidieren
revalidatePath("/courses");
revalidatePath(`/courses/edit/${courseId}`);
return {
success: true,
message:
status === "published"
? "Kurs erfolgreich aktualisiert und veröffentlicht"
: "Entwurf erfolgreich aktualisiert",
};
} catch (error) {
console.error("Error updating course:", error);
return {
success: false,
error: "Ein Fehler ist beim Aktualisieren des Kurses aufgetreten",
};
}
}
/**
* Löscht einen Kurs aus der Datenbank
*

View file

@ -0,0 +1,18 @@
export type Course = {
id: string;
name: string;
sport: string;
date: Date;
time: string;
trainers: string[];
room: string;
description: string;
maxParticipants: number;
isStandingOrder: boolean;
frequency: string | null;
weekdays: string[];
status: string;
createdBy: string;
createdAt: Date;
updatedAt: Date;
};

View file

@ -1,5 +1,5 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import InputComponent from "@/src/components/layout/InputComponent";
import GenericDropdown from "@/src/components/layout/GenericDropdown";
@ -8,8 +8,9 @@ import RichTextEditor from "@/src/components/layout/RichTextEditor";
import { Button } from "@/src/components/ui/button";
import { Checkbox } from "@/src/components/ui/checkbox";
import { Label } from "@/src/components/ui/label";
import { createCourse } from "../../actions/course-actions";
import { createCourse, updateCourse } from "../../actions/course-actions";
import { toast } from "sonner";
import { Course } from "../../types/course.types";
// Hardcoded data
const SPORTS = [
@ -54,7 +55,17 @@ const WEEKDAYS = [
{ value: "sunday", label: "Sonntag" },
];
const CourseCreateView = () => {
interface CourseCreateViewProps {
mode?: "create" | "edit";
courseId?: string;
initialData?: Course;
}
const CourseCreateView = ({
mode = "create",
courseId,
initialData,
}: CourseCreateViewProps = {}) => {
const router = useRouter();
const [courseName, setCourseName] = useState("");
const [courseDate, setCourseDate] = useState("");
@ -70,6 +81,34 @@ const CourseCreateView = () => {
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Load initial data when in edit mode
useEffect(() => {
if (mode === "edit" && initialData) {
setCourseName(initialData.name || "");
setSelectedSport(initialData.sport || "");
// Format date for input field (YYYY-MM-DD)
if (initialData.date) {
const date = new Date(initialData.date);
const formattedDate = [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, "0"),
String(date.getDate()).padStart(2, "0"),
].join("-");
setCourseDate(formattedDate);
}
setCourseTime(initialData.time || "");
setSelectedTrainers(initialData.trainers || []);
setSelectedRoom(initialData.room || "");
setDescription(initialData.description || "");
setMaxParticipants(initialData.maxParticipants?.toString() || "");
setIsStandingOrder(initialData.isStandingOrder || false);
setSelectedFrequency(initialData.frequency || "");
setSelectedWeekdays(initialData.weekdays || []);
}
}, [mode, initialData]);
const validateForm = () => {
const newErrors: Record<string, string> = {};
@ -110,22 +149,28 @@ const CourseCreateView = () => {
setIsSubmitting(true);
try {
const result = await createCourse(
{
name: courseName,
sport: selectedSport,
date: courseDate,
time: courseTime,
trainers: selectedTrainers,
room: selectedRoom,
description,
maxParticipants: parseInt(maxParticipants),
isStandingOrder,
frequency: selectedFrequency || undefined,
weekdays: selectedWeekdays.length > 0 ? selectedWeekdays : undefined,
},
status
);
const courseData = {
name: courseName,
sport: selectedSport,
date: courseDate,
time: courseTime,
trainers: selectedTrainers,
room: selectedRoom,
description,
maxParticipants: parseInt(maxParticipants),
isStandingOrder,
frequency: selectedFrequency || undefined,
weekdays: selectedWeekdays.length > 0 ? selectedWeekdays : undefined,
};
if (mode === "edit" && !courseId) {
throw new Error("Missing courseId in edit mode");
}
const result =
mode === "edit"
? await updateCourse(courseId!, courseData, status)
: await createCourse(courseData, status);
if (result.success) {
toast.success(result.message);
@ -152,9 +197,11 @@ const CourseCreateView = () => {
return (
<div>
<h1 className="text-3xl font-bold">Kurs anlegen </h1>
<h1 className="text-3xl font-bold">{mode === "edit" ? "Kurs bearbeiten" : "Kurs anlegen"}</h1>
<p className="text-xl mt-2">
Alle Kurse können nach dem Speichern angepasst oder gelöscht werden
{mode === "edit"
? "Ändern Sie die Kursdetails und speichern Sie Ihre Änderungen"
: "Alle Kurse können nach dem Speichern angepasst oder gelöscht werden"}
</p>
<div className="mt-6 flex flex-col gap-4 max-w-xl">
<div>
@ -285,7 +332,11 @@ const CourseCreateView = () => {
)}
<div className="flex gap-2">
<Button onClick={() => handleSubmit("published")} disabled={isSubmitting}>
{isSubmitting ? "Wird gespeichert..." : "Veröffentlichen"}
{isSubmitting
? "Wird gespeichert..."
: mode === "edit"
? "Änderungen speichern"
: "Veröffentlichen"}
</Button>
<Button variant="outline" onClick={() => router.push("/courses")} disabled={isSubmitting}>
Abbrechen
@ -296,7 +347,7 @@ const CourseCreateView = () => {
onClick={() => handleSubmit("draft")}
disabled={isSubmitting}
>
Entwurf speichern
{mode === "edit" ? "Als Entwurf speichern" : "Entwurf speichern"}
</button>
</div>
</div>

View file

@ -0,0 +1,55 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { getCourseById } from "../../actions/course-actions";
import { toast } from "sonner";
import CourseCreateView from "./course-create-view";
import { Course } from "../../types/course.types";
interface CourseEditViewProps {
courseId: string;
}
const CourseEditView = ({ courseId }: CourseEditViewProps) => {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [courseData, setCourseData] = useState<Course | null>(null);
useEffect(() => {
let alive = true;
const fetchCourse = async () => {
if (alive) setIsLoading(true);
const result = await getCourseById(courseId);
if (result.success && result.course) {
if (alive) setCourseData(result.course);
} else {
toast.error(result.error || "Fehler beim Laden des Kurses");
router.push("/courses");
}
if (alive) setIsLoading(false);
};
fetchCourse();
return () => {
alive = false;
};
}, [courseId, router]);
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<p className="text-xl">Kurs wird geladen...</p>
</div>
);
}
if (!courseData) {
return null;
}
return <CourseCreateView mode="edit" courseId={courseId} initialData={courseData} />;
};
export default CourseEditView;