mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
587 lines
18 KiB
TypeScript
587 lines
18 KiB
TypeScript
/**
|
|
* Course Actions - Server-seitige Aktionen für Kursverwaltung
|
|
*
|
|
* Diese Datei enthält alle Server Actions für das Kurs-Management-System.
|
|
* Funktionen umfassen: Kurse erstellen, abrufen, aktualisieren und löschen.
|
|
* Alle Funktionen sind mit Authentifizierung und Autorisierung geschützt.
|
|
*/
|
|
|
|
"use server";
|
|
|
|
import { prisma } from "@/src/lib/prisma";
|
|
import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser";
|
|
import { courseSchema, publishedCourseSchema, type CourseFormData } from "../schemas/course-schema";
|
|
import { revalidatePath } from "next/cache";
|
|
import { Prisma } from "@prisma/client";
|
|
import { generateUniqueSlug } from "@/src/lib/slug";
|
|
|
|
/**
|
|
* Erstellt einen neuen Kurs in der Datenbank
|
|
*
|
|
* Diese Funktion validiert die Eingabedaten, überprüft die Berechtigungen des Benutzers
|
|
* und erstellt einen neuen Kurs. Nur Manager können Kurse erstellen.
|
|
*
|
|
* @param data - Die 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, optionaler courseId und Nachricht oder Fehler
|
|
*/
|
|
export async function createCourse(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 erstellen",
|
|
};
|
|
}
|
|
|
|
// Schritt 3: Manager oder Employee mit Berechtigung
|
|
const canCreate =
|
|
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.create);
|
|
if (!canCreate) {
|
|
return {
|
|
success: false,
|
|
error: "Keine Berechtigung zum Erstellen von Kursen",
|
|
};
|
|
}
|
|
|
|
// Schritt 4: Formulardaten mit Zod-Schema validieren
|
|
const schema = status === "published" ? publishedCourseSchema : courseSchema;
|
|
const validation = schema.safeParse(data);
|
|
|
|
if (!validation.success) {
|
|
return {
|
|
success: false,
|
|
error: "Validierungsfehler",
|
|
fieldErrors: validation.error.flatten().fieldErrors,
|
|
};
|
|
}
|
|
|
|
const validatedData = validation.data;
|
|
|
|
// Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
|
|
const creatorId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
// Schritt 6: Kurs in der Datenbank erstellen
|
|
const course = await prisma.course.create({
|
|
data: {
|
|
name: validatedData.name || "",
|
|
sport: validatedData.sport || [],
|
|
level: validatedData.level || "any",
|
|
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 || [],
|
|
status,
|
|
createdBy: creatorId,
|
|
businessId: validatedData.businessId ?? null,
|
|
},
|
|
});
|
|
|
|
// Schritt 6: Next.js Cache für /courses Seite invalidieren, damit neue Daten angezeigt werden
|
|
revalidatePath("/courses");
|
|
|
|
return {
|
|
success: true,
|
|
courseId: course.id,
|
|
message:
|
|
status === "published"
|
|
? "Kurs erfolgreich veröffentlicht"
|
|
: "Entwurf erfolgreich gespeichert",
|
|
};
|
|
} catch (error) {
|
|
console.error("Error creating course:", error);
|
|
return {
|
|
success: false,
|
|
error: "Ein Fehler ist beim Erstellen des Kurses aufgetreten",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ruft alle veröffentlichten Kurse ab
|
|
*
|
|
* Diese Funktion gibt alle Kurse zurück, die den Status "published" haben.
|
|
* Die Kurse werden nach Datum aufsteigend sortiert.
|
|
*
|
|
* @returns Ein Objekt mit success-Flag und einem Array von Kursen
|
|
*/
|
|
export async function getAllCourses() {
|
|
try {
|
|
// Alle veröffentlichten Kurse aus der Datenbank abrufen
|
|
// Sortiert nach Datum aufsteigend (älteste zuerst)
|
|
const courses = await prisma.course.findMany({
|
|
where: {
|
|
status: "published", // Nur veröffentlichte Kurse
|
|
},
|
|
orderBy: {
|
|
date: "asc", // Aufsteigende Sortierung nach Datum
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
courses,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error fetching courses:", error);
|
|
return {
|
|
success: false,
|
|
error: "Fehler beim Laden der Kurse",
|
|
courses: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches all courses created by the logged-in manager
|
|
*
|
|
* This function returns all courses created by the currently logged-in manager,
|
|
* regardless of status (draft or published). Supports optional date range filtering
|
|
* to implement efficient loading for large datasets.
|
|
*
|
|
* @param options - Optional filtering parameters
|
|
* @param options.dateFrom - Start date for filtering (inclusive)
|
|
* @param options.dateTo - End date for filtering (inclusive)
|
|
* @returns An object with success flag and an array of the manager's courses
|
|
*/
|
|
export async function getMyCourses(options?: { dateFrom?: Date; dateTo?: Date }) {
|
|
try {
|
|
// Step 1: Get current user
|
|
const userData = await getUserData();
|
|
|
|
// Step 2: Manager oder Employee mit Berechtigung
|
|
if (
|
|
!userData ||
|
|
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view))
|
|
) {
|
|
return {
|
|
success: false,
|
|
error: "Unauthorized",
|
|
courses: [],
|
|
};
|
|
}
|
|
|
|
// Step 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
|
|
const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
// Step 4: Build the where clause with optional date filtering
|
|
const whereClause: Prisma.CourseWhereInput = {
|
|
createdBy: managerId,
|
|
};
|
|
|
|
// Add date range filter if provided
|
|
if (options?.dateFrom || options?.dateTo) {
|
|
whereClause.date = {};
|
|
|
|
if (options.dateFrom) {
|
|
// Filter courses from this date onwards (inclusive)
|
|
whereClause.date.gte = options.dateFrom;
|
|
}
|
|
|
|
if (options.dateTo) {
|
|
// Filter courses up to this date (inclusive)
|
|
whereClause.date.lte = options.dateTo;
|
|
}
|
|
}
|
|
|
|
// Step 4: Fetch courses created by this manager with optional date filtering
|
|
const courses = await prisma.course.findMany({
|
|
where: whereClause,
|
|
include: {
|
|
_count: {
|
|
select: { bookings: true },
|
|
},
|
|
},
|
|
orderBy: {
|
|
date: "asc", // Sort by course date ascending (oldest first)
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
courses,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error fetching my courses:", error);
|
|
return {
|
|
success: false,
|
|
error: "Fehler beim Laden der Kurse",
|
|
courses: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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: Manager oder Employee mit Berechtigung
|
|
if (
|
|
!userData ||
|
|
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view))
|
|
) {
|
|
return {
|
|
success: false,
|
|
error: "Nur Manager können Kurse bearbeiten",
|
|
};
|
|
}
|
|
|
|
// Schritt 3: Effektive Manager-ID ermitteln
|
|
const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
// Schritt 4: Kurs aus der Datenbank abrufen
|
|
const course = await prisma.course.findUnique({
|
|
where: { id: courseId },
|
|
});
|
|
|
|
// Schritt 5: Überprüfen, ob Kurs existiert
|
|
if (!course) {
|
|
return {
|
|
success: false,
|
|
error: "Kurs nicht gefunden",
|
|
};
|
|
}
|
|
|
|
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Kurses ist
|
|
if (course.createdBy !== managerId) {
|
|
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: Manager oder Employee mit Berechtigung
|
|
const canEdit =
|
|
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.edit);
|
|
if (!canEdit) {
|
|
return {
|
|
success: false,
|
|
error: "Keine Berechtigung zum Bearbeiten von Kursen",
|
|
};
|
|
}
|
|
|
|
// Schritt 4: Effektive Manager-ID ermitteln
|
|
const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
// Schritt 5: Kurs aus der Datenbank abrufen
|
|
const existingCourse = await prisma.course.findUnique({
|
|
where: { id: courseId },
|
|
});
|
|
|
|
// Schritt 6: Überprüfen, ob Kurs existiert
|
|
if (!existingCourse) {
|
|
return {
|
|
success: false,
|
|
error: "Kurs nicht gefunden",
|
|
};
|
|
}
|
|
|
|
// Schritt 7: Überprüfen, ob der Manager der Besitzer des Kurses ist
|
|
if (existingCourse.createdBy !== managerId) {
|
|
return {
|
|
success: false,
|
|
error: "Sie können nur Ihre eigenen Kurse bearbeiten",
|
|
};
|
|
}
|
|
|
|
// Schritt 8: Formulardaten mit Zod-Schema validieren
|
|
const schema = status === "published" ? publishedCourseSchema : courseSchema;
|
|
const validation = schema.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 || [],
|
|
level: validatedData.level || "any",
|
|
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 || [],
|
|
status,
|
|
...(validatedData.businessId !== undefined
|
|
? { businessId: validatedData.businessId || null }
|
|
: {}),
|
|
},
|
|
});
|
|
|
|
// 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
|
|
*
|
|
* Diese Funktion löscht einen Kurs, überprüft aber vorher:
|
|
* - Ob der Benutzer ein Manager ist
|
|
* - Ob der Kurs existiert
|
|
* - Ob der Manager der Besitzer des Kurses ist (Manager können nur eigene Kurse löschen)
|
|
*
|
|
* @param courseId - Die eindeutige ID des zu löschenden Kurses
|
|
* @returns Ein Objekt mit success-Flag und Nachricht oder Fehler
|
|
*/
|
|
export async function deleteCourse(courseId: string) {
|
|
try {
|
|
// Schritt 1: Aktuellen Benutzer abrufen
|
|
const userData = await getUserData();
|
|
|
|
// Schritt 2: Manager oder Employee mit Berechtigung
|
|
if (
|
|
!userData ||
|
|
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.delete))
|
|
) {
|
|
return {
|
|
success: false,
|
|
error: "Keine Berechtigung zum Löschen von Kursen",
|
|
};
|
|
}
|
|
|
|
// Schritt 3: Effektive Manager-ID ermitteln
|
|
const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
// Schritt 4: Kurs aus der Datenbank abrufen
|
|
const course = await prisma.course.findUnique({
|
|
where: { id: courseId },
|
|
});
|
|
|
|
// Schritt 5: Überprüfen, ob Kurs existiert
|
|
if (!course) {
|
|
return {
|
|
success: false,
|
|
error: "Kurs nicht gefunden",
|
|
};
|
|
}
|
|
|
|
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Kurses ist
|
|
if (course.createdBy !== managerId) {
|
|
return {
|
|
success: false,
|
|
error: "Sie können nur Ihre eigenen Kurse löschen",
|
|
};
|
|
}
|
|
|
|
// Schritt 7: Kurs aus der Datenbank löschen
|
|
await prisma.course.delete({
|
|
where: { id: courseId },
|
|
});
|
|
|
|
// Schritt 8: Next.js Cache invalidieren, damit gelöschter Kurs nicht mehr angezeigt wird
|
|
revalidatePath("/courses");
|
|
|
|
return {
|
|
success: true,
|
|
message: "Kurs erfolgreich gelöscht",
|
|
};
|
|
} catch (error) {
|
|
console.error("Error deleting course:", error);
|
|
return {
|
|
success: false,
|
|
error: "Fehler beim Löschen des Kurses",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Alle verwendeten Sportarten des aktuellen Managers abrufen
|
|
*/
|
|
export async function getMySportTypes() {
|
|
try {
|
|
const userData = await getUserData();
|
|
|
|
if (
|
|
!userData ||
|
|
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view))
|
|
) {
|
|
return { success: false, error: "Unauthorized", sports: [] };
|
|
}
|
|
|
|
const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
const courses = await prisma.course.findMany({
|
|
where: { createdBy: managerId },
|
|
select: { sport: true },
|
|
});
|
|
|
|
const allSports = courses.flatMap((course) => course.sport);
|
|
const uniqueSports = [...new Set(allSports)];
|
|
|
|
const formattedSports = uniqueSports.map((sport) => ({
|
|
value: sport,
|
|
label: sport.charAt(0).toUpperCase() + sport.slice(1),
|
|
}));
|
|
|
|
return { success: true, sports: formattedSports };
|
|
} catch (error) {
|
|
console.error("Error fetching sport types:", error);
|
|
return { success: false, error: "Fehler beim Laden der Sportarten", sports: [] };
|
|
}
|
|
}
|
|
|
|
export async function getMyBusinesses() {
|
|
try {
|
|
const userData = await getUserData();
|
|
|
|
if (!userData || (!isManager(userData) && !isEmployee(userData))) {
|
|
return { success: false, error: "Unauthorized", businesses: [] };
|
|
}
|
|
|
|
const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id;
|
|
|
|
const businesses = await prisma.business.findMany({
|
|
where: { managerId },
|
|
select: { id: true, name: true, slug: true, isPublic: true },
|
|
orderBy: { name: "asc" },
|
|
});
|
|
|
|
return { success: true, businesses };
|
|
} catch (error) {
|
|
console.error("Error fetching businesses:", error);
|
|
return { success: false, error: "Fehler beim Laden der Businesses", businesses: [] };
|
|
}
|
|
}
|
|
|
|
export async function toggleBusinessPublic(businessId: string, isPublic: boolean) {
|
|
try {
|
|
const userData = await getUserData();
|
|
|
|
if (!userData || !isManager(userData)) {
|
|
return { success: false, error: "Keine Berechtigung" };
|
|
}
|
|
|
|
const business = await prisma.business.findUnique({
|
|
where: { id: businessId },
|
|
select: { managerId: true, slug: true, name: true },
|
|
});
|
|
|
|
if (!business || business.managerId !== userData.id) {
|
|
return { success: false, error: "Business nicht gefunden" };
|
|
}
|
|
|
|
// Generate slug on first publish if missing
|
|
let slug = business.slug;
|
|
if (!slug) {
|
|
slug = await generateUniqueSlug(business.name, prisma);
|
|
}
|
|
|
|
await prisma.business.update({
|
|
where: { id: businessId },
|
|
data: { isPublic, slug },
|
|
});
|
|
|
|
// Auto-assign courses that belong to this manager but have no businessId yet.
|
|
// Only do this when there is exactly one business — if there are multiple,
|
|
// the manager must assign courses manually via the course edit form.
|
|
const totalBusinesses = await prisma.business.count({ where: { managerId: userData.id } });
|
|
if (totalBusinesses === 1) {
|
|
await prisma.course.updateMany({
|
|
where: { createdBy: userData.id, businessId: null },
|
|
data: { businessId },
|
|
});
|
|
}
|
|
|
|
revalidatePath("/settings");
|
|
revalidatePath(`/business/${slug}`);
|
|
|
|
return { success: true, slug };
|
|
} catch (error) {
|
|
console.error("Error toggling business public:", error);
|
|
return { success: false, error: "Fehler beim Aktualisieren" };
|
|
}
|
|
}
|