diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..f9b671f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,5 @@ +{ + "permissions": { + "allow": ["Bash(npx prisma migrate:*)"] + } +} diff --git a/prisma.config.ts b/prisma.config.ts index 410cb59..ef88082 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -9,5 +9,6 @@ export default defineConfig({ engine: "classic", datasource: { url: env("DATABASE_URL"), + directUrl: env("DIRECT_URL"), }, }); diff --git a/prisma/migrations/20251217161022_init/migration.sql b/prisma/migrations/20251217161022_init/migration.sql new file mode 100644 index 0000000..43d6534 --- /dev/null +++ b/prisma/migrations/20251217161022_init/migration.sql @@ -0,0 +1,137 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "complaints" TEXT, + "birthday" TIMESTAMP(3), + "plz" TEXT, + "city" TEXT, + "street" TEXT, + "houseNumber" TEXT, + "email" TEXT NOT NULL, + "tel" TEXT, + "gender" TEXT, + "is2FA" BOOLEAN NOT NULL DEFAULT false, + "isSendingMail" BOOLEAN NOT NULL DEFAULT false, + "isNewsletter" BOOLEAN NOT NULL DEFAULT false, + "pbSrc" TEXT, + "isValid" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3), + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Employee" ( + "id" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "is2FA" BOOLEAN NOT NULL DEFAULT false, + "isSendingMail" BOOLEAN NOT NULL DEFAULT false, + "pbSrc" TEXT, + "status" TEXT, + "affiliation" TEXT, + "label" TEXT, + "qualification" TEXT, + "authorization" JSONB, + "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3), + + CONSTRAINT "Employee_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserTraining" ( + "userId" TEXT NOT NULL, + "employeeId" TEXT NOT NULL, + + CONSTRAINT "UserTraining_pkey" PRIMARY KEY ("userId","employeeId") +); + +-- CreateTable +CREATE TABLE "Manager" ( + "id" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "tel" TEXT, + "email" TEXT NOT NULL, + "is2FA" BOOLEAN NOT NULL DEFAULT false, + "isSendingMail" BOOLEAN NOT NULL DEFAULT false, + "pbSrc" TEXT, + "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3), + + CONSTRAINT "Manager_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Business" ( + "id" TEXT NOT NULL, + "managerId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "address" TEXT NOT NULL, + "email" TEXT NOT NULL, + "title" TEXT NOT NULL, + "ustId" TEXT, + "banking" TEXT, + "plan" TEXT, + "subscription" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Business_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Course" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "sport" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "time" TEXT NOT NULL, + "trainers" TEXT[], + "room" TEXT NOT NULL, + "description" TEXT NOT NULL, + "maxParticipants" INTEGER NOT NULL, + "isStandingOrder" BOOLEAN NOT NULL DEFAULT false, + "frequency" TEXT, + "weekdays" TEXT[], + "status" TEXT NOT NULL DEFAULT 'draft', + "createdBy" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Course_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Room" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "createdBy" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Room_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Employee_email_key" ON "Employee"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Manager_email_key" ON "Manager"("email"); + +-- AddForeignKey +ALTER TABLE "UserTraining" ADD CONSTRAINT "UserTraining_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserTraining" ADD CONSTRAINT "UserTraining_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Business" ADD CONSTRAINT "Business_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "Manager"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20251217164527_add_room_model/migration.sql b/prisma/migrations/20251217164527_add_room_model/migration.sql new file mode 100644 index 0000000..ec6d0e6 --- /dev/null +++ b/prisma/migrations/20251217164527_add_room_model/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `status` on the `Room` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Room" DROP COLUMN "status"; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3e6e250..065cf42 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -109,4 +109,12 @@ model Course { createdBy String // Manager ID who created the course createdAt DateTime @default(now()) updatedAt DateTime @updatedAt +} + +model Room { + id String @id @default(uuid()) + name String + createdBy String // Manager ID who created the room + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } \ No newline at end of file diff --git a/src/app/(main)/courses/create/page.tsx b/src/app/(main)/courses/create/page.tsx index f1e2ed9..c92b3f7 100644 --- a/src/app/(main)/courses/create/page.tsx +++ b/src/app/(main)/courses/create/page.tsx @@ -1,6 +1,8 @@ +import { requireManager } from "@/src/lib/auth/getUser"; import CourseCreateView from "@/src/modules/courses/ui/views/course-create-view"; -const CourseCreate = () => { +const CourseCreate = async () => { + await requireManager(); return ; }; diff --git a/src/app/(main)/courses/edit/[id]/page.tsx b/src/app/(main)/courses/edit/[id]/page.tsx index 1fcae42..e3d5e5a 100644 --- a/src/app/(main)/courses/edit/[id]/page.tsx +++ b/src/app/(main)/courses/edit/[id]/page.tsx @@ -1,6 +1,8 @@ +import { requireManager } from "@/src/lib/auth/getUser"; import CourseEditView from "@/src/modules/courses/ui/views/course-edit-view"; const CourseEdit = async ({ params }: { params: Promise<{ id: string }> }) => { + await requireManager(); const { id } = await params; return ; }; diff --git a/src/app/(main)/courses/page.tsx b/src/app/(main)/courses/page.tsx index 79b1687..1dac21a 100644 --- a/src/app/(main)/courses/page.tsx +++ b/src/app/(main)/courses/page.tsx @@ -1,6 +1,8 @@ +import { requireManager } from "@/src/lib/auth/getUser"; import CourseView from "@/src/modules/courses/ui/views/course-view"; -const Course = () => { +const Course = async () => { + await requireManager(); return ; }; diff --git a/src/app/(main)/rooms/create/page.tsx b/src/app/(main)/rooms/create/page.tsx new file mode 100644 index 0000000..8e4ad5e --- /dev/null +++ b/src/app/(main)/rooms/create/page.tsx @@ -0,0 +1,9 @@ +import { requireManager } from "@/src/lib/auth/getUser"; +import RoomsCreateView from "@/src/modules/rooms/ui/views/rooms-create-view"; + +const RoomsCreate = async () => { + await requireManager(); + return ; +}; + +export default RoomsCreate; diff --git a/src/app/(main)/rooms/edit/[id]/page.tsx b/src/app/(main)/rooms/edit/[id]/page.tsx new file mode 100644 index 0000000..622cda5 --- /dev/null +++ b/src/app/(main)/rooms/edit/[id]/page.tsx @@ -0,0 +1,10 @@ +import { requireManager } from "@/src/lib/auth/getUser"; +import RoomsEditView from "@/src/modules/rooms/ui/views/rooms-edit-view"; + +const RoomsEdit = async ({ params }: { params: Promise<{ id: string }> }) => { + await requireManager(); + const { id } = await params; + return ; +}; + +export default RoomsEdit; diff --git a/src/app/(main)/rooms/page.tsx b/src/app/(main)/rooms/page.tsx new file mode 100644 index 0000000..a744c8d --- /dev/null +++ b/src/app/(main)/rooms/page.tsx @@ -0,0 +1,9 @@ +import { requireManager } from "@/src/lib/auth/getUser"; +import RoomsView from "@/src/modules/rooms/ui/views/rooms-view"; + +const Rooms = async () => { + await requireManager(); + return ; +}; + +export default Rooms; diff --git a/src/lib/auth/getUser.ts b/src/lib/auth/getUser.ts index 1663e88..0f21b8c 100644 --- a/src/lib/auth/getUser.ts +++ b/src/lib/auth/getUser.ts @@ -147,9 +147,16 @@ export async function requireAuth() { } export async function requireAuthWithData() { + const user = await getUser(); + if (!user) { + const { redirect } = await import("next/navigation"); + redirect("/login"); + } + const userData = await getUserData(); if (!userData) { - throw new Error("User or Manager not found"); + const { redirect } = await import("next/navigation"); + redirect("/login"); } return userData; } @@ -162,6 +169,22 @@ export async function requireGuest() { } } +/** + * Requires that the authenticated user is a Manager. + * Redirects to /dashboard if user is not a manager. + * Redirects to /login if not authenticated. + */ +export async function requireManager() { + const userData = await requireAuthWithData(); + + if (!isManager(userData)) { + const { redirect } = await import("next/navigation"); + redirect("/dashboard"); + } + + return userData; +} + // Helper function to check if user is a manager export function isManager(userData: AuthUserData): userData is ManagerData { return userData.role === "manager"; diff --git a/src/modules/dashboard/ui/components/DashboardNavbar.tsx b/src/modules/dashboard/ui/components/DashboardNavbar.tsx index 3c2ecd0..67ab46d 100644 --- a/src/modules/dashboard/ui/components/DashboardNavbar.tsx +++ b/src/modules/dashboard/ui/components/DashboardNavbar.tsx @@ -9,13 +9,46 @@ const menuTitles: Record = { "/dashboard": "Dashboard", "/courses": "Kurs Übersicht", "/courses/create": "Erstelle einen Kurs", + "/courses/edit": "Bearbeite den Kurs", + "/rooms": "Räume", + "/rooms/create": "Erstelle einen Raum", + "/rooms/edit": "Bearbeite den Raum", +}; + +const dynamicRoutes: Array<{ + prefix: string; + titleKey: string; +}> = [ + { + prefix: "/courses/edit", + titleKey: "/courses/edit", + }, + { + prefix: "/rooms/edit", + titleKey: "/rooms/edit", + }, +]; + +const getPageTitle = (pathname: string) => { + if (menuTitles[pathname]) { + return menuTitles[pathname]; + } + + for (const route of dynamicRoutes) { + if (pathname.startsWith(route.prefix)) { + return menuTitles[route.titleKey]; + } + } + + // Fallback + return "Dashboard"; }; const DashboardNavbar = () => { const { state, toggleSidebar, isMobile } = useSidebar(); const pathname = usePathname(); - const pageTitle = menuTitles[pathname] || "Dashboard"; + const pageTitle = getPageTitle(pathname); return (
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}

} +
+
+ + +
+
+
+ ); +}; + +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 ( +
+

Raum wird geladen...

+
+ ); + } + + 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 ? ( +
+

Keine Räume gefunden

+
+ ) : ( +
+ {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

+
+ + +
+
+ +
+ ); +}; + +export default RoomsView;