coaches are saved in DB and shown

This commit is contained in:
Rajpreet0 2026-01-09 12:59:19 +01:00
parent 99615d4fe6
commit bdd831903a
9 changed files with 978 additions and 21 deletions

View file

@ -39,17 +39,24 @@ model User {
model Employee {
id String @id @default(uuid())
firstName String
lastName String
firstName String?
lastName String?
email String @unique
is2FA Boolean @default(false)
isSendingMail Boolean @default(false)
pbSrc String?
status String?
roles String[] @default([])
locations String[] @default([])
permissions Json?
affiliation String?
label String?
qualification String?
authorization Json?
onboardingToken String? @unique
onboardingTokenExpiry DateTime?
isOnboarded Boolean @default(false)
createdBy String? // Manager ID who created the employee
trainings UserTraining[]
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt

View file

@ -1,9 +1,15 @@
import { requireManager } from "@/src/lib/auth/getUser";
import EmployeeCreateView from "@/src/modules/employee/ui/views/employee-create-view";
import { getMyEmployeeRoles } from "@/src/modules/employee/actions/employee-actions";
const EmployeeCreate = async () => {
await requireManager();
return <EmployeeCreateView />;
// Lade alle bereits verwendeten custom Rollen
const rolesResult = await getMyEmployeeRoles();
const customRoles = rolesResult.success ? rolesResult.roles : [];
return <EmployeeCreateView customRoles={customRoles} />;
};
export default EmployeeCreate;

View file

@ -0,0 +1,5 @@
const Onboarding = () => {
return <div>Onboarding Employee</div>;
};
export default Onboarding;

View file

@ -0,0 +1,469 @@
/**
* Employee Actions - Server-seitige Aktionen für Mitarbeiterverwaltung
*
* Diese Datei enthält alle Server Actions für das Mitarbeiter-Management-System.
* Funktionen umfassen: Mitarbeiter 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 { employeeSchema, type EmployeeFormData } from "../schemas/employee-schema";
import { revalidatePath } from "next/cache";
import { randomBytes } from "crypto";
/**
* Generiert einen sicheren Onboarding-Token
*
* @returns Ein zufälliger 32-Byte Token als Hex-String
*/
function generateOnboardingToken(): string {
return randomBytes(32).toString("hex");
}
/**
* Erstellt einen neuen Mitarbeiter in der Datenbank
*
* Diese Funktion validiert die Eingabedaten, überprüft die Berechtigungen des Benutzers
* und erstellt einen neuen Mitarbeiter. Der Mitarbeiter erhält einen Onboarding-Token,
* der später für den Onboarding-Prozess verwendet wird.
* Nur Manager können Mitarbeiter erstellen.
*
* @param data - Die Mitarbeiterdaten aus dem Formular
* @param status - Der Status des Mitarbeiters ("draft" für Entwurf oder "published" für veröffentlicht)
* @returns Ein Objekt mit success-Flag, optionaler employeeId, onboardingToken und Nachricht oder Fehler
*/
export async function createEmployee(data: EmployeeFormData, status: "draft" | "published") {
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 Mitarbeiter zu erstellen",
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist (nur Manager dürfen Mitarbeiter erstellen)
if (!isManager(userData)) {
return {
success: false,
error: "Nur Manager können Mitarbeiter erstellen",
};
}
// Schritt 4: Formulardaten mit Zod-Schema validieren
const validation = employeeSchema.safeParse(data);
if (!validation.success) {
return {
success: false,
error: "Validierungsfehler",
fieldErrors: validation.error.flatten().fieldErrors,
};
}
const validatedData = validation.data;
// Schritt 5: Überprüfen, ob E-Mail bereits existiert
const existingEmployee = await prisma.employee.findUnique({
where: { email: validatedData.email },
});
if (existingEmployee) {
return {
success: false,
error: "Ein Mitarbeiter mit dieser E-Mail-Adresse existiert bereits",
};
}
// Schritt 6: Onboarding-Token generieren (nur wenn veröffentlicht)
let onboardingToken: string | null = null;
let onboardingTokenExpiry: Date | null = null;
if (status === "published") {
onboardingToken = generateOnboardingToken();
// Token ist 7 Tage gültig
onboardingTokenExpiry = new Date();
onboardingTokenExpiry.setDate(onboardingTokenExpiry.getDate() + 7);
}
// Schritt 7: Mitarbeiter in der Datenbank erstellen
const employee = await prisma.employee.create({
data: {
email: validatedData.email,
roles: validatedData.roles,
locations: validatedData.locations || [],
permissions: validatedData.permissions,
status: status,
isOnboarded: false,
onboardingToken,
onboardingTokenExpiry,
createdBy: userData.id, // Manager-ID als Ersteller
},
});
// Schritt 8: Next.js Cache für /employee Seite invalidieren, damit neue Daten angezeigt werden
revalidatePath("/employee");
return {
success: true,
employeeId: employee.id,
onboardingToken: onboardingToken || undefined,
message:
status === "published"
? "Mitarbeiter erfolgreich veröffentlicht. Onboarding-Link wurde generiert."
: "Entwurf erfolgreich gespeichert",
};
} catch (error) {
console.error("Error creating employee:", error);
return {
success: false,
error: "Ein Fehler ist beim Erstellen des Mitarbeiters aufgetreten",
};
}
}
/**
* Ruft alle Mitarbeiter ab, die vom eingeloggten Manager erstellt wurden
*
* Diese Funktion gibt alle Mitarbeiter zurück, die vom aktuellen Manager erstellt wurden,
* unabhängig vom Status (draft oder published).
*
* @returns Ein Objekt mit success-Flag und einem Array von Mitarbeitern
*/
export async function getMyEmployees() {
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: "Unauthorized",
employees: [],
};
}
// Schritt 3: Mitarbeiter aus der Datenbank abrufen
const employees = await prisma.employee.findMany({
where: {
createdBy: userData.id, // Filter nach Manager ID
},
orderBy: {
createdAt: "desc", // Neueste zuerst
},
});
return {
success: true,
employees,
};
} catch (error) {
console.error("Error fetching employees:", error);
return {
success: false,
error: "Fehler beim Laden der Mitarbeiter",
employees: [],
};
}
}
/**
* Ruft einen einzelnen Mitarbeiter nach ID ab
*
* Diese Funktion gibt einen Mitarbeiter zurück, wenn der eingeloggte Manager
* der Ersteller des Mitarbeiters ist.
*
* @param employeeId - Die eindeutige ID des Mitarbeiters
* @returns Ein Objekt mit success-Flag und dem Mitarbeiter oder Fehler
*/
export async function getEmployeeById(employeeId: 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 Mitarbeiter bearbeiten",
};
}
// Schritt 3: Mitarbeiter aus der Datenbank abrufen
const employee = await prisma.employee.findUnique({
where: { id: employeeId },
});
// Schritt 4: Überprüfen, ob Mitarbeiter existiert
if (!employee) {
return {
success: false,
error: "Mitarbeiter nicht gefunden",
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
if (employee.createdBy !== userData.id) {
return {
success: false,
error: "Sie können nur Ihre eigenen Mitarbeiter bearbeiten",
};
}
return {
success: true,
employee,
};
} catch (error) {
console.error("Error fetching employee:", error);
return {
success: false,
error: "Fehler beim Laden des Mitarbeiters",
};
}
}
/**
* Aktualisiert einen bestehenden Mitarbeiter
*
* Diese Funktion validiert die Eingabedaten, überprüft die Berechtigungen des Benutzers
* und aktualisiert einen bestehenden Mitarbeiter. Nur der Manager, der den Mitarbeiter erstellt hat,
* kann ihn bearbeiten.
*
* @param employeeId - Die eindeutige ID des zu aktualisierenden Mitarbeiters
* @param data - Die aktualisierten Mitarbeiterdaten aus dem Formular
* @param status - Der Status des Mitarbeiters ("draft" für Entwurf oder "published" für veröffentlicht)
* @returns Ein Objekt mit success-Flag und Nachricht oder Fehler
*/
export async function updateEmployee(
employeeId: string,
data: EmployeeFormData,
status: "draft" | "published"
) {
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 Mitarbeiter zu bearbeiten",
};
}
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist
if (!isManager(userData)) {
return {
success: false,
error: "Nur Manager können Mitarbeiter bearbeiten",
};
}
// Schritt 4: Mitarbeiter aus der Datenbank abrufen
const existingEmployee = await prisma.employee.findUnique({
where: { id: employeeId },
});
// Schritt 5: Überprüfen, ob Mitarbeiter existiert
if (!existingEmployee) {
return {
success: false,
error: "Mitarbeiter nicht gefunden",
};
}
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
if (existingEmployee.createdBy !== userData.id) {
return {
success: false,
error: "Sie können nur Ihre eigenen Mitarbeiter bearbeiten",
};
}
// Schritt 7: Formulardaten mit Zod-Schema validieren
const validation = employeeSchema.safeParse(data);
if (!validation.success) {
return {
success: false,
error: "Validierungsfehler",
fieldErrors: validation.error.flatten().fieldErrors,
};
}
const validatedData = validation.data;
// Schritt 8: Onboarding-Token generieren, falls noch nicht vorhanden und veröffentlicht
let onboardingToken = existingEmployee.onboardingToken;
let onboardingTokenExpiry = existingEmployee.onboardingTokenExpiry;
if (status === "published" && !existingEmployee.onboardingToken) {
onboardingToken = generateOnboardingToken();
onboardingTokenExpiry = new Date();
onboardingTokenExpiry.setDate(onboardingTokenExpiry.getDate() + 7);
}
// Schritt 9: Mitarbeiter in der Datenbank aktualisieren
await prisma.employee.update({
where: { id: employeeId },
data: {
email: validatedData.email,
roles: validatedData.roles,
locations: validatedData.locations || [],
permissions: validatedData.permissions,
status,
onboardingToken,
onboardingTokenExpiry,
},
});
// Schritt 10: Next.js Cache invalidieren
revalidatePath("/employee");
revalidatePath(`/employee/edit/${employeeId}`);
return {
success: true,
message:
status === "published"
? "Mitarbeiter erfolgreich aktualisiert und veröffentlicht"
: "Entwurf erfolgreich aktualisiert",
};
} catch (error) {
console.error("Error updating employee:", error);
return {
success: false,
error: "Ein Fehler ist beim Aktualisieren des Mitarbeiters aufgetreten",
};
}
}
/**
* Ruft alle verwendeten Rollen des Managers ab
*
* Diese Funktion extrahiert alle Rollen aus den Mitarbeitern des aktuellen Managers,
* dedupliziert sie und gibt sie zurück. Dies ermöglicht es, custom Rollen
* wiederzuverwenden ohne eine separate Rollen-Tabelle zu benötigen.
*
* @returns Ein Objekt mit success-Flag und einem Array von Rollen im Format {value, label}
*/
export async function getMyEmployeeRoles() {
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: "Unauthorized",
roles: [],
};
}
// Schritt 3: Alle Mitarbeiter des Managers abrufen (nur roles Feld)
const employees = await prisma.employee.findMany({
where: {
createdBy: userData.id,
},
select: {
roles: true,
},
});
// Schritt 4: Alle Rollen extrahieren und deduplizieren
const allRoles = employees.flatMap((employee) => employee.roles);
const uniqueRoles = [...new Set(allRoles)];
// Schritt 5: In das Format {value, label} konvertieren
const formattedRoles = uniqueRoles.map((role) => ({
value: role,
label: role.charAt(0).toUpperCase() + role.slice(1), // Capitalize first letter
}));
return {
success: true,
roles: formattedRoles,
};
} catch (error) {
console.error("Error fetching employee roles:", error);
return {
success: false,
error: "Fehler beim Laden der Rollen",
roles: [],
};
}
}
/**
* Löscht einen Mitarbeiter aus der Datenbank
*
* Diese Funktion löscht einen Mitarbeiter, überprüft aber vorher:
* - Ob der Benutzer ein Manager ist
* - Ob der Mitarbeiter existiert
* - Ob der Manager der Besitzer des Mitarbeiters ist (Manager können nur eigene Mitarbeiter löschen)
*
* @param employeeId - Die eindeutige ID des zu löschenden Mitarbeiters
* @returns Ein Objekt mit success-Flag und Nachricht oder Fehler
*/
export async function deleteEmployee(employeeId: 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 Mitarbeiter löschen",
};
}
// Schritt 3: Mitarbeiter aus der Datenbank abrufen
const employee = await prisma.employee.findUnique({
where: { id: employeeId },
});
// Schritt 4: Überprüfen, ob Mitarbeiter existiert
if (!employee) {
return {
success: false,
error: "Mitarbeiter nicht gefunden",
};
}
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
if (employee.createdBy !== userData.id) {
return {
success: false,
error: "Sie können nur Ihre eigenen Mitarbeiter löschen",
};
}
// Schritt 6: Mitarbeiter aus der Datenbank löschen
await prisma.employee.delete({
where: { id: employeeId },
});
// Schritt 7: Next.js Cache invalidieren
revalidatePath("/employee");
return {
success: true,
message: "Mitarbeiter erfolgreich gelöscht",
};
} catch (error) {
console.error("Error deleting employee:", error);
return {
success: false,
error: "Fehler beim Löschen des Mitarbeiters",
};
}
}

