mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
Merge pull request #18 from Since-3/feature/courses-edit
Able to edit the courses
This commit is contained in:
commit
e9f9400480
7 changed files with 329 additions and 28 deletions
8
src/app/(main)/courses/edit/[id]/page.tsx
Normal file
8
src/app/(main)/courses/edit/[id]/page.tsx
Normal 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;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
const MyCourses = () => {
|
||||
return <div>myCourses</div>;
|
||||
};
|
||||
|
||||
export default MyCourses;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
18
src/modules/courses/types/course.types.ts
Normal file
18
src/modules/courses/types/course.types.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
55
src/modules/courses/ui/views/course-edit-view.tsx
Normal file
55
src/modules/courses/ui/views/course-edit-view.tsx
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue