"use client"; import { RotateCcw, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { applyReorg, undoReorg, type ReorgMovePayload } from "@/actions/reorg"; import { Lookup } from "@/components/ui/Lookup"; import { useToast } from "@/components/ui/Toast"; import { fmtDate } from "@/lib/format"; import type { OrgDepartment, OrgDivision, OrgEmployee, OrgTeam, ReorgScenarioSummary } from "./types"; type ChangeKind = "emp" | "team" | "abt" | "dept"; type PendingMove = { id: string; kind: ChangeKind; label: string; employeeIds: string[]; targetTeamId: string; targetTeamLabel: string; }; const KIND_LABELS: Record = { emp: "Mitarbeiter:in(nen)", team: "Ganzes Team", abt: "Ganze Abteilung", dept: "Ganzer Bereich", }; type ReorgWorkbenchProps = { employees: OrgEmployee[]; divisions: OrgDivision[]; departments: OrgDepartment[]; teams: OrgTeam[]; reorgScenarios: ReorgScenarioSummary[]; }; export function ReorgWorkbench({ employees, divisions, departments, teams, reorgScenarios }: ReorgWorkbenchProps) { const { showToast } = useToast(); const router = useRouter(); const [name, setName] = useState(""); const [effectiveDate, setEffectiveDate] = useState(""); const [changeType, setChangeType] = useState("emp"); const [selectedEmployees, setSelectedEmployees] = useState([]); const [sourceTeamId, setSourceTeamId] = useState(""); const [sourceDeptId, setSourceDeptId] = useState(""); const [sourceDivisionId, setSourceDivisionId] = useState(""); const [targetDivisionId, setTargetDivisionId] = useState(""); const [targetTeamId, setTargetTeamId] = useState(""); const [pendingMoves, setPendingMoves] = useState([]); const [applying, setApplying] = useState(false); const [undoingId, setUndoingId] = useState(null); const divisionById = useMemo(() => new Map(divisions.map((d) => [d.id, d])), [divisions]); const departmentById = useMemo(() => new Map(departments.map((d) => [d.id, d])), [departments]); const teamById = useMemo(() => new Map(teams.map((t) => [t.id, t])), [teams]); const teamDivisionId = useMemo(() => { const deptDivision = new Map(departments.map((d) => [d.id, d.division_id])); const m = new Map(); for (const t of teams) m.set(t.id, deptDivision.get(t.department_id) ?? ""); return m; }, [teams, departments]); const teamsInTargetDivision = useMemo( () => teams.filter((t) => teamDivisionId.get(t.id) === targetDivisionId), [teams, teamDivisionId, targetDivisionId] ); async function searchLocalEmployees(query: string): Promise { const q = query.trim().toLowerCase(); if (q.length < 2) return []; return employees .filter( (e) => !selectedEmployees.some((s) => s.id === e.id) && (`${e.first_name} ${e.last_name}`.toLowerCase().includes(q) || e.job_title.toLowerCase().includes(q)) ) .slice(0, 20); } function resolveEmployeeIds(): string[] { if (changeType === "emp") return selectedEmployees.map((e) => e.id); if (changeType === "team") return employees.filter((e) => e.team_id === sourceTeamId).map((e) => e.id); if (changeType === "abt") { const teamIds = new Set(teams.filter((t) => t.department_id === sourceDeptId).map((t) => t.id)); return employees.filter((e) => e.team_id && teamIds.has(e.team_id)).map((e) => e.id); } return employees.filter((e) => e.division_id === sourceDivisionId).map((e) => e.id); } function sourceLabel(): string { if (changeType === "emp") return `${selectedEmployees.length} Mitarbeiter:in(nen)`; if (changeType === "team") return `Team ${teamById.get(sourceTeamId)?.name ?? ""}`; if (changeType === "abt") return `Abteilung ${departmentById.get(sourceDeptId)?.name ?? ""}`; return `Bereich ${divisionById.get(sourceDivisionId)?.name ?? ""}`; } function handleAddMove() { const employeeIds = resolveEmployeeIds(); if (employeeIds.length === 0) { showToast("Keine Mitarbeiter:innen in der Auswahl gefunden.", "error"); return; } if (!targetTeamId) { showToast("Bitte ein Ziel-Team wählen.", "error"); return; } const targetTeam = teamById.get(targetTeamId); setPendingMoves((prev) => [ ...prev, { id: crypto.randomUUID(), kind: changeType, label: sourceLabel(), employeeIds, targetTeamId, targetTeamLabel: targetTeam?.name ?? "", }, ]); setSelectedEmployees([]); setSourceTeamId(""); setSourceDeptId(""); setSourceDivisionId(""); } function removeMove(id: string) { setPendingMoves((prev) => prev.filter((m) => m.id !== id)); } const divisionBefore = useMemo(() => { const m = new Map(); for (const e of employees) m.set(e.division_id, (m.get(e.division_id) ?? 0) + 1); return m; }, [employees]); const divisionDelta = useMemo(() => { const m = new Map(); const employeeById = new Map(employees.map((e) => [e.id, e])); for (const move of pendingMoves) { const targetDivId = teamDivisionId.get(move.targetTeamId); for (const empId of move.employeeIds) { const emp = employeeById.get(empId); if (!emp || !targetDivId || emp.division_id === targetDivId) continue; m.set(emp.division_id, (m.get(emp.division_id) ?? 0) - 1); m.set(targetDivId, (m.get(targetDivId) ?? 0) + 1); } } return m; }, [pendingMoves, employees, teamDivisionId]); async function handleApply() { if (!name || !effectiveDate || pendingMoves.length === 0) { showToast("Bitte Name, Datum und mindestens eine Änderung angeben.", "error"); return; } setApplying(true); const moves: ReorgMovePayload[] = pendingMoves.map((m) => ({ kind: m.kind, label: m.label, employee_ids: m.employeeIds, target_team_id: m.targetTeamId, })); const result = await applyReorg({ name, effective_date: effectiveDate, moves }); setApplying(false); if (result.success) { showToast("Reorganisation durchgeführt."); setPendingMoves([]); setName(""); setEffectiveDate(""); router.refresh(); } else { showToast(result.error ?? "Fehler bei der Reorganisation.", "error"); } } async function handleUndo(scenarioId: string) { setUndoingId(scenarioId); const result = await undoReorg({ scenario_id: scenarioId }); setUndoingId(null); if (result.success) { showToast("Reorganisation rückgängig gemacht."); router.refresh(); } else { showToast(result.error ?? "Fehler beim Rückgängigmachen.", "error"); } } return (

Neue Reorganisation

setName(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" />
setEffectiveDate(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" />
{(Object.keys(KIND_LABELS) as ChangeKind[]).map((kind) => ( ))}
{changeType === "emp" && (
placeholder="Mitarbeiter:in suchen…" onSearch={searchLocalEmployees} onSelect={(e) => setSelectedEmployees((prev) => [...prev, e])} renderResult={(e) => (
{e.first_name} {e.last_name}
{e.job_title}
)} /> {selectedEmployees.length > 0 && (
{selectedEmployees.map((e) => ( {e.first_name} {e.last_name} ))}
)}
)} {changeType === "team" && ( )} {changeType === "abt" && ( )} {changeType === "dept" && ( )}
{pendingMoves.length > 0 && (

Geplante Änderungen ({pendingMoves.length})

    {pendingMoves.map((m) => (
  • {KIND_LABELS[m.kind]} {m.label} → {m.targetTeamLabel} ({m.employeeIds.length} Mitarbeiter:innen)
  • ))}

Auswirkung auf Headcount

{Array.from(divisionDelta.entries()) .filter(([, delta]) => delta !== 0) .map(([divId, delta]) => { const before = divisionBefore.get(divId) ?? 0; return ( ); })}
Bereich Vorher Nachher Δ
{divisionById.get(divId)?.name} {before} {before + delta} 0 ? "text-success-text" : "text-danger-text"}`}> {delta > 0 ? `+${delta}` : delta}
)} {reorgScenarios.length > 0 && (

↩ Durchgeführte Reorganisationen – rückgängig machbar

    {reorgScenarios.map((s) => (
  • {s.name} wirksam ab {fmtDate(s.effective_date)} · durchgeführt am {fmtDate(s.applied_at)}
  • ))}
)}
); }