diff --git a/src/modules/dashboard/ui/components/DashboardSidebar.tsx b/src/modules/dashboard/ui/components/DashboardSidebar.tsx
index 15773cd..5ca7ab0 100644
--- a/src/modules/dashboard/ui/components/DashboardSidebar.tsx
+++ b/src/modules/dashboard/ui/components/DashboardSidebar.tsx
@@ -69,7 +69,7 @@ const allMenuItems = [
{
icon: DoorOpen,
label: "Räume",
- href: "/raume",
+ href: "/rooms",
roles: ["manager"],
},
{
diff --git a/src/modules/rooms/actions/room-actions.ts b/src/modules/rooms/actions/room-actions.ts
new file mode 100644
index 0000000..7dcfc51
--- /dev/null
+++ b/src/modules/rooms/actions/room-actions.ts
@@ -0,0 +1,374 @@
+/**
+ * Room Actions - Server-seitige Aktionen für Raumverwaltung
+ *
+ * Diese Datei enthält alle Server Actions für das Raum-Management-System.
+ * Funktionen umfassen: Räume 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 } from "@/src/lib/auth/getUser";
+import { roomSchema, type RoomFormData } from "../schemas/room-schema";
+import { revalidatePath } from "next/cache";
+
+/**
+ * Erstellt einen neuen Raum in der Datenbank
+ *
+ * Diese Funktion validiert die Eingabedaten, überprüft die Berechtigungen des Benutzers
+ * und erstellt einen neuen Raum. Nur Manager können Räume erstellen.
+ *
+ * @param data - Die Raumdaten aus dem Formular
+ * @returns Ein Objekt mit success-Flag, optionaler roomId und Nachricht oder Fehler
+ */
+export async function createRoom(data: RoomFormData) {
+ 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 Raum zu erstellen",
+ };
+ }
+
+ // Schritt 3: Überprüfen, ob Benutzer ein Manager ist (nur Manager dürfen Räume erstellen)
+ if (!isManager(userData)) {
+ return {
+ success: false,
+ error: "Nur Manager können Räume erstellen",
+ };
+ }
+
+ // Schritt 4: Formulardaten mit Zod-Schema validieren
+ const validation = roomSchema.safeParse(data);
+
+ if (!validation.success) {
+ return {
+ success: false,
+ error: "Validierungsfehler",
+ fieldErrors: validation.error.flatten().fieldErrors,
+ };
+ }
+
+ const validatedData = validation.data;
+
+ // Schritt 5: Raum in der Datenbank erstellen
+ const room = await prisma.room.create({
+ data: {
+ name: validatedData.name,
+ createdBy: userData.id, // Manager-ID als Ersteller
+ },
+ });
+
+ // Schritt 6: Next.js Cache für /rooms Seite invalidieren, damit neue Daten angezeigt werden
+ revalidatePath("/rooms");
+
+ return {
+ success: true,
+ roomId: room.id,
+ message: "Raum erfolgreich erstellt",
+ };
+ } catch (error) {
+ console.error("Error creating room:", error);
+ return {
+ success: false,
+ error: "Ein Fehler ist beim Erstellen des Raumes aufgetreten",
+ };
+ }
+}
+
+/**
+ * Ruft alle Räume ab
+ *
+ * Diese Funktion gibt alle Räume zurück.
+ * Die Räume werden nach Namen aufsteigend sortiert.
+ *
+ * @returns Ein Objekt mit success-Flag und einem Array von Räumen
+ */
+export async function getAllRooms() {
+ try {
+ // Alle Räume aus der Datenbank abrufen
+ // Sortiert nach Namen aufsteigend
+ const rooms = await prisma.room.findMany({
+ orderBy: {
+ name: "asc", // Aufsteigende Sortierung nach Name
+ },
+ });
+
+ return {
+ success: true,
+ rooms,
+ };
+ } catch (error) {
+ console.error("Error fetching rooms:", error);
+ return {
+ success: false,
+ error: "Fehler beim Laden der Räume",
+ rooms: [],
+ };
+ }
+}
+
+/**
+ * Ruft alle Räume ab, die vom eingeloggten Manager erstellt wurden
+ *
+ * Diese Funktion gibt alle Räume zurück, die vom aktuell eingeloggten Manager erstellt wurden.
+ *
+ * @returns Ein Objekt mit success-Flag und einem Array der Räume des Managers
+ */
+export async function getMyRooms() {
+ try {
+ // Schritt 1: Aktuellen Benutzer abrufen
+ const userData = await getUserData();
+
+ // Schritt 2: Überprüfen, ob Benutzer angemeldet ist und ein Manager ist
+ if (!userData || !isManager(userData)) {
+ return {
+ success: false,
+ error: "Unauthorized",
+ rooms: [],
+ };
+ }
+
+ // Schritt 3: Räume abrufen, die von diesem Manager erstellt wurden
+ const rooms = await prisma.room.findMany({
+ where: {
+ createdBy: userData.id, // Filtere nach Manager-ID
+ },
+ orderBy: {
+ name: "asc", // Sortiere nach Name aufsteigend
+ },
+ });
+
+ return {
+ success: true,
+ rooms,
+ };
+ } catch (error) {
+ console.error("Error fetching my rooms:", error);
+ return {
+ success: false,
+ error: "Fehler beim Laden der Räume",
+ rooms: [],
+ };
+ }
+}
+
+/**
+ * Ruft einen einzelnen Raum nach ID ab
+ *
+ * Diese Funktion gibt einen Raum zurück, wenn der eingeloggte Manager
+ * der Ersteller des Raumes ist.
+ *
+ * @param roomId - Die eindeutige ID des Raumes
+ * @returns Ein Objekt mit success-Flag und dem Raum oder Fehler
+ */
+export async function getRoomById(roomId: 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 Räume bearbeiten",
+ };
+ }
+
+ // Schritt 3: Raum aus der Datenbank abrufen
+ const room = await prisma.room.findUnique({
+ where: { id: roomId },
+ });
+
+ // Schritt 4: Überprüfen, ob Raum existiert
+ if (!room) {
+ return {
+ success: false,
+ error: "Raum nicht gefunden",
+ };
+ }
+
+ // Schritt 5: Überprüfen, ob der Manager der Besitzer des Raumes ist
+ if (room.createdBy !== userData.id) {
+ return {
+ success: false,
+ error: "Sie können nur Ihre eigenen Räume bearbeiten",
+ };
+ }
+
+ return {
+ success: true,
+ room,
+ };
+ } catch (error) {
+ console.error("Error fetching room:", error);
+ return {
+ success: false,
+ error: "Fehler beim Laden des Raumes",
+ };
+ }
+}
+
+/**
+ * Aktualisiert einen bestehenden Raum
+ *
+ * Diese Funktion validiert die Eingabedaten, überprüft die Berechtigungen des Benutzers
+ * und aktualisiert einen bestehenden Raum. Nur der Manager, der den Raum erstellt hat,
+ * kann ihn bearbeiten.
+ *
+ * @param roomId - Die eindeutige ID des zu aktualisierenden Raumes
+ * @param data - Die aktualisierten Raumdaten aus dem Formular
+ * @param status - Der Status des Raumes ("draft" für Entwurf oder "published" für veröffentlicht)
+ * @returns Ein Objekt mit success-Flag und Nachricht oder Fehler
+ */
+export async function updateRoom(roomId: string, data: RoomFormData) {
+ 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 Raum zu bearbeiten",
+ };
+ }
+
+ // Schritt 3: Überprüfen, ob Benutzer ein Manager ist
+ if (!isManager(userData)) {
+ return {
+ success: false,
+ error: "Nur Manager können Räume bearbeiten",
+ };
+ }
+
+ // Schritt 4: Raum aus der Datenbank abrufen
+ const existingRoom = await prisma.room.findUnique({
+ where: { id: roomId },
+ });
+
+ // Schritt 5: Überprüfen, ob Raum existiert
+ if (!existingRoom) {
+ return {
+ success: false,
+ error: "Raum nicht gefunden",
+ };
+ }
+
+ // Schritt 6: Überprüfen, ob der Manager der Besitzer des Raumes ist
+ if (existingRoom.createdBy !== userData.id) {
+ return {
+ success: false,
+ error: "Sie können nur Ihre eigenen Räume bearbeiten",
+ };
+ }
+
+ // Schritt 7: Formulardaten mit Zod-Schema validieren
+ const validation = roomSchema.safeParse(data);
+
+ if (!validation.success) {
+ return {
+ success: false,
+ error: "Validierungsfehler",
+ fieldErrors: validation.error.flatten().fieldErrors,
+ };
+ }
+
+ const validatedData = validation.data;
+
+ // Schritt 8: Raum in der Datenbank aktualisieren
+ await prisma.room.update({
+ where: { id: roomId },
+ data: {
+ name: validatedData.name,
+ },
+ });
+
+ // Schritt 9: Next.js Cache für /rooms Seite invalidieren
+ revalidatePath("/rooms");
+ revalidatePath(`/rooms/edit/${roomId}`);
+
+ return {
+ success: true,
+ message: "Raum erfolgreich aktualisiert",
+ };
+ } catch (error) {
+ console.error("Error updating room:", error);
+ return {
+ success: false,
+ error: "Ein Fehler ist beim Aktualisieren des Raumes aufgetreten",
+ };
+ }
+}
+
+/**
+ * Löscht einen Raum aus der Datenbank
+ *
+ * Diese Funktion löscht einen Raum, überprüft aber vorher:
+ * - Ob der Benutzer ein Manager ist
+ * - Ob der Raum existiert
+ * - Ob der Manager der Besitzer des Raumes ist (Manager können nur eigene Räume löschen)
+ *
+ * @param roomId - Die eindeutige ID des zu löschenden Raumes
+ * @returns Ein Objekt mit success-Flag und Nachricht oder Fehler
+ */
+export async function deleteRoom(roomId: 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 Räume löschen",
+ };
+ }
+
+ // Schritt 3: Raum aus der Datenbank abrufen
+ const room = await prisma.room.findUnique({
+ where: { id: roomId },
+ });
+
+ // Schritt 4: Überprüfen, ob Raum existiert
+ if (!room) {
+ return {
+ success: false,
+ error: "Raum nicht gefunden",
+ };
+ }
+
+ // Schritt 5: Überprüfen, ob der Manager der Besitzer des Raumes ist
+ // (Sicherheit: Manager können nur ihre eigenen Räume löschen)
+ if (room.createdBy !== userData.id) {
+ return {
+ success: false,
+ error: "Sie können nur Ihre eigenen Räume löschen",
+ };
+ }
+
+ // Schritt 6: Raum aus der Datenbank löschen
+ await prisma.room.delete({
+ where: { id: roomId },
+ });
+
+ // Schritt 7: Next.js Cache invalidieren, damit gelöschter Raum nicht mehr angezeigt wird
+ revalidatePath("/rooms");
+
+ return {
+ success: true,
+ message: "Raum erfolgreich gelöscht",
+ };
+ } catch (error) {
+ console.error("Error deleting room:", error);
+ return {
+ success: false,
+ error: "Fehler beim Löschen des Raumes",
+ };
+ }
+}
diff --git a/src/modules/rooms/schemas/room-schema.ts b/src/modules/rooms/schemas/room-schema.ts
new file mode 100644
index 0000000..5fe749e
--- /dev/null
+++ b/src/modules/rooms/schemas/room-schema.ts
@@ -0,0 +1,7 @@
+import { z } from "zod";
+
+export const roomSchema = z.object({
+ name: z.string().min(1, "Name ist erforderlich"),
+});
+
+export type RoomFormData = z.infer
;
diff --git a/src/modules/rooms/types/room.types.ts b/src/modules/rooms/types/room.types.ts
new file mode 100644
index 0000000..d099287
--- /dev/null
+++ b/src/modules/rooms/types/room.types.ts
@@ -0,0 +1,7 @@
+export type Room = {
+ id: string;
+ name: string;
+ createdBy: string;
+ createdAt: Date;
+ updatedAt: Date;
+};
diff --git a/src/modules/rooms/ui/components/RoomsListItem.tsx b/src/modules/rooms/ui/components/RoomsListItem.tsx
new file mode 100644
index 0000000..2d5618f
--- /dev/null
+++ b/src/modules/rooms/ui/components/RoomsListItem.tsx
@@ -0,0 +1,40 @@
+import { Pen, Trash2 } from "lucide-react";
+
+/**
+ * Props für die RoomListItem Komponente
+ */
+interface RoomsListItemProps {
+ roomName: string; // Name des Raumes
+ onEdit?: () => void; // Callback-Funktion beim Klick auf Bearbeiten-Button
+ onDelete?: () => void; // Callback-Funktion beim Klick auf Löschen-Button
+ showDeleteIcon?: boolean; // Ob das Löschen-Icon angezeigt werden soll
+}
+
+const RoomsListItem: React.FC = ({
+ roomName,
+ onEdit,
+ onDelete,
+ showDeleteIcon,
+}) => {
+ return (
+
+
+
+
{roomName}
+
+
+ {showDeleteIcon && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default RoomsListItem;
diff --git a/src/modules/rooms/ui/views/rooms-create-view.tsx b/src/modules/rooms/ui/views/rooms-create-view.tsx
new file mode 100644
index 0000000..44b62ff
--- /dev/null
+++ b/src/modules/rooms/ui/views/rooms-create-view.tsx
@@ -0,0 +1,118 @@
+"use client";
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import InputComponent from "@/src/components/layout/InputComponent";
+import { Button } from "@/src/components/ui/button";
+import { createRoom, updateRoom } from "../../actions/room-actions";
+import { toast } from "sonner";
+import { Room } from "../../types/room.types";
+
+interface RoomsCreateViewProps {
+ mode?: "create" | "edit";
+ roomId?: string;
+ initialData?: Room;
+}
+
+const RoomsCreateView = ({ mode = "create", roomId, initialData }: RoomsCreateViewProps = {}) => {
+ const router = useRouter();
+ const [roomName, setRoomName] = useState("");
+ const [errors, setErrors] = useState>({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Load initial data when in edit mode
+ useEffect(() => {
+ if (mode === "edit" && initialData) {
+ setRoomName(initialData.name || "");
+ }
+ }, [mode, initialData]);
+
+ const validateForm = () => {
+ const newErrors: Record = {};
+
+ if (!roomName.trim()) newErrors.name = "Raumname ist erforderlich";
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) {
+ toast.error("Bitte füllen Sie alle erforderlichen Felder aus");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const roomData = {
+ name: roomName,
+ };
+
+ if (mode === "edit" && !roomId) {
+ throw new Error("Missing roomId in edit mode");
+ }
+
+ const result =
+ mode === "edit" ? await updateRoom(roomId!, roomData) : await createRoom(roomData);
+
+ if (result.success) {
+ toast.success(result.message);
+ router.push("/rooms");
+ } else {
+ toast.error(result.error || "Ein Fehler ist aufgetreten");
+ if (result.fieldErrors) {
+ const fieldErrors: Record = {};
+ Object.entries(result.fieldErrors).forEach(([key, value]) => {
+ if (Array.isArray(value) && value.length > 0) {
+ fieldErrors[key] = value[0];
+ }
+ });
+ setErrors(fieldErrors);
+ }
+ }
+ } catch (error) {
+ toast.error("Ein unerwarteter Fehler ist aufgetreten");
+ console.error(error);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
{mode === "edit" ? "Raum bearbeiten" : "Raum anlegen"}
+
+ {mode === "edit"
+ ? "Ändern Sie den Raumnamen und speichern Sie Ihre Änderungen"
+ : "Alle Räume können nach dem Speichern angepasst oder gelöscht werden"}
+
+
+
+
setRoomName(e.target.value)}
+ />
+ {errors.name && {errors.name}
}
+
+
+
+ {isSubmitting
+ ? "Wird gespeichert..."
+ : mode === "edit"
+ ? "Änderungen speichern"
+ : "Raum erstellen"}
+
+ router.push("/rooms")} disabled={isSubmitting}>
+ Abbrechen
+
+
+
+
+ );
+};
+
+export default RoomsCreateView;
diff --git a/src/modules/rooms/ui/views/rooms-edit-view.tsx b/src/modules/rooms/ui/views/rooms-edit-view.tsx
new file mode 100644
index 0000000..23e037f
--- /dev/null
+++ b/src/modules/rooms/ui/views/rooms-edit-view.tsx
@@ -0,0 +1,55 @@
+"use client";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { getRoomById } from "../../actions/room-actions";
+import { toast } from "sonner";
+import RoomsCreateView from "./rooms-create-view";
+import { Room } from "../../types/room.types";
+
+interface RoomsEditViewProps {
+ roomId: string;
+}
+
+const RoomsEditView = ({ roomId }: RoomsEditViewProps) => {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(true);
+ const [roomData, setRoomData] = useState(null);
+
+ useEffect(() => {
+ let alive = true;
+
+ const fetchRoom = async () => {
+ if (alive) setIsLoading(true);
+ const result = await getRoomById(roomId);
+
+ if (result.success && result.room) {
+ if (alive) setRoomData(result.room);
+ } else {
+ toast.error(result.error || "Fehler beim Laden des Raumes");
+ router.push("/rooms");
+ }
+ if (alive) setIsLoading(false);
+ };
+
+ fetchRoom();
+ return () => {
+ alive = false;
+ };
+ }, [roomId, router]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!roomData) {
+ return null;
+ }
+
+ return ;
+};
+
+export default RoomsEditView;
diff --git a/src/modules/rooms/ui/views/rooms-list-view.tsx b/src/modules/rooms/ui/views/rooms-list-view.tsx
new file mode 100644
index 0000000..bf8de20
--- /dev/null
+++ b/src/modules/rooms/ui/views/rooms-list-view.tsx
@@ -0,0 +1,123 @@
+"use client";
+import { useEffect, useState } from "react";
+import { getMyRooms, deleteRoom } from "../../actions/room-actions";
+import { toast } from "sonner";
+import { useRouter } from "next/navigation";
+import RoomsListItem from "../components/RoomsListItem";
+import DeleteDialog from "@/src/components/layout/DeleteDialog";
+
+type Room = {
+ id: string;
+ name: string;
+ createdBy: string;
+ createdAt: Date;
+ updatedAt: Date;
+};
+
+interface RoomsListViewProps {
+ deleteMode: boolean;
+}
+
+const RoomsListView = ({ deleteMode }: RoomsListViewProps) => {
+ const router = useRouter();
+ const [rooms, setRooms] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [roomToDelete, setRoomToDelete] = useState<{ id: string; name: string } | null>(null);
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ // Load rooms
+ const loadRooms = async () => {
+ const result = await getMyRooms();
+ if (result.success) {
+ setRooms(result.rooms);
+ } else {
+ toast.error(result.error || "Fehler beim Laden der Räume");
+ }
+ };
+
+ // Initial load
+ useEffect(() => {
+ const fetchRooms = async () => {
+ setIsLoading(true);
+ await loadRooms();
+ setIsLoading(false);
+ };
+ fetchRooms();
+ }, []);
+
+ // Handle delete click
+ const handleDeleteClick = (id: string, name: string) => {
+ setRoomToDelete({ id, name });
+ setDeleteDialogOpen(true);
+ };
+
+ // Confirm delete
+ const handleDeleteConfirm = async () => {
+ if (!roomToDelete) return;
+
+ setIsDeleting(true);
+ const result = await deleteRoom(roomToDelete.id);
+
+ if (result.success) {
+ toast.success(result.message);
+ // Remove the deleted room from the list
+ setRooms((prev) => prev.filter((room) => room.id !== roomToDelete.id));
+ setDeleteDialogOpen(false);
+ setRoomToDelete(null);
+ } else {
+ toast.error(result.error || "Fehler beim Löschen des Raumes");
+ }
+ setIsDeleting(false);
+ };
+
+ // Handle edit click
+ const handleEditClick = (id: string) => {
+ router.push(`/rooms/edit/${id}`);
+ };
+
+ if (isLoading) {
+ return (
+
+
Räume werden geladen...
+
+ );
+ }
+
+ return (
+
+ {/* Rooms List */}
+ {rooms.length === 0 ? (
+
+ ) : (
+
+ {rooms.map((room) => (
+ handleEditClick(room.id)}
+ onDelete={() => handleDeleteClick(room.id, room.name)}
+ showDeleteIcon={deleteMode}
+ />
+ ))}
+
+ )}
+
+ {/* Delete Confirmation Dialog */}
+
{
+ setDeleteDialogOpen(open);
+ if (!open) setRoomToDelete(null);
+ }}
+ courseName={roomToDelete?.name}
+ onConfirm={handleDeleteConfirm}
+ isLoading={isDeleting}
+ />
+
+ );
+};
+
+export default RoomsListView;
diff --git a/src/modules/rooms/ui/views/rooms-view.tsx b/src/modules/rooms/ui/views/rooms-view.tsx
new file mode 100644
index 0000000..2e353a3
--- /dev/null
+++ b/src/modules/rooms/ui/views/rooms-view.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/src/components/ui/button";
+import { useRouter } from "next/navigation";
+import RoomsListView from "./rooms-list-view";
+
+const RoomsView = () => {
+ const router = useRouter();
+ const [deleteMode, setDeleteMode] = useState(false);
+
+ return (
+
+
+
Räume
+
+ router.push("/rooms/create")}
+ className="text-sm tracking-wide font-normal"
+ >
+ Raum anlegen
+
+ setDeleteMode(!deleteMode)}
+ className="text-sm tracking-wide font-normal"
+ >
+ {deleteMode ? "Abbrechen" : "Raum löschen"}
+
+
+
+
+
+ );
+};
+
+export default RoomsView;