mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 19:51:17 +00:00
Merge pull request #84 from Since-3/feature/employee-deletion
fallback on courses if employee does not exist anymore but is display…
This commit is contained in:
commit
d3791060d2
7 changed files with 126 additions and 10 deletions
|
|
@ -17,6 +17,7 @@ interface DeleteDialogProps {
|
|||
topicName?: string;
|
||||
onConfirm: () => void;
|
||||
isLoading?: boolean;
|
||||
warningContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
||||
|
|
@ -26,6 +27,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||
topicName,
|
||||
onConfirm,
|
||||
isLoading,
|
||||
warningContent,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
|
@ -39,6 +41,11 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||
dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{warningContent && (
|
||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-800">
|
||||
{warningContent}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose className="mt-6 flex gap-2" asChild>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const CourseListItem: React.FC<CourseListItemProps> = ({
|
|||
<div className="flex items-center w-full">
|
||||
{trainers.map((trainerId, index) => {
|
||||
const trainerInfo = trainersMap[trainerId];
|
||||
const displayName = trainerInfo?.label ?? trainerId;
|
||||
const displayName = trainerInfo?.label ?? "Ehemaliger Trainer";
|
||||
const avatarSrc = trainerInfo?.pbSrc;
|
||||
return (
|
||||
<AbstractTooltip key={`${trainerId}-${index}`} tooltipText={displayName}>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [filterStatus, setFilterStatus] = useState<"all" | "draft" | "published">("all");
|
||||
const [filterSport, setFilterSport] = useState<string>("all");
|
||||
const [filterTrainer, setFilterTrainer] = useState<string>("all");
|
||||
const [filterTrainer, setFilterTrainer] = useState<string>(searchParams.get("trainer") ?? "all");
|
||||
const [filterRoom, setFilterRoom] = useState<string>(searchParams.get("room") ?? "all");
|
||||
const [filterLevel, setFilterLevel] = useState<string>("all");
|
||||
const [dateFrom, setDateFrom] = useState("");
|
||||
|
|
|
|||
|
|
@ -708,6 +708,38 @@ export async function deleteEmployee(employeeId: string) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Trainer noch in aktiven (zukünftigen) Kursen eingetragen ist.
|
||||
* Gibt die Anzahl der betroffenen Kurse zurück.
|
||||
*/
|
||||
export async function getActiveCoursesCountByTrainer(employeeId: string) {
|
||||
try {
|
||||
const userData = await getUserData();
|
||||
|
||||
if (
|
||||
!userData ||
|
||||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.employees.delete))
|
||||
) {
|
||||
return { success: false, error: "Unauthorized", count: 0 };
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const count = await prisma.course.count({
|
||||
where: {
|
||||
trainers: { has: employeeId },
|
||||
date: { gte: today },
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, count };
|
||||
} catch (error) {
|
||||
console.error("Error checking active courses for trainer:", error);
|
||||
return { success: false, error: "Fehler beim Prüfen der Kurse", count: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft einen Mitarbeiter anhand des Onboarding-Tokens ab (öffentlich, kein Login nötig)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
import { useState } from "react";
|
||||
import { deleteEmployee } from "../actions/employee-actions";
|
||||
import { deleteEmployee, getActiveCoursesCountByTrainer } from "../actions/employee-actions";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface UseDeleteEmployeeOptions {
|
||||
|
|
@ -13,9 +13,12 @@ export function useDeleteEmployee({ onSuccess }: UseDeleteEmployeeOptions = {})
|
|||
null
|
||||
);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [activeCourseCount, setActiveCourseCount] = useState(0);
|
||||
|
||||
const handleDeleteClick = (id: string, name: string) => {
|
||||
const handleDeleteClick = async (id: string, name: string) => {
|
||||
setEmployeeToDelete({ id, name });
|
||||
const result = await getActiveCoursesCountByTrainer(id);
|
||||
setActiveCourseCount(result.success ? result.count : 0);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
|
|
@ -31,9 +34,11 @@ export function useDeleteEmployee({ onSuccess }: UseDeleteEmployeeOptions = {})
|
|||
toast.success(result.message);
|
||||
const deletedId = employeeToDelete.id;
|
||||
setEmployeeToDelete(null);
|
||||
setActiveCourseCount(0);
|
||||
onSuccess?.(deletedId);
|
||||
} else {
|
||||
setEmployeeToDelete(null);
|
||||
setActiveCourseCount(0);
|
||||
toast.error(result.error || "Fehler beim Löschen des Mitarbeiters");
|
||||
}
|
||||
};
|
||||
|
|
@ -44,6 +49,7 @@ export function useDeleteEmployee({ onSuccess }: UseDeleteEmployeeOptions = {})
|
|||
employeeToDelete,
|
||||
setEmployeeToDelete,
|
||||
isDeleting,
|
||||
activeCourseCount,
|
||||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import { useRouter } from "next/navigation";
|
|||
import { useState } from "react";
|
||||
import { createEmployee, updateEmployee } from "../../actions/employee-actions";
|
||||
import { toast } from "sonner";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import DeleteDialog from "@/src/components/layout/DeleteDialog";
|
||||
import { useDeleteEmployee } from "../../hooks/useDeleteEmployee";
|
||||
|
||||
const LOCATION = [
|
||||
{ value: "hainburg", label: "Hainburg" },
|
||||
|
|
@ -115,6 +118,19 @@ const EmployeeCreateView = ({
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
||||
|
||||
const {
|
||||
deleteDialogOpen,
|
||||
setDeleteDialogOpen,
|
||||
employeeToDelete,
|
||||
setEmployeeToDelete,
|
||||
isDeleting,
|
||||
activeCourseCount,
|
||||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
} = useDeleteEmployee({
|
||||
onSuccess: () => router.push("/employee"),
|
||||
});
|
||||
|
||||
const handleCreateRole = (newRole: { value: string; label: string }) => {
|
||||
setRoles([...roles, newRole]);
|
||||
setNewlyCreatedRoles([...newlyCreatedRoles, newRole.value]);
|
||||
|
|
@ -182,12 +198,24 @@ const EmployeeCreateView = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">
|
||||
{mode === "edit" ? "Mitarbeiter bearbeiten" : "Mitarbeiter anlegen"}
|
||||
</h1>
|
||||
<p className="text-xl mt-2">
|
||||
Alle Daten können nach dem Speichern geändert oder gelöscht werden.
|
||||
</p>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">
|
||||
{mode === "edit" ? "Mitarbeiter bearbeiten" : "Mitarbeiter anlegen"}
|
||||
</h1>
|
||||
<p className="text-xl mt-2">
|
||||
Alle Daten können nach dem Speichern geändert oder gelöscht werden.
|
||||
</p>
|
||||
</div>
|
||||
{mode === "edit" && employeeId && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => handleDeleteClick(employeeId, initialData?.email ?? employeeId)}
|
||||
>
|
||||
<Trash2 size={20} className="mb-1" /> Löschen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col gap-4 max-w-xl">
|
||||
<div>
|
||||
<InputComponent
|
||||
|
|
@ -343,6 +371,29 @@ const EmployeeCreateView = ({
|
|||
{isSubmitting ? "Wird gespeichert..." : "Entwurf speichern"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDeleteDialogOpen(open);
|
||||
if (!open) setEmployeeToDelete(null);
|
||||
}}
|
||||
itemName={employeeToDelete?.name}
|
||||
topicName="Mitarbeiter"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
isLoading={isDeleting}
|
||||
warningContent={
|
||||
activeCourseCount > 0 && employeeToDelete ? (
|
||||
<p>
|
||||
Dieser Trainer ist noch in{" "}
|
||||
<strong>
|
||||
{activeCourseCount} aktiven Kurs{activeCourseCount !== 1 ? "en" : ""}
|
||||
</strong>{" "}
|
||||
eingetragen. Nach dem Löschen wird er in diesen Kursen nicht mehr angezeigt.
|
||||
</p>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import EmployeeListItem from "../components/EmployeeListItem";
|
|||
import { toast } from "sonner";
|
||||
import DeleteDialog from "@/src/components/layout/DeleteDialog";
|
||||
import { useDeleteEmployee } from "../../hooks/useDeleteEmployee";
|
||||
import Link from "next/link";
|
||||
|
||||
type Employee = {
|
||||
id: string;
|
||||
|
|
@ -39,6 +40,7 @@ const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewPro
|
|||
employeeToDelete,
|
||||
setEmployeeToDelete,
|
||||
isDeleting,
|
||||
activeCourseCount,
|
||||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
} = useDeleteEmployee({
|
||||
|
|
@ -144,6 +146,24 @@ const EmployeeListView = ({ canCreate, canEdit, canDelete }: EmployeeListViewPro
|
|||
topicName="Mitarbeiter"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
isLoading={isDeleting}
|
||||
warningContent={
|
||||
activeCourseCount > 0 && employeeToDelete ? (
|
||||
<p>
|
||||
Dieser Trainer ist noch in{" "}
|
||||
<strong>
|
||||
{activeCourseCount} aktiven Kurs{activeCourseCount !== 1 ? "en" : ""}
|
||||
</strong>{" "}
|
||||
eingetragen.{" "}
|
||||
<Link
|
||||
href={`/courses?trainer=${employeeToDelete.id}`}
|
||||
className="underline font-semibold"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
>
|
||||
Kurse anzeigen
|
||||
</Link>
|
||||
</p>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue