mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
added crud feature for rooms
This commit is contained in:
parent
eab6b672bc
commit
a61ba54d08
23 changed files with 1018 additions and 5 deletions
5
.claude/settings.local.json
Normal file
5
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": ["Bash(npx prisma migrate:*)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,5 +9,6 @@ export default defineConfig({
|
||||||
engine: "classic",
|
engine: "classic",
|
||||||
datasource: {
|
datasource: {
|
||||||
url: env("DATABASE_URL"),
|
url: env("DATABASE_URL"),
|
||||||
|
directUrl: env("DIRECT_URL"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
137
prisma/migrations/20251217161022_init/migration.sql
Normal file
137
prisma/migrations/20251217161022_init/migration.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -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";
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
|
|
@ -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"
|
||||||
|
|
@ -109,4 +109,12 @@ model Course {
|
||||||
createdBy String // Manager ID who created the course
|
createdBy String // Manager ID who created the course
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { requireManager } from "@/src/lib/auth/getUser";
|
||||||
import CourseCreateView from "@/src/modules/courses/ui/views/course-create-view";
|
import CourseCreateView from "@/src/modules/courses/ui/views/course-create-view";
|
||||||
|
|
||||||
const CourseCreate = () => {
|
const CourseCreate = async () => {
|
||||||
|
await requireManager();
|
||||||
return <CourseCreateView />;
|
return <CourseCreateView />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { requireManager } from "@/src/lib/auth/getUser";
|
||||||
import CourseEditView from "@/src/modules/courses/ui/views/course-edit-view";
|
import CourseEditView from "@/src/modules/courses/ui/views/course-edit-view";
|
||||||
|
|
||||||
const CourseEdit = async ({ params }: { params: Promise<{ id: string }> }) => {
|
const CourseEdit = async ({ params }: { params: Promise<{ id: string }> }) => {
|
||||||
|
await requireManager();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
return <CourseEditView courseId={id} />;
|
return <CourseEditView courseId={id} />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { requireManager } from "@/src/lib/auth/getUser";
|
||||||
import CourseView from "@/src/modules/courses/ui/views/course-view";
|
import CourseView from "@/src/modules/courses/ui/views/course-view";
|
||||||
|
|
||||||
const Course = () => {
|
const Course = async () => {
|
||||||
|
await requireManager();
|
||||||
return <CourseView />;
|
return <CourseView />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
9
src/app/(main)/rooms/create/page.tsx
Normal file
9
src/app/(main)/rooms/create/page.tsx
Normal file
|
|
@ -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 <RoomsCreateView />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsCreate;
|
||||||
10
src/app/(main)/rooms/edit/[id]/page.tsx
Normal file
10
src/app/(main)/rooms/edit/[id]/page.tsx
Normal file
|
|
@ -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 <RoomsEditView roomId={id} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsEdit;
|
||||||
9
src/app/(main)/rooms/page.tsx
Normal file
9
src/app/(main)/rooms/page.tsx
Normal file
|
|
@ -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 <RoomsView />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Rooms;
|
||||||
|
|
@ -147,9 +147,16 @@ export async function requireAuth() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireAuthWithData() {
|
export async function requireAuthWithData() {
|
||||||
|
const user = await getUser();
|
||||||
|
if (!user) {
|
||||||
|
const { redirect } = await import("next/navigation");
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
const userData = await getUserData();
|
const userData = await getUserData();
|
||||||
if (!userData) {
|
if (!userData) {
|
||||||
throw new Error("User or Manager not found");
|
const { redirect } = await import("next/navigation");
|
||||||
|
redirect("/login");
|
||||||
}
|
}
|
||||||
return userData;
|
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
|
// Helper function to check if user is a manager
|
||||||
export function isManager(userData: AuthUserData): userData is ManagerData {
|
export function isManager(userData: AuthUserData): userData is ManagerData {
|
||||||
return userData.role === "manager";
|
return userData.role === "manager";
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,46 @@ const menuTitles: Record<string, string> = {
|
||||||
"/dashboard": "Dashboard",
|
"/dashboard": "Dashboard",
|
||||||
"/courses": "Kurs Übersicht",
|
"/courses": "Kurs Übersicht",
|
||||||
"/courses/create": "Erstelle einen Kurs",
|
"/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 DashboardNavbar = () => {
|
||||||
const { state, toggleSidebar, isMobile } = useSidebar();
|
const { state, toggleSidebar, isMobile } = useSidebar();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const pageTitle = menuTitles[pathname] || "Dashboard";
|
const pageTitle = getPageTitle(pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const allMenuItems = [
|
||||||
{
|
{
|
||||||
icon: DoorOpen,
|
icon: DoorOpen,
|
||||||
label: "Räume",
|
label: "Räume",
|
||||||
href: "/raume",
|
href: "/rooms",
|
||||||
roles: ["manager"],
|
roles: ["manager"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
374
src/modules/rooms/actions/room-actions.ts
Normal file
374
src/modules/rooms/actions/room-actions.ts
Normal file
|
|
@ -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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/modules/rooms/schemas/room-schema.ts
Normal file
7
src/modules/rooms/schemas/room-schema.ts
Normal file
|
|
@ -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<typeof roomSchema>;
|
||||||
7
src/modules/rooms/types/room.types.ts
Normal file
7
src/modules/rooms/types/room.types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export type Room = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
40
src/modules/rooms/ui/components/RoomsListItem.tsx
Normal file
40
src/modules/rooms/ui/components/RoomsListItem.tsx
Normal file
|
|
@ -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<RoomsListItemProps> = ({
|
||||||
|
roomName,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
showDeleteIcon,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-5 shadow-sm hover:shadow-md transition flex flex-col gap-4 w-full max-w-xs justify-self-center">
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-xl text-gray-900">{roomName}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{showDeleteIcon && (
|
||||||
|
<button onClick={onDelete} className="p-2 rounded-md hover:bg-red-100 transition">
|
||||||
|
<Trash2 size={20} className="text-red-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={onEdit} className="p-2 rounded-md hover:bg-gray-100 transition">
|
||||||
|
<Pen size={20} className="text-gray-700" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsListItem;
|
||||||
118
src/modules/rooms/ui/views/rooms-create-view.tsx
Normal file
118
src/modules/rooms/ui/views/rooms-create-view.tsx
Normal file
|
|
@ -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<Record<string, string>>({});
|
||||||
|
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<string, string> = {};
|
||||||
|
|
||||||
|
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<string, string> = {};
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">{mode === "edit" ? "Raum bearbeiten" : "Raum anlegen"}</h1>
|
||||||
|
<p className="text-xl mt-2">
|
||||||
|
{mode === "edit"
|
||||||
|
? "Ändern Sie den Raumnamen und speichern Sie Ihre Änderungen"
|
||||||
|
: "Alle Räume können nach dem Speichern angepasst oder gelöscht werden"}
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-col gap-4 max-w-xl">
|
||||||
|
<div>
|
||||||
|
<InputComponent
|
||||||
|
isLabel
|
||||||
|
label="Name"
|
||||||
|
type="text"
|
||||||
|
id="room-create-name"
|
||||||
|
value={roomName}
|
||||||
|
onChange={(e) => setRoomName(e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||||
|
{isSubmitting
|
||||||
|
? "Wird gespeichert..."
|
||||||
|
: mode === "edit"
|
||||||
|
? "Änderungen speichern"
|
||||||
|
: "Raum erstellen"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => router.push("/rooms")} disabled={isSubmitting}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsCreateView;
|
||||||
55
src/modules/rooms/ui/views/rooms-edit-view.tsx
Normal file
55
src/modules/rooms/ui/views/rooms-edit-view.tsx
Normal file
|
|
@ -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<Room | null>(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 (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<p className="text-xl">Raum wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roomData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RoomsCreateView mode="edit" roomId={roomId} initialData={roomData} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsEditView;
|
||||||
123
src/modules/rooms/ui/views/rooms-list-view.tsx
Normal file
123
src/modules/rooms/ui/views/rooms-list-view.tsx
Normal file
|
|
@ -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<Room[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<p className="text-xl">Räume werden geladen...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
{/* Rooms List */}
|
||||||
|
{rooms.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500 text-lg">Keine Räume gefunden</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 justify-center">
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<RoomsListItem
|
||||||
|
key={room.id}
|
||||||
|
roomName={room.name}
|
||||||
|
onEdit={() => handleEditClick(room.id)}
|
||||||
|
onDelete={() => handleDeleteClick(room.id, room.name)}
|
||||||
|
showDeleteIcon={deleteMode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<DeleteDialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDeleteDialogOpen(open);
|
||||||
|
if (!open) setRoomToDelete(null);
|
||||||
|
}}
|
||||||
|
courseName={roomToDelete?.name}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsListView;
|
||||||
37
src/modules/rooms/ui/views/rooms-view.tsx
Normal file
37
src/modules/rooms/ui/views/rooms-view.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<div className="w-full flex flex-col items-left p-2 md:flex-row md:justify-between md:items-center">
|
||||||
|
<h1 className="text-4xl font-bold">Räume</h1>
|
||||||
|
<div className="flex flex-col gap-2 mt-2 md:flex-row md:mt-0">
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push("/rooms/create")}
|
||||||
|
className="text-sm tracking-wide font-normal"
|
||||||
|
>
|
||||||
|
Raum anlegen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={deleteMode ? "destructive" : "outline"}
|
||||||
|
onClick={() => setDeleteMode(!deleteMode)}
|
||||||
|
className="text-sm tracking-wide font-normal"
|
||||||
|
>
|
||||||
|
{deleteMode ? "Abbrechen" : "Raum löschen"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<RoomsListView deleteMode={deleteMode} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsView;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue