added attributtion access management for courses, employees and rooms for create, view, update and delete

This commit is contained in:
Rajpreet0 2026-03-24 15:47:42 +01:00
parent df09e04d4b
commit 11f5be2d72
24 changed files with 652 additions and 302 deletions

View file

@ -3,7 +3,7 @@ import CourseCreateView from "@/src/modules/courses/ui/views/course-create-view"
import { getMySportTypes } from "@/src/modules/courses/actions/course-actions";
const CourseCreate = async () => {
await requireManagerOrPermission("canCreateCourses");
await requireManagerOrPermission((p) => p.courses.create);
const sportsResult = await getMySportTypes();
const customSports = sportsResult.success ? sportsResult.sports : [];

View file

@ -3,7 +3,7 @@ import CourseEditView from "@/src/modules/courses/ui/views/course-edit-view";
import { getMySportTypes } from "@/src/modules/courses/actions/course-actions";
const CourseEdit = async ({ params }: { params: Promise<{ id: string }> }) => {
await requireManagerOrPermission("canCreateCourses");
await requireManagerOrPermission((p) => p.courses.edit);
const { id } = await params;
const sportsResult = await getMySportTypes();

View file

@ -1,9 +1,15 @@
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
import { requireManagerOrPermission, isManager, isEmployee } from "@/src/lib/auth/getUser";
import CourseView from "@/src/modules/courses/ui/views/course-view";
const Course = async () => {
await requireManagerOrPermission("canCreateCourses");
return <CourseView />;
const userData = await requireManagerOrPermission((p) => p.courses.view);
const canCreate =
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.create);
const canEdit =
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.edit);
const canDelete =
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.delete);
return <CourseView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />;
};
export default Course;

View file

@ -3,7 +3,7 @@ import EmployeeCreateView from "@/src/modules/employee/ui/views/employee-create-
import { getMyEmployeeRoles } from "@/src/modules/employee/actions/employee-actions";
const EmployeeCreate = async () => {
await requireManagerOrPermission("canCreateEmployees");
await requireManagerOrPermission((p) => p.employees.create);
// Lade alle bereits verwendeten custom Rollen
const rolesResult = await getMyEmployeeRoles();

View file

@ -1,9 +1,15 @@
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
import { requireManagerOrPermission, isManager, isEmployee } from "@/src/lib/auth/getUser";
import EmployeeView from "@/src/modules/employee/ui/views/employee-view";
const Employee = async () => {
await requireManagerOrPermission("canCreateEmployees");
return <EmployeeView />;
const userData = await requireManagerOrPermission((p) => p.employees.view);
const canCreate =
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.create);
const canEdit =
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.edit);
const canDelete =
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.delete);
return <EmployeeView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />;
};
export default Employee;

View file

@ -1,8 +1,8 @@
import { requireManager } from "@/src/lib/auth/getUser";
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
import RoomsCreateView from "@/src/modules/rooms/ui/views/rooms-create-view";
const RoomsCreate = async () => {
await requireManager();
await requireManagerOrPermission((p) => p.rooms.create);
return <RoomsCreateView />;
};

View file

@ -1,8 +1,8 @@
import { requireManager } from "@/src/lib/auth/getUser";
import { requireManagerOrPermission } 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();
await requireManagerOrPermission((p) => p.rooms.edit);
const { id } = await params;
return <RoomsEditView roomId={id} />;
};

View file

@ -1,9 +1,14 @@
import { requireManager } from "@/src/lib/auth/getUser";
import { requireManagerOrPermission, isManager, isEmployee } from "@/src/lib/auth/getUser";
import RoomsView from "@/src/modules/rooms/ui/views/rooms-view";
const Rooms = async () => {
await requireManager();
return <RoomsView />;
const userData = await requireManagerOrPermission((p) => p.rooms.view);
const canCreate =
isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.create);
const canEdit = isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.edit);
const canDelete =
isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.delete);
return <RoomsView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />;
};
export default Rooms;

View file

@ -45,8 +45,9 @@ export type EmployeeData = {
createdAt: Date | null;
role: "employee";
permissions: {
canCreateCourses: boolean;
canCreateEmployees: boolean;
employees: { view: boolean; create: boolean; edit: boolean; delete: boolean };
courses: { view: boolean; create: boolean; edit: boolean; delete: boolean };
rooms: { view: boolean; create: boolean; edit: boolean; delete: boolean };
};
};
@ -177,18 +178,61 @@ export const getUserData = cache(async (): Promise<AuthUserData | null> => {
"✅ [DB] Employee data fetched:",
`${employeeData.firstName} ${employeeData.lastName}`
);
const rawPermissions = employeeData.permissions as {
canCreateCourses?: boolean;
canCreateEmployees?: boolean;
} | null;
const raw = employeeData.permissions as Record<string, unknown> | null;
const empty = () => ({ view: false, create: false, edit: false, delete: false });
let permissions: EmployeeData["permissions"];
if (raw && ("canCreateCourses" in raw || "canCreateEmployees" in raw)) {
// Backward-Kompatibilität: altes Format migrieren
const old = raw as { canCreateCourses?: boolean; canCreateEmployees?: boolean };
permissions = {
employees: {
view: old.canCreateEmployees ?? false,
create: old.canCreateEmployees ?? false,
edit: false,
delete: false,
},
courses: {
view: old.canCreateCourses ?? false,
create: old.canCreateCourses ?? false,
edit: false,
delete: false,
},
rooms: empty(),
};
} else {
const p = raw as {
employees?: Partial<EmployeeData["permissions"]["employees"]>;
courses?: Partial<EmployeeData["permissions"]["courses"]>;
rooms?: Partial<EmployeeData["permissions"]["rooms"]>;
} | null;
permissions = {
employees: {
view: p?.employees?.view ?? false,
create: p?.employees?.create ?? false,
edit: p?.employees?.edit ?? false,
delete: p?.employees?.delete ?? false,
},
courses: {
view: p?.courses?.view ?? false,
create: p?.courses?.create ?? false,
edit: p?.courses?.edit ?? false,
delete: p?.courses?.delete ?? false,
},
rooms: {
view: p?.rooms?.view ?? false,
create: p?.rooms?.create ?? false,
edit: p?.rooms?.edit ?? false,
delete: p?.rooms?.delete ?? false,
},
};
}
const employeeWithRole: EmployeeData = {
...employeeData,
role: "employee",
permissions: {
canCreateCourses: rawPermissions?.canCreateCourses ?? false,
canCreateEmployees: rawPermissions?.canCreateEmployees ?? false,
},
permissions,
};
return employeeWithRole;
}
@ -260,12 +304,14 @@ export function isEmployee(userData: AuthUserData): userData is EmployeeData {
* Requires that the authenticated user is a Manager OR an Employee with the given permission.
* Redirects to /dashboard if access is denied.
*/
export async function requireManagerOrPermission(permission: keyof EmployeeData["permissions"]) {
export async function requireManagerOrPermission(
check: (permissions: EmployeeData["permissions"]) => boolean
) {
const userData = await requireAuthWithData();
if (isManager(userData)) return userData;
if (isEmployee(userData) && userData.permissions[permission]) return userData;
if (isEmployee(userData) && check(userData.permissions)) return userData;
const { redirect } = await import("next/navigation");
redirect("/dashboard");

View file

@ -9,7 +9,7 @@
"use server";
import { prisma } from "@/src/lib/prisma";
import { getUserData, isManager } from "@/src/lib/auth/getUser";
import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser";
import { courseSchema, type CourseFormData } from "../schemas/course-schema";
import { revalidatePath } from "next/cache";
import { Prisma } from "@prisma/client";
@ -37,11 +37,13 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist (nur Manager dürfen Kurse erstellen)
if (!isManager(userData)) {
// Schritt 3: Manager oder Employee mit Berechtigung
const canCreate =
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.create);
if (!canCreate) {
return {
success: false,
error: "Nur Manager können Kurse erstellen",
error: "Keine Berechtigung zum Erstellen von Kursen",
};
}
@ -58,13 +60,20 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ
const validatedData = validation.data;
// Schritt 5: Kurs in der Datenbank erstellen
// Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
let creatorId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
creatorId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 6: Kurs in der Datenbank erstellen
const course = await prisma.course.create({
data: {
name: validatedData.name,
sport: validatedData.sport,
level: validatedData.level || "any",
date: new Date(validatedData.date), // String zu Date konvertieren
date: new Date(validatedData.date),
timeFrom: validatedData.timeFrom,
timeTo: validatedData.timeTo,
trainers: validatedData.trainers,
@ -75,8 +84,8 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ
isStandingOrder: validatedData.isStandingOrder,
frequency: validatedData.frequency || null,
weekdays: validatedData.weekdays || [],
status, // "draft" oder "published"
createdBy: userData.id, // Manager-ID als Ersteller
status,
createdBy: creatorId,
},
});
@ -152,8 +161,11 @@ export async function getMyCourses(options?: { dateFrom?: Date; dateTo?: Date })
// Step 1: Get current user
const userData = await getUserData();
// Step 2: Check if user is logged in and is a manager
if (!userData || !isManager(userData)) {
// Step 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view))
) {
return {
success: false,
error: "Unauthorized",
@ -161,9 +173,16 @@ export async function getMyCourses(options?: { dateFrom?: Date; dateTo?: Date })
};
}
// Step 3: Build the where clause with optional date filtering
// Step 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Step 4: Build the where clause with optional date filtering
const whereClause: Prisma.CourseWhereInput = {
createdBy: userData.id, // Filter by manager ID
createdBy: managerId,
};
// Add date range filter if provided
@ -222,20 +241,30 @@ export async function getCourseById(courseId: string) {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
if (!userData || !isManager(userData)) {
// Schritt 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view))
) {
return {
success: false,
error: "Nur Manager können Kurse bearbeiten",
};
}
// Schritt 3: Kurs aus der Datenbank abrufen
// Schritt 3: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 4: Kurs aus der Datenbank abrufen
const course = await prisma.course.findUnique({
where: { id: courseId },
});
// Schritt 4: Überprüfen, ob Kurs existiert
// Schritt 5: Überprüfen, ob Kurs existiert
if (!course) {
return {
success: false,
@ -243,8 +272,8 @@ export async function getCourseById(courseId: string) {
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (course.createdBy !== userData.id) {
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (course.createdBy !== managerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Kurse bearbeiten",
@ -293,20 +322,29 @@ export async function updateCourse(
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist
if (!isManager(userData)) {
// Schritt 3: Manager oder Employee mit Berechtigung
const canEdit =
isManager(userData) || (isEmployee(userData) && userData.permissions.courses.edit);
if (!canEdit) {
return {
success: false,
error: "Nur Manager können Kurse bearbeiten",
error: "Keine Berechtigung zum Bearbeiten von Kursen",
};
}
// Schritt 4: Kurs aus der Datenbank abrufen
// Schritt 4: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 5: Kurs aus der Datenbank abrufen
const existingCourse = await prisma.course.findUnique({
where: { id: courseId },
});
// Schritt 5: Überprüfen, ob Kurs existiert
// Schritt 6: Überprüfen, ob Kurs existiert
if (!existingCourse) {
return {
success: false,
@ -314,15 +352,15 @@ export async function updateCourse(
};
}
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (existingCourse.createdBy !== userData.id) {
// Schritt 7: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (existingCourse.createdBy !== managerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Kurse bearbeiten",
};
}
// Schritt 7: Formulardaten mit Zod-Schema validieren
// Schritt 8: Formulardaten mit Zod-Schema validieren
const validation = courseSchema.safeParse(data);
if (!validation.success) {
@ -393,20 +431,30 @@ export async function deleteCourse(courseId: string) {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
if (!userData || !isManager(userData)) {
// Schritt 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.delete))
) {
return {
success: false,
error: "Nur Manager können Kurse löschen",
error: "Keine Berechtigung zum Löschen von Kursen",
};
}
// Schritt 3: Kurs aus der Datenbank abrufen
// Schritt 3: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 4: Kurs aus der Datenbank abrufen
const course = await prisma.course.findUnique({
where: { id: courseId },
});
// Schritt 4: Überprüfen, ob Kurs existiert
// Schritt 5: Überprüfen, ob Kurs existiert
if (!course) {
return {
success: false,
@ -414,21 +462,20 @@ export async function deleteCourse(courseId: string) {
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Kurses ist
// (Sicherheit: Manager können nur ihre eigenen Kurse löschen)
if (course.createdBy !== userData.id) {
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Kurses ist
if (course.createdBy !== managerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Kurse löschen",
};
}
// Schritt 6: Kurs aus der Datenbank löschen
// Schritt 7: Kurs aus der Datenbank löschen
await prisma.course.delete({
where: { id: courseId },
});
// Schritt 7: Next.js Cache invalidieren, damit gelöschter Kurs nicht mehr angezeigt wird
// Schritt 8: Next.js Cache invalidieren, damit gelöschter Kurs nicht mehr angezeigt wird
revalidatePath("/courses");
return {
@ -451,12 +498,21 @@ export async function getMySportTypes() {
try {
const userData = await getUserData();
if (!userData || !isManager(userData)) {
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.courses.view))
) {
return { success: false, error: "Unauthorized", sports: [] };
}
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
const courses = await prisma.course.findMany({
where: { createdBy: userData.id },
where: { createdBy: managerId },
select: { sport: true },
});

View file

@ -36,7 +36,13 @@ type Course = {
};
};
const CourseListView = () => {
interface CourseListViewProps {
canCreate: boolean;
canEdit: boolean;
canDelete: boolean;
}
const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [courses, setCourses] = useState<Course[]>([]);
@ -378,16 +384,20 @@ const CourseListView = () => {
<div className="flex justify-between items-center mb-6">
<div className="flex flex-col w-full gap-2 mr-2 sm:flex-row sm:justify-end">
<div className="flex gap-2 sm:hidden">
<Button onClick={() => router.push("/courses/create")} className="flex-1">
Kurs anlegen
</Button>
<Button
variant={deleteMode ? "destructive" : "outline"}
onClick={() => setDeleteMode(!deleteMode)}
className="flex-1"
>
{deleteMode ? "Abbrechen" : "Kurs löschen"}
</Button>
{canCreate && (
<Button onClick={() => router.push("/courses/create")} className="flex-1">
Kurs anlegen
</Button>
)}
{canDelete && (
<Button
variant={deleteMode ? "destructive" : "outline"}
onClick={() => setDeleteMode(!deleteMode)}
className="flex-1"
>
{deleteMode ? "Abbrechen" : "Kurs löschen"}
</Button>
)}
</div>
{/* Mobile: Filter button below the two action buttons */}
@ -503,13 +513,15 @@ const CourseListView = () => {
)}
</Button>
</CourseFilter>
<Button
variant={deleteMode ? "destructive" : "outline"}
onClick={() => setDeleteMode(!deleteMode)}
className="hidden sm:flex items-center gap-2"
>
{deleteMode ? "Abbrechen" : "Kurs löschen"}
</Button>
{canDelete && (
<Button
variant={deleteMode ? "destructive" : "outline"}
onClick={() => setDeleteMode(!deleteMode)}
className="hidden sm:flex items-center gap-2"
>
{deleteMode ? "Abbrechen" : "Kurs löschen"}
</Button>
)}
</div>
</div>
@ -540,7 +552,7 @@ const CourseListView = () => {
? "Erstellen Sie Ihren ersten Kurs!"
: "Ändern Sie die Filter oder setzen Sie sie zurück"}
</p>
{courses.length === 0 && (
{courses.length === 0 && canCreate && (
<Button onClick={() => router.push("/courses/create")} className="mt-4">
Kurs anlegen
</Button>
@ -573,9 +585,11 @@ const CourseListView = () => {
trainers={course.trainers}
status={course.status}
isPast={isPastCourse(course.date)}
showDeleteIcon={deleteMode}
onDelete={() => handleDeleteClick(course.id, course.name)}
onEdit={() => router.push(`/courses/edit/${course.id}`)}
showDeleteIcon={deleteMode && canDelete}
onDelete={
canDelete ? () => handleDeleteClick(course.id, course.name) : undefined
}
onEdit={canEdit ? () => router.push(`/courses/edit/${course.id}`) : undefined}
/>
))}
</div>

View file

@ -5,7 +5,13 @@ import { Button } from "@/src/components/ui/button";
import { useRouter } from "next/navigation";
import CourseListView from "./course-list-view";
const CourseView = () => {
interface CourseViewProps {
canCreate: boolean;
canEdit: boolean;
canDelete: boolean;
}
const CourseView = ({ canCreate, canEdit, canDelete }: CourseViewProps) => {
const router = useRouter();
return (
@ -13,16 +19,18 @@ const CourseView = () => {
<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">Kurse</h1>
<div className="flex flex-col gap-4 mt-2 md:flex-row md:mt-0">
<Button
onClick={() => router.push("/courses/create")}
className="hidden sm:flex text-sm tracking-wide font-normal"
>
Kurs anlegen
</Button>
{canCreate && (
<Button
onClick={() => router.push("/courses/create")}
className="hidden sm:flex text-sm tracking-wide font-normal"
>
Kurs anlegen
</Button>
)}
</div>
</div>
<Suspense>
<CourseListView />
<CourseListView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />
</Suspense>
</div>
);

View file

@ -33,7 +33,7 @@ type MenuItem = {
label: string;
href: string;
roles?: string[];
permission?: keyof EmployeeData["permissions"];
permission?: (p: EmployeeData["permissions"]) => boolean;
};
const allMenuItems: MenuItem[] = [
@ -60,7 +60,7 @@ const allMenuItems: MenuItem[] = [
label: "Kurse",
href: "/courses",
roles: ["employee"],
permission: "canCreateCourses",
permission: (p) => p.courses.view,
},
{
icon: IdCardLanyard,
@ -73,7 +73,7 @@ const allMenuItems: MenuItem[] = [
label: "Mitarbeiter",
href: "/employee",
roles: ["employee"],
permission: "canCreateEmployees",
permission: (p) => p.employees.view,
},
{
icon: User,
@ -93,6 +93,13 @@ const allMenuItems: MenuItem[] = [
href: "/rooms",
roles: ["manager"],
},
{
icon: DoorOpen,
label: "Räume",
href: "/rooms",
roles: ["employee"],
permission: (p) => p.rooms.view,
},
{
icon: ChartNoAxesCombined,
label: "Marketing",
@ -114,7 +121,7 @@ const DashboardSidebar = ({ userData, displayName }: DashboardSidebarProps) => {
if (!item.roles?.includes(userRole)) return false;
if (userRole === "employee" && item.permission) {
const employeeData = userData as EmployeeData;
return employeeData.permissions[item.permission] === true;
return item.permission(employeeData.permissions);
}
return true;
});

View file

@ -44,14 +44,39 @@ const DashboardView = async () => {
) : isEmployee(userData) ? (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-2">Meine Berechtigungen</h2>
<ul className="text-gray-600 text-sm space-y-1 mt-2">
{userData.permissions.canCreateCourses && <li>• Kurse erstellen</li>}
{userData.permissions.canCreateEmployees && <li>• Mitarbeiter anlegen</li>}
{!userData.permissions.canCreateCourses &&
!userData.permissions.canCreateEmployees && (
<li className="text-gray-400">Keine erweiterten Berechtigungen</li>
)}
</ul>
{(() => {
const p = userData.permissions;
const RESOURCE_LABELS: Record<string, string> = {
employees: "Mitarbeiter",
courses: "Kurse",
rooms: "Räume",
};
const ACTION_LABELS: Record<string, string> = {
view: "ansehen",
create: "erstellen",
edit: "bearbeiten",
delete: "löschen",
};
const rows = (Object.entries(p) as [string, Record<string, boolean>][])
.map(([res, actions]) => {
const active = Object.entries(actions)
.filter(([, v]) => v)
.map(([a]) => ACTION_LABELS[a] ?? a);
return active.length > 0
? `${RESOURCE_LABELS[res] ?? res}: ${active.join(", ")}`
: null;
})
.filter(Boolean);
return rows.length > 0 ? (
<ul className="text-gray-600 text-sm space-y-1 mt-2">
{rows.map((row) => (
<li key={row}>• {row}</li>
))}
</ul>
) : (
<p className="text-gray-400 text-sm mt-2">Keine erweiterten Berechtigungen</p>
);
})()}
</div>
) : (
<div className="bg-white rounded-lg shadow p-6">

View file

@ -54,7 +54,7 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
// Schritt 3: Manager oder Employee mit Berechtigung
const canManageEmployees =
isManager(userData) || (isEmployee(userData) && userData.permissions.canCreateEmployees);
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.create);
if (!canManageEmployees) {
return {
success: false,
@ -181,7 +181,7 @@ export async function getMyEmployees() {
// Schritt 2: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.canCreateEmployees))
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.employees.view))
) {
return {
success: false,
@ -239,7 +239,7 @@ export async function getEmployeeById(employeeId: string) {
// Schritt 2: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.canCreateEmployees))
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.employees.view))
) {
return {
success: false,
@ -320,7 +320,7 @@ export async function updateEmployee(
// Schritt 3: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
const canManageEmployees =
isManager(userData) || (isEmployee(userData) && userData.permissions.canCreateEmployees);
isManager(userData) || (isEmployee(userData) && userData.permissions.employees.edit);
if (!canManageEmployees) {
return {
success: false,
@ -349,11 +349,16 @@ export async function updateEmployee(
};
}
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
if (isManager(userData) && existingEmployee.createdBy !== userData.id) {
// Schritt 6: Überprüfen, ob der Benutzer der Besitzer des Mitarbeiters ist
let ownerManagerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
ownerManagerId = selfRecord?.createdBy ?? userData.id;
}
if (existingEmployee.createdBy !== ownerManagerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Mitarbeiter bearbeiten",
error: "Sie können nur Mitarbeiter Ihres Managers bearbeiten",
};
}
@ -455,8 +460,11 @@ export async function getMyEmployeeRoles() {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
if (!userData || !isManager(userData)) {
// Schritt 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.employees.view))
) {
return {
success: false,
error: "Unauthorized",
@ -464,10 +472,17 @@ export async function getMyEmployeeRoles() {
};
}
// Schritt 3: Alle Mitarbeiter des Managers abrufen (nur roles Feld)
// Schritt 3: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 4: Alle Mitarbeiter des Managers abrufen (nur roles Feld)
const employees = await prisma.employee.findMany({
where: {
createdBy: userData.id,
createdBy: managerId,
},
select: {
roles: true,
@ -517,7 +532,7 @@ export async function deleteEmployee(employeeId: string) {
// Schritt 2: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.canCreateEmployees))
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.employees.delete))
) {
return {
success: false,
@ -546,11 +561,16 @@ export async function deleteEmployee(employeeId: string) {
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
if (isManager(userData) && employee.createdBy !== userData.id) {
// Schritt 5: Überprüfen, ob der Benutzer der Besitzer des Mitarbeiters ist
let ownerManagerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
ownerManagerId = selfRecord?.createdBy ?? userData.id;
}
if (employee.createdBy !== ownerManagerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Mitarbeiter löschen",
error: "Sie können nur Mitarbeiter Ihres Managers löschen",
};
}

View file

@ -32,10 +32,24 @@ export const employeeSchema = z
// Administrationsrechte
permissions: z.object({
// Berechtigung zum Anzeigen/Verwalten von Kursen
canCreateCourses: z.boolean(),
// Berechtigung zum Anzeigen/Verwalten von Mitarbeitern
canCreateEmployees: z.boolean(),
employees: z.object({
view: z.boolean(),
create: z.boolean(),
edit: z.boolean(),
delete: z.boolean(),
}),
courses: z.object({
view: z.boolean(),
create: z.boolean(),
edit: z.boolean(),
delete: z.boolean(),
}),
rooms: z.object({
view: z.boolean(),
create: z.boolean(),
edit: z.boolean(),
delete: z.boolean(),
}),
}),
})
.refine(

View file

@ -18,8 +18,9 @@ interface EmployeeListItemProps {
roles: string[]; // Rollen des Mitarbeiters
locations: string[]; // Standorte
permissions: {
canCreateCourses: boolean;
canCreateEmployees: boolean;
employees: { view: boolean; create: boolean; edit: boolean; delete: boolean };
courses: { view: boolean; create: boolean; edit: boolean; delete: boolean };
rooms: { view: boolean; create: boolean; edit: boolean; delete: boolean };
} | null; // Administrationsrechte
status: string; // Status ("draft" oder "published")
isOnboarded: boolean; // Ob der Mitarbeiter das Onboarding abgeschlossen hat
@ -113,26 +114,49 @@ const EmployeeListItem: React.FC<EmployeeListItemProps> = ({
)}
{/* Administrationsrechte */}
{permissions && (permissions.canCreateCourses || permissions.canCreateEmployees) && (
<div>
<div className="flex items-center gap-2 text-gray-600 mb-2">
<Lock size={16} />
<span className="text-sm font-medium">Administrationsrechte</span>
</div>
<div className="flex flex-wrap gap-2">
{permissions.canCreateCourses && (
<span className="px-3 py-1 bg-purple-50 text-purple-700 text-sm rounded-full">
Kurse anlegen
</span>
)}
{permissions.canCreateEmployees && (
<span className="px-3 py-1 bg-purple-50 text-purple-700 text-sm rounded-full">
Trainer anlegen
</span>
)}
</div>
</div>
)}
{permissions &&
(() => {
const LABELS: Record<string, string> = {
"employees.view": "Mitarbeiter ansehen",
"employees.create": "Mitarbeiter erstellen",
"employees.edit": "Mitarbeiter bearbeiten",
"employees.delete": "Mitarbeiter löschen",
"courses.view": "Kurse ansehen",
"courses.create": "Kurse erstellen",
"courses.edit": "Kurse bearbeiten",
"courses.delete": "Kurse löschen",
"rooms.view": "Räume ansehen",
"rooms.create": "Räume erstellen",
"rooms.edit": "Räume bearbeiten",
"rooms.delete": "Räume löschen",
};
const active = (
Object.entries(permissions) as [string, Record<string, boolean>][]
).flatMap(([res, actions]) =>
(Object.entries(actions) as [string, boolean][])
.filter(([, v]) => v)
.map(([action]) => `${res}.${action}`)
);
if (active.length === 0) return null;
return (
<div>
<div className="flex items-center gap-2 text-gray-600 mb-2">
<Lock size={16} />
<span className="text-sm font-medium">Administrationsrechte</span>
</div>
<div className="flex flex-wrap gap-2">
{active.map((key) => (
<span
key={key}
className="px-3 py-1 bg-purple-50 text-purple-700 text-sm rounded-full"
>
{LABELS[key] ?? key}
</span>
))}
</div>
</div>
);
})()}
{/* Action Buttons */}
<div className="flex justify-end items-center pt-3 border-t border-gray-200">

View file

@ -23,6 +23,25 @@ const INITIAL_ROLES = [
{ value: "manager", label: "Manager" },
];
const PERMISSION_GROUPS = [
{ key: "employees" as const, label: "Mitarbeiter" },
{ key: "courses" as const, label: "Kurse" },
{ key: "rooms" as const, label: "Räume" },
];
const PERMISSION_ACTIONS = [
{ key: "view" as const, label: "Ansehen" },
{ key: "create" as const, label: "Erstellen" },
{ key: "edit" as const, label: "Bearbeiten" },
{ key: "delete" as const, label: "Löschen" },
];
type PermissionsState = {
employees: { view: boolean; create: boolean; edit: boolean; delete: boolean };
courses: { view: boolean; create: boolean; edit: boolean; delete: boolean };
rooms: { view: boolean; create: boolean; edit: boolean; delete: boolean };
};
interface EmployeeCreateViewProps {
customRoles: Array<{ value: string; label: string }>;
}
@ -47,8 +66,11 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
const [roles, setRoles] = useState(combinedRoles);
const [newlyCreatedRoles, setNewlyCreatedRoles] = useState<string[]>([]);
const [isPermissionCourseCreation, setIsPermissionCourseCreation] = useState(false);
const [isPermissionEmplyoeeCreation, setIsPermissionEmplyoeeCreation] = useState(false);
const [permissionsState, setPermissionsState] = useState<PermissionsState>({
employees: { view: false, create: false, edit: false, delete: false },
courses: { view: false, create: false, edit: false, delete: false },
rooms: { view: false, create: false, edit: false, delete: false },
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string[]>>({});
@ -63,80 +85,61 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
setSelectedRoles(selectedRoles.filter((v) => v !== valueToDelete));
};
const handlePermissionChange = (
resource: keyof PermissionsState,
action: "view" | "create" | "edit" | "delete",
value: boolean
) => {
setPermissionsState((prev) => {
const updated = { ...prev, [resource]: { ...prev[resource], [action]: value } };
// Andere Aktionen erfordern view
if (action !== "view" && value) {
updated[resource] = { ...updated[resource], view: true };
}
// view deaktivieren → alles deaktivieren
if (action === "view" && !value) {
updated[resource] = { view: false, create: false, edit: false, delete: false };
}
return updated;
});
};
const buildData = () => ({
email: employeeMail,
roles: selectedRoles,
isMultipleLocation,
locations: isMultipleLocation ? selectedLocations : [],
permissions: permissionsState,
});
const handlePublish = async () => {
setIsSubmitting(true);
setErrors({});
const data = {
email: employeeMail,
roles: selectedRoles,
isMultipleLocation,
locations: isMultipleLocation ? selectedLocations : [],
permissions: {
canCreateCourses: isPermissionCourseCreation,
canCreateEmployees: isPermissionEmplyoeeCreation,
},
};
const result = await createEmployee(data, "published");
const result = await createEmployee(buildData(), "published");
if (result.success) {
toast.success(result.message);
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
if (result.fieldErrors) {
setErrors(result.fieldErrors);
}
if (result.fieldErrors) setErrors(result.fieldErrors);
}
setIsSubmitting(false);
};
const handleSaveDraft = async () => {
setIsSubmitting(true);
setErrors({});
const data = {
email: employeeMail,
roles: selectedRoles,
isMultipleLocation,
locations: isMultipleLocation ? selectedLocations : [],
permissions: {
canCreateCourses: isPermissionCourseCreation,
canCreateEmployees: isPermissionEmplyoeeCreation,
},
};
const result = await createEmployee(data, "draft");
const result = await createEmployee(buildData(), "draft");
if (result.success) {
toast.success(result.message);
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
if (result.fieldErrors) {
setErrors(result.fieldErrors);
}
if (result.fieldErrors) setErrors(result.fieldErrors);
}
setIsSubmitting(false);
};
const permissions = [
{
id: "course-creation",
label: "Kurs anlegen",
value: isPermissionCourseCreation,
setValue: setIsPermissionCourseCreation,
},
{
id: "employee-creation",
label: "Trainer anlegen",
value: isPermissionEmplyoeeCreation,
setValue: setIsPermissionEmplyoeeCreation,
},
];
return (
<div>
<h1 className="text-3xl font-bold">Mitarbeiter anlegen</h1>
@ -202,25 +205,56 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
)}
<div>
<Label className="p-1 mb-2 text-blue text-lg font-semibold">
Welche Administrationsrechte soll der Mitarbeiter erhalten
</Label>
<div className="grid grid-cols-3 gap-3">
{permissions.map((p) => (
<div key={p.id} className="grid grid-cols-[auto_1fr] items-center gap-3">
<Checkbox
id={p.id}
className="shadow-none h-5 w-5 bg-white border-blue cursor-pointer"
checked={p.value}
onCheckedChange={(checked) => p.setValue(checked === true)}
/>
<Label htmlFor={p.id} className="cursor-pointer text-blue">
{p.label}
</Label>
<Label className="p-1 mb-3 text-blue text-lg font-semibold">Administrationsrechte</Label>
<div className="grid grid-cols-[auto_1fr] items-center gap-2 mb-4">
<Checkbox
id="all-permissions"
className="shadow-none h-5 w-5 bg-white border-blue cursor-pointer"
checked={PERMISSION_GROUPS.every((g) =>
PERMISSION_ACTIONS.every((a) => permissionsState[g.key][a.key])
)}
onCheckedChange={(checked) => {
const value = checked === true;
setPermissionsState({
employees: { view: value, create: value, edit: value, delete: value },
courses: { view: value, create: value, edit: value, delete: value },
rooms: { view: value, create: value, edit: value, delete: value },
});
}}
/>
<Label htmlFor="all-permissions" className="cursor-pointer text-blue font-semibold">
Alle Berechtigungen
</Label>
</div>
<div className="flex flex-col gap-4 mt-2">
{PERMISSION_GROUPS.map((group) => (
<div key={group.key}>
<p className="text-sm font-semibold text-gray-700 mb-2">{group.label}</p>
<div className="grid grid-cols-4 gap-2">
{PERMISSION_ACTIONS.map((action) => {
const id = `${group.key}-${action.key}`;
return (
<div key={id} className="grid grid-cols-[auto_1fr] items-center gap-2">
<Checkbox
id={id}
className="shadow-none h-5 w-5 bg-white border-blue cursor-pointer"
checked={permissionsState[group.key][action.key]}
onCheckedChange={(checked) =>
handlePermissionChange(group.key, action.key, checked === true)
}
/>
<Label htmlFor={id} className="cursor-pointer text-blue text-sm">
{action.label}
</Label>
</div>
);
})}
</div>
</div>
))}
</div>
</div>
<div className="flex gap-2">
<Button onClick={handlePublish} disabled={isSubmitting}>
{isSubmitting ? "Wird veröffentlicht..." : "Veröffentlichen"}

View file

@ -14,15 +14,20 @@ type Employee = {
email: string;
roles: string[];
locations: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
permissions: any;
permissions: unknown;
status: string | null;
isOnboarded: boolean;
createdAt: Date | null;
updatedAt: Date | null;
};
const EmployeeListView = () => {
interface EmployeeListViewProps {
canCreate: boolean;
canEdit: boolean;
canDelete: boolean;
}
const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewProps) => {
const router = useRouter();
const [employees, setEmployees] = useState<Employee[]>([]);
const [isLoading, setIsLoading] = useState(true);
@ -74,23 +79,29 @@ const EmployeeListView = () => {
<div>
<div className="flex justify-between items-center mb-6">
<div className="flex w-full justify-end mr-2 gap-2">
<Button
variant={deleteMode ? "destructive" : "outline"}
onClick={() => setDeleteMode(!deleteMode)}
className="flex items-center gap-2"
>
{deleteMode ? "Abbrechen" : "Mitarbeiter löschen"}
</Button>
{canDelete && (
<Button
variant={deleteMode ? "destructive" : "outline"}
onClick={() => setDeleteMode(!deleteMode)}
className="flex items-center gap-2"
>
{deleteMode ? "Abbrechen" : "Mitarbeiter löschen"}
</Button>
)}
</div>
</div>
{employees.length === 0 ? (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<p className="text-xl text-gray-600">Sie haben noch keinen Mitarbeiter erstellt</p>
<p className="text-gray-500 mt-2">Erstellen Sie Ihren ersten Mitarbeiter!</p>
<Button onClick={() => router.push("/employee/create")} className="mt-4">
Mitarbeiter anlegen
</Button>
<p className="text-xl text-gray-600">Keine Mitarbeiter vorhanden</p>
{canCreate && (
<>
<p className="text-gray-500 mt-2">Erstellen Sie Ihren ersten Mitarbeiter!</p>
<Button onClick={() => router.push("/employee/create")} className="mt-4">
Mitarbeiter anlegen
</Button>
</>
)}
</div>
) : (
<div className="space-y-4">
@ -104,15 +115,20 @@ const EmployeeListView = () => {
locations={employee.locations}
permissions={
employee.permissions as {
canCreateCourses: boolean;
canCreateEmployees: boolean;
employees: { view: boolean; create: boolean; edit: boolean; delete: boolean };
courses: { view: boolean; create: boolean; edit: boolean; delete: boolean };
rooms: { view: boolean; create: boolean; edit: boolean; delete: boolean };
} | null
}
status={employee.status || "draft"}
isOnboarded={employee.isOnboarded}
showDeleteIcon={deleteMode}
onDelete={() => handleDeleteClick(employee.id, getEmployeeName(employee))}
onEdit={() => router.push(`/employee/edit/${employee.id}`)}
showDeleteIcon={deleteMode && canDelete}
onDelete={
canDelete
? () => handleDeleteClick(employee.id, getEmployeeName(employee))
: undefined
}
onEdit={canEdit ? () => router.push(`/employee/edit/${employee.id}`) : undefined}
/>
))}
</div>

View file

@ -4,7 +4,13 @@ import { Button } from "@/src/components/ui/button";
import { useRouter } from "next/navigation";
import EmployeeListView from "./employee-list-view";
const EmployeeView = () => {
interface EmployeeViewProps {
canCreate: boolean;
canEdit: boolean;
canDelete: boolean;
}
const EmployeeView = ({ canCreate, canEdit, canDelete }: EmployeeViewProps) => {
const router = useRouter();
return (
@ -12,15 +18,17 @@ const EmployeeView = () => {
<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">Deine Mitarbeiter</h1>
<div className="flex flex-col gap-4 mt-2 md:flex-row md:mt-0">
<Button
onClick={() => router.push("/employee/create")}
className="text-sm tracking-wide font-normal"
>
Mitarbeiter anlegen
</Button>
{canCreate && (
<Button
onClick={() => router.push("/employee/create")}
className="text-sm tracking-wide font-normal"
>
Mitarbeiter anlegen
</Button>
)}
</div>
</div>
<EmployeeListView />
<EmployeeListView canCreate={canCreate} canEdit={canEdit} canDelete={canDelete} />
</div>
);
};

View file

@ -9,7 +9,7 @@
"use server";
import { prisma } from "@/src/lib/prisma";
import { getUserData, isManager } from "@/src/lib/auth/getUser";
import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser";
import { roomSchema, type RoomFormData } from "../schemas/room-schema";
import { revalidatePath } from "next/cache";
@ -35,11 +35,13 @@ export async function createRoom(data: RoomFormData) {
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist (nur Manager dürfen Räume erstellen)
if (!isManager(userData)) {
// Schritt 3: Manager oder Employee mit Berechtigung
const canCreate =
isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.create);
if (!canCreate) {
return {
success: false,
error: "Nur Manager können Räume erstellen",
error: "Keine Berechtigung zum Erstellen von Räumen",
};
}
@ -56,11 +58,18 @@ export async function createRoom(data: RoomFormData) {
const validatedData = validation.data;
// Schritt 5: Raum in der Datenbank erstellen
// Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
let creatorId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
creatorId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 6: Raum in der Datenbank erstellen
const room = await prisma.room.create({
data: {
name: validatedData.name,
createdBy: userData.id, // Manager-ID als Ersteller
createdBy: creatorId,
},
});
@ -125,8 +134,11 @@ export async function getMyRooms() {
// 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)) {
// Schritt 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.rooms.view))
) {
return {
success: false,
error: "Unauthorized",
@ -134,10 +146,17 @@ export async function getMyRooms() {
};
}
// Schritt 3: Räume abrufen, die von diesem Manager erstellt wurden
// Schritt 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 4: Räume abrufen, die von diesem Manager erstellt wurden
const rooms = await prisma.room.findMany({
where: {
createdBy: userData.id, // Filtere nach Manager-ID
createdBy: managerId,
},
orderBy: {
name: "asc", // Sortiere nach Name aufsteigend
@ -172,20 +191,30 @@ export async function getRoomById(roomId: string) {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
if (!userData || !isManager(userData)) {
// Schritt 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.rooms.view))
) {
return {
success: false,
error: "Nur Manager können Räume bearbeiten",
};
}
// Schritt 3: Raum aus der Datenbank abrufen
// Schritt 3: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 4: Raum aus der Datenbank abrufen
const room = await prisma.room.findUnique({
where: { id: roomId },
});
// Schritt 4: Überprüfen, ob Raum existiert
// Schritt 5: Überprüfen, ob Raum existiert
if (!room) {
return {
success: false,
@ -193,8 +222,8 @@ export async function getRoomById(roomId: string) {
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Raumes ist
if (room.createdBy !== userData.id) {
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Raumes ist
if (room.createdBy !== managerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Räume bearbeiten",
@ -239,20 +268,29 @@ export async function updateRoom(roomId: string, data: RoomFormData) {
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist
if (!isManager(userData)) {
// Schritt 3: Manager oder Employee mit Berechtigung
const canEdit =
isManager(userData) || (isEmployee(userData) && userData.permissions.rooms.edit);
if (!canEdit) {
return {
success: false,
error: "Nur Manager können Räume bearbeiten",
error: "Keine Berechtigung zum Bearbeiten von Räumen",
};
}
// Schritt 4: Raum aus der Datenbank abrufen
// Schritt 4: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 5: Raum aus der Datenbank abrufen
const existingRoom = await prisma.room.findUnique({
where: { id: roomId },
});
// Schritt 5: Überprüfen, ob Raum existiert
// Schritt 6: Überprüfen, ob Raum existiert
if (!existingRoom) {
return {
success: false,
@ -260,15 +298,15 @@ export async function updateRoom(roomId: string, data: RoomFormData) {
};
}
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Raumes ist
if (existingRoom.createdBy !== userData.id) {
// Schritt 7: Überprüfen, ob der Manager der Besitzer des Raumes ist
if (existingRoom.createdBy !== managerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Räume bearbeiten",
};
}
// Schritt 7: Formulardaten mit Zod-Schema validieren
// Schritt 8: Formulardaten mit Zod-Schema validieren
const validation = roomSchema.safeParse(data);
if (!validation.success) {
@ -322,20 +360,30 @@ export async function deleteRoom(roomId: string) {
// Schritt 1: Aktuellen Benutzer abrufen
const userData = await getUserData();
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
if (!userData || !isManager(userData)) {
// Schritt 2: Manager oder Employee mit Berechtigung
if (
!userData ||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.rooms.delete))
) {
return {
success: false,
error: "Nur Manager können Räume löschen",
error: "Keine Berechtigung zum Löschen von Räumen",
};
}
// Schritt 3: Raum aus der Datenbank abrufen
// Schritt 3: Effektive Manager-ID ermitteln
let managerId = userData.id;
if (isEmployee(userData)) {
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 4: Raum aus der Datenbank abrufen
const room = await prisma.room.findUnique({
where: { id: roomId },
});
// Schritt 4: Überprüfen, ob Raum existiert
// Schritt 5: Überprüfen, ob Raum existiert
if (!room) {
return {
success: false,
@ -343,16 +391,15 @@ export async function deleteRoom(roomId: string) {
};
}
// 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) {
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Raumes ist
if (room.createdBy !== managerId) {
return {
success: false,
error: "Sie können nur Ihre eigenen Räume löschen",
};
}
// Schritt 6: Prüfen, ob der Raum noch in zukünftigen Kursen verwendet wird
// Schritt 7: Prüfen, ob der Raum noch in zukünftigen Kursen verwendet wird
const today = new Date();
today.setHours(0, 0, 0, 0);
@ -360,7 +407,7 @@ export async function deleteRoom(roomId: string) {
where: {
room: roomId,
date: { gte: today },
createdBy: userData.id,
createdBy: managerId,
},
select: { id: true, name: true },
});
@ -375,12 +422,12 @@ export async function deleteRoom(roomId: string) {
};
}
// Schritt 7: Raum aus der Datenbank löschen
// Schritt 8: 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
// Schritt 9: Next.js Cache invalidieren, damit gelöschter Raum nicht mehr angezeigt wird
revalidatePath("/rooms");
return {

View file

@ -28,9 +28,11 @@ const RoomsListItem: React.FC<RoomsListItemProps> = ({
<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>
{onEdit && (
<button onClick={onEdit} className="p-2 rounded-md hover:bg-gray-100 transition">
<Pen size={20} className="text-gray-700" />
</button>
)}
</div>
</div>
</div>

View file

@ -17,9 +17,11 @@ type Room = {
interface RoomsListViewProps {
deleteMode: boolean;
canEdit: boolean;
canDelete: boolean;
}
const RoomsListView = ({ deleteMode }: RoomsListViewProps) => {
const RoomsListView = ({ deleteMode, canEdit, canDelete }: RoomsListViewProps) => {
const router = useRouter();
const [rooms, setRooms] = useState<Room[]>([]);
const [isLoading, setIsLoading] = useState(true);
@ -82,9 +84,9 @@ const RoomsListView = ({ deleteMode }: RoomsListViewProps) => {
<RoomsListItem
key={room.id}
roomName={room.name}
onEdit={() => handleEditClick(room.id)}
onDelete={() => handleDeleteClick(room.id, room.name)}
showDeleteIcon={deleteMode}
onEdit={canEdit ? () => handleEditClick(room.id) : undefined}
onDelete={canDelete ? () => handleDeleteClick(room.id, room.name) : undefined}
showDeleteIcon={deleteMode && canDelete}
/>
))}
</div>

View file

@ -5,7 +5,13 @@ import { Button } from "@/src/components/ui/button";
import { useRouter } from "next/navigation";
import RoomsListView from "./rooms-list-view";
const RoomsView = () => {
interface RoomsViewProps {
canCreate: boolean;
canEdit: boolean;
canDelete: boolean;
}
const RoomsView = ({ canCreate, canEdit, canDelete }: RoomsViewProps) => {
const router = useRouter();
const [deleteMode, setDeleteMode] = useState(false);
@ -14,22 +20,26 @@ const RoomsView = () => {
<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>
{canCreate && (
<Button
onClick={() => router.push("/rooms/create")}
className="text-sm tracking-wide font-normal"
>
Raum anlegen
</Button>
)}
{canDelete && (
<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} />
<RoomsListView deleteMode={deleteMode && canDelete} canEdit={canEdit} canDelete={canDelete} />
</div>
);
};