"use client";
import { Plus, Trash2 } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { ladePlanstellenDerEinheit, type PlanstelleDerEinheit } from "@/actions/positions";
import { EditPositionModal } from "@/components/positions/EditPositionModal";
import type { UnitOption } from "@/components/positions/CreatePositionModal";
import { Button } from "@/components/ui/Button";
import { SlideOver } from "@/components/ui/SlideOver";
import { fmtDate, fmtName } from "@/lib/format";
import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "./types";
// Was an einer Organisationseinheit steht — beim Klick auf ihre Karte.
//
// Alles hier kommt aus den Daten, die die Struktursicht ohnehin geladen hat:
// Einheiten, Personen und Vakanzen zum eingestellten Stichtag. Kein zusätzlicher
// Gang zur Datenbank, und damit auch keine zweite Wahrheit — was die Karte
// zeigt, ist dasselbe, woraus der Baum daneben gezeichnet ist.
function Zeile({ label, children }: { label: string; children: React.ReactNode }) {
return (
{label}{children}
);
}
export function OrgUnitPanel({
unit,
units,
unitOptions,
employees,
vacancies,
onAddUnit,
onDeleteUnit,
onClose,
}: {
unit: OrgUnitNode;
units: OrgUnitNode[];
/** Für den Änderungsdialog: derselbe Baum, nur in seiner Form. */
unitOptions: UnitOption[];
employees: OrgEmployee[];
vacancies: OrgVacancy[];
onAddUnit: (unitId: string) => void;
onDeleteUnit: (unitId: string) => void;
onClose: () => void;
}) {
// Erst beim Öffnen geladen, und nur für diese eine Einheit — siehe den Grund
// bei ladePlanstellenDerEinheit.
const [stellen, setStellen] = useState(null);
const [kostenstellen, setKostenstellen] = useState<{ id: string; code: string; name: string }[]>([]);
const [heute, setHeute] = useState("");
const [bearbeitet, setBearbeitet] = useState(null);
const laden = useCallback(async () => {
const daten = await ladePlanstellenDerEinheit(unit.id);
setStellen(daten.planstellen);
setKostenstellen(daten.kostenstellen);
setHeute(daten.heute);
}, [unit.id]);
useEffect(() => {
let verworfen = false;
// Der Nachtrag einer Antwort, die zu einer anderen Einheit gehört, wäre
// hier besonders tückisch: die Leiste sähe richtig aus und zeigte fremde
// Planstellen.
void ladePlanstellenDerEinheit(unit.id).then((daten) => {
if (verworfen) return;
setStellen(daten.planstellen);
setKostenstellen(daten.kostenstellen);
setHeute(daten.heute);
});
return () => {
verworfen = true;
};
}, [unit.id]);
const kinder = units.filter((u) => u.parent_id === unit.id);
// Die Kette nach oben, mit Schutz gegen einen Kreis in parent_id. Der ist
// heute nicht herstellbar, aber diese Schleife wäre die erste Stelle, die
// ihn ausbaden müsste — und ein hängender Browser sagt niemandem, woran es
// liegt.
const kette: OrgUnitNode[] = [];
const gesehen = new Set();
let lauf = unit.parent_id ? units.find((u) => u.id === unit.parent_id) : undefined;
while (lauf && !gesehen.has(lauf.id)) {
gesehen.add(lauf.id);
kette.unshift(lauf);
lauf = lauf.parent_id ? units.find((u) => u.id === lauf!.parent_id) : undefined;
}
const eigene = employees.filter((e) => e.org_unit_id === unit.id);
const leitung = eigene.find((e) => e.is_chief);
const belegschaft = eigene.filter((e) => !e.is_chief);
const offene = vacancies.filter((v) => v.org_unit_id === unit.id);
// Alles darunter, nicht nur die direkten Kinder — die Frage hinter „wie
// gross ist dieser Bereich" meint immer den ganzen Teilbaum.
const teilbaum = new Set([unit.id]);
for (let gewachsen = true; gewachsen; ) {
gewachsen = false;
for (const u of units) {
if (u.parent_id && teilbaum.has(u.parent_id) && !teilbaum.has(u.id)) {
teilbaum.add(u.id);
gewachsen = true;
}
}
}
const imTeilbaum = employees.filter((e) => teilbaum.has(e.org_unit_id)).length;
// Dieselbe Bedingung wie am Papierkorb auf der Karte: nur ein Blatt ohne
// Planstellen lässt sich entfernen. Die Datenbank prüft strenger — sie sieht
// auch geschlossene Planstellen, die hier nicht gezeichnet sind.
const entfernbar = kinder.length === 0 && eigene.length === 0 && offene.length === 0 && unit.parent_id !== null;
return (
{/* Auch die besetzten sind anklickbar. Was sich daran ändern
lässt, entscheidet die Datenbankfunktion: Einheit und
Gültigkeitsende bleiben bei einer besetzten Stelle
gesperrt, Tätigkeit und Kontierung nicht. */}
))}
)}
Alle unbesetzten Planstellen
{entfernbar && (
)}
{/* Derselbe Dialog wie auf der Seite Positionen. Ein zweiter, der
dasselbe tut, liefe beim ersten Zusatz auseinander. */}
{bearbeitet && (
{
setBearbeitet(null);
// Die Liste stammt aus dem Zustand dieser Leiste; ohne erneutes
// Laden stünde nach dem Umkontieren weiter die alte Kostenstelle
// da, und der Dialog daneben zeigte die neue.
void laden();
}}
/>
)}
);
}