View file

@ -0,0 +1,59 @@
/**
* Employee Schema - Validierungsschema für Mitarbeiter-Formulare
*
* Diese Datei definiert das Zod-Schema zur Validierung von Mitarbeiterdaten.
* Es stellt sicher, dass alle erforderlichen Felder korrekt ausgefüllt werden.
*/
import { z } from "zod";
/**
* Haupt-Validierungsschema für Mitarbeiter
*
* Dieses Schema validiert:
* - E-Mail-Adresse des Mitarbeiters
* - Rollen/Status des Mitarbeiters
* - Standort-Zugriffe (einzeln oder mehrere)
* - Administrationsrechte
*/
export const employeeSchema = z
.object({
// E-Mail (Pflichtfeld, muss gültige E-Mail sein)
email: z.string().email("Gültige E-Mail-Adresse erforderlich"),
// Array von Rollen (mindestens eine Rolle erforderlich)
roles: z.array(z.string()).min(1, "Mindestens eine Rolle muss ausgewählt werden"),
// Gibt an, ob der Mitarbeiter für mehrere Standorte zuständig ist
isMultipleLocation: z.boolean(),
// Array von Standort-IDs - optional
locations: z.array(z.string()).optional(),
// Administrationsrechte
permissions: z.object({
// Berechtigung zum Anlegen von Kursen
canCreateCourses: z.boolean(),
// Berechtigung zum Anlegen von Mitarbeitern
canCreateEmployees: z.boolean(),
}),
})
.refine(
(data) => {
// Validierungsregel: Wenn mehrere Standorte aktiviert sind, muss mindestens ein Standort ausgewählt werden
if (data.isMultipleLocation && (!data.locations || data.locations.length === 0)) {
return false;
}
return true;
},
{
message: "Mindestens ein Standort muss ausgewählt werden",
path: ["locations"],
}
);
/**
* TypeScript-Typ für Mitarbeiterdaten
* Wird automatisch aus dem Zod-Schema generiert
*/
export type EmployeeFormData = z.infer<typeof employeeSchema>;

View file

@ -0,0 +1,156 @@
/**
* EmployeeListItem - Mitarbeiter-Karten-Komponente
*
* Diese Komponente zeigt einen einzelnen Mitarbeiter als Karte an.
* Sie wird in der Mitarbeiterliste verwendet und zeigt alle wichtigen Mitarbeiterinformationen
* inklusive Rollen, Standorte, Onboarding-Status und Bearbeitungsoption.
*/
import { Pen, Trash2, Mail, MapPin, Shield, Lock } from "lucide-react";
/**
* Props für die EmployeeListItem Komponente
*/
interface EmployeeListItemProps {
employeeId: string; // ID des Mitarbeiters
employeeName: string; // Name des Mitarbeiters (oder E-Mail wenn Name nicht vorhanden)
email: string; // E-Mail-Adresse
roles: string[]; // Rollen des Mitarbeiters
locations: string[]; // Standorte
permissions: {
canCreateCourses: boolean;
canCreateEmployees: boolean;
} | null; // Administrationsrechte
status: string; // Status ("draft" oder "published")
isOnboarded: boolean; // Ob der Mitarbeiter das Onboarding abgeschlossen hat
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
}
/**
* Mitarbeiter-Karten-Komponente
* Zeigt Mitarbeiterinformationen in einem visuell ansprechenden Karten-Layout
*/
const EmployeeListItem: React.FC<EmployeeListItemProps> = ({
employeeName,
email,
roles,
locations,
permissions,
status,
isOnboarded,
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 relative">
{/* Status Badge */}
<div className="flex justify-between items-start">
<div className="flex gap-2">
<span
className={`w-fit text-xs font-semibold px-2 py-1 rounded ${
status === "draft" ? "bg-yellow-100 text-yellow-700" : "bg-green-100 text-green-700"
}`}
>
{status === "draft" ? "Entwurf" : "Veröffentlicht"}
</span>
<span
className={`w-fit text-xs font-semibold px-2 py-1 rounded ${
isOnboarded ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"
}`}
>
{isOnboarded ? "Onboarded" : "Ausstehend"}
</span>
</div>
</div>
{/* Name und E-Mail */}
<div>
<h2 className="text-xl font-semibold text-gray-900">{employeeName}</h2>
<div className="flex items-center gap-2 text-gray-500 mt-1">
<Mail size={16} />
<p className="text-sm">{email}</p>
</div>
</div>
{/* Rollen */}
{roles.length > 0 && (
<div>
<div className="flex items-center gap-2 text-gray-600 mb-2">
<Shield size={16} />
<span className="text-sm font-medium">Rollen</span>
</div>
<div className="flex flex-wrap gap-2">
{roles.map((role, index) => (
<span key={index} className="px-3 py-1 bg-blue-50 text-blue-700 text-sm rounded-full">
{role}
</span>
))}
</div>
</div>
)}
{/* Standorte */}
{locations.length > 0 && (
<div>
<div className="flex items-center gap-2 text-gray-600 mb-2">
<MapPin size={16} />
<span className="text-sm font-medium">Standorte</span>
</div>
<div className="flex flex-wrap gap-2">
{locations.map((location, index) => (
<span
key={index}
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
>
{location}
</span>
))}
</div>
</div>
)}
{/* 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>
)}
{/* Action Buttons */}
<div className="flex justify-end items-center pt-3 border-t border-gray-200">
<div className="flex gap-2">
{showDeleteIcon && onDelete && (
<button onClick={onDelete} className="p-2 rounded-md hover:bg-red-100 transition">
<Trash2 size={20} className="text-red-600" />
</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>
);
};
export default EmployeeListItem;

View file

@ -7,6 +7,8 @@ import { Checkbox } from "@/src/components/ui/checkbox";
import { Label } from "@/src/components/ui/label";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { createEmployee } from "../../actions/employee-actions";
import { toast } from "sonner";
const LOCATION = [
{ value: "hainburg", label: "Hainburg" },
@ -21,20 +23,105 @@ const INITIAL_ROLES = [
{ value: "manager", label: "Manager" },
];
const EmployeeCreateView = () => {
interface EmployeeCreateViewProps {
customRoles: Array<{ value: string; label: string }>;
}
const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
const router = useRouter();
const [employeeMail, setEmplyoeeMail] = useState("");
const [isMultipleLocation, setIsMultipleLocation] = useState(false);
const [selectedLocations, setSelectedLocations] = useState<string[]>([]);
const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
const [roles, setRoles] = useState(INITIAL_ROLES);
// Kombiniere Standard-Rollen mit custom Rollen (ohne Duplikate)
const allRoleValues = new Set([
...INITIAL_ROLES.map((r) => r.value),
...customRoles.map((r) => r.value),
]);
const combinedRoles = Array.from(allRoleValues).map((value) => {
const customRole = customRoles.find((r) => r.value === value);
const initialRole = INITIAL_ROLES.find((r) => r.value === value);
return customRole || initialRole || { value, label: value };
});
const [roles, setRoles] = useState(combinedRoles);
const [isPermissionCourseCreation, setIsPermissionCourseCreation] = useState(false);
const [isPermissionEmplyoeeCreation, setIsPermissionEmplyoeeCreation] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string[]>>({});
const handleCreateRole = (newRole: { value: string; label: string }) => {
setRoles([...roles, newRole]);
};
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");
if (result.success) {
toast.success(result.message);
if (result.onboardingToken) {
const onboardingUrl = `${window.location.origin}/employee/onboarding/${result.onboardingToken}`;
console.log("Onboarding URL:", onboardingUrl);
// TODO: Hier später die Mail-Versendung implementieren
}
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
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");
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);
}
}
setIsSubmitting(false);
};
const permissions = [
{
id: "course-creation",
@ -66,16 +153,20 @@ const EmployeeCreateView = () => {
value={employeeMail}
onChange={(e) => setEmplyoeeMail(e.target.value)}
/>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email[0]}</p>}
</div>
<MultiSelectDropdown
label="Welche Rollen hat der Mitarbeiter?"
selected={selectedRoles}
onSelect={setSelectedRoles}
options={roles}
allowCreate={true}
onCreateOption={handleCreateRole}
/>
<div>
<MultiSelectDropdown
label="Welche Rollen hat der Mitarbeiter?"
selected={selectedRoles}
onSelect={setSelectedRoles}
options={roles}
allowCreate={true}
onCreateOption={handleCreateRole}
/>
{errors.roles && <p className="text-red-500 text-sm mt-1">{errors.roles[0]}</p>}
</div>
<div className="flex items-center gap-2">
<Checkbox
@ -95,19 +186,20 @@ const EmployeeCreateView = () => {
</div>
{isMultipleLocation && (
<>
<div>
<MultiSelectDropdown
label="An welchen Standorten ist der Trainer aktiv?"
selected={selectedLocations}
onSelect={setSelectedLocations}
options={LOCATION}
/>
</>
{errors.locations && <p className="text-red-500 text-sm mt-1">{errors.locations[0]}</p>}
</div>
)}
<div>
<Label className="p-1 mb-2 text-blue text-lg font-semibold">
Welche Administrationsrechte soll der Trainer erhalten
Welche Administrationsrechte soll der Mitarbeiter erhalten
</Label>
<div className="grid grid-cols-3 gap-3">
{permissions.map((p) => (
@ -126,13 +218,23 @@ const EmployeeCreateView = () => {
</div>
</div>
<div className="flex gap-2">
<Button>Veröffentlichen</Button>
<Button variant="outline" onClick={() => router.push("/employee")}>
<Button onClick={handlePublish} disabled={isSubmitting}>
{isSubmitting ? "Wird veröffentlicht..." : "Veröffentlichen"}
</Button>
<Button
variant="outline"
onClick={() => router.push("/employee")}
disabled={isSubmitting}
>
Abbrechen
</Button>
</div>
<button className="mt-6 w-fit cursor-pointer hover:underline disabled:opacity-50 disabled:cursor-not-allowed">
Entwurf speichern
<button
onClick={handleSaveDraft}
disabled={isSubmitting}
className="mt-6 w-fit cursor-pointer hover:underline disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? "Wird gespeichert..." : "Entwurf speichern"}
</button>
</div>
</div>

View file

@ -0,0 +1,152 @@
import { Button } from "@/src/components/ui/button";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { getMyEmployees, deleteEmployee } from "../../actions/employee-actions";
import EmployeeListItem from "../components/EmployeeListItem";
import { toast } from "sonner";
import DeleteDialog from "@/src/components/layout/DeleteDialog";
type Employee = {
id: string;
firstName: string | null;
lastName: string | null;
email: string;
roles: string[];
locations: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
permissions: any;
status: string | null;
isOnboarded: boolean;
createdAt: Date | null;
updatedAt: Date | null;
};
const EmployeeListView = () => {
const router = useRouter();
const [employees, setEmployees] = useState<Employee[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [deleteMode, setDeleteMode] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [employeeToDelete, setEmployeeToDelete] = useState<{ id: string; name: string } | null>(
null
);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
const loadEmployees = async () => {
setIsLoading(true);
const result = await getMyEmployees();
if (result.success) {
setEmployees(result.employees);
} else {
toast.error(result.error || "Fehler beim Laden der Mitarbeiter");
}
setIsLoading(false);
};
loadEmployees();
}, []);
const requestDelete = (employeeId: string, employeeName: string) => {
setEmployeeToDelete({ id: employeeId, name: employeeName });
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!employeeToDelete) return;
setIsDeleting(true);
const result = await deleteEmployee(employeeToDelete.id);
setIsDeleting(false);
setDeleteDialogOpen(false);
setEmployeeToDelete(null);
if (result.success) {
toast.success(result.message);
// Reload employees
const reloadResult = await getMyEmployees();
if (reloadResult.success) {
setEmployees(reloadResult.employees);
}
} else {
toast.error(result.error || "Fehler beim Löschen");
}
};
const getEmployeeName = (employee: Employee) => {
if (employee.firstName && employee.lastName) {
return `${employee.firstName} ${employee.lastName}`;
}
return employee.email;
};
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<p className="text-xl">Mitarbeiter wird geladen</p>
</div>
);
}
return (
<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>
</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>
</div>
) : (
<div className="space-y-4">
{employees.map((employee) => (
<EmployeeListItem
key={employee.id}
employeeId={employee.id}
employeeName={getEmployeeName(employee)}
email={employee.email}
roles={employee.roles}
locations={employee.locations}
permissions={
employee.permissions as {
canCreateCourses: boolean;
canCreateEmployees: boolean;
} | null
}
status={employee.status || "draft"}
isOnboarded={employee.isOnboarded}
showDeleteIcon={deleteMode}
onDelete={() => requestDelete(employee.id, getEmployeeName(employee))}
onEdit={() => router.push(`/employee/edit/${employee.id}`)}
/>
))}
</div>
)}
<DeleteDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
courseName={employeeToDelete?.name}
onConfirm={confirmDelete}
isLoading={isDeleting}
/>
</div>
);
};
export default EmployeeListView;

View file

@ -2,6 +2,7 @@
import { Button } from "@/src/components/ui/button";
import { useRouter } from "next/navigation";
import EmployeeListView from "./employee-list-view";
const EmployeeView = () => {
const router = useRouter();
@ -19,7 +20,7 @@ const EmployeeView = () => {
</Button>
</div>
</div>
{/* Mitarbeiter List View */}
<EmployeeListView />
</div>
);
};