mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +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",
|
||||
datasource: {
|
||||
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"
|
||||
|
|
@ -110,3 +110,11 @@ model 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
|
||||
}
|
||||
|
|
@ -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 <CourseCreateView />;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <CourseEditView courseId={id} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 <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() {
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -9,13 +9,46 @@ const menuTitles: Record<string, string> = {
|
|||
"/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 (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const allMenuItems = [
|
|||
{
|
||||
icon: DoorOpen,
|
||||
label: "Räume",
|
||||
href: "/raume",
|
||||
href: "/rooms",
|
||||
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