"use client"; import { ChevronRight, Printer } from "lucide-react"; import Link from "next/link"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/Button"; import { fmtDate } from "@/lib/format"; import type { PrintModel, PrintPerson, PrintUnit } from "@/lib/orgchart-print"; // Auswahl, dann Vorschau, dann Papier. // // Zwei Entscheidungen prägen das hier: // // **Erstens wird gefragt, bevor gedruckt wird.** Ein Organigramm mit 850 // Personen auf Papier ist fast nie das, was jemand braucht — meist geht es um // einen Bereich, oder um die oberen zwei Ebenen für eine Besprechung. Wer // alles bekommt, druckt vierzig Blätter und wirft achtunddreissig weg. // // **Zweitens wird hierarchisch gezeichnet, nicht aufgelistet.** Kästen mit // Verbindungslinien zeigen die Über- und Unterordnung; eine Spaltenliste // zeigt sie nicht, egal wie sauber sie gesetzt ist. Die Linien entstehen aus // Rahmen von Pseudo-Elementen — damit sind sie im PDF Vektoren und bleiben // beim Hineinzoomen scharf. type Tiefe = "bereich" | "abteilung" | "team" | "personen"; const TIEFEN: { wert: Tiefe; label: string; hinweis: string }[] = [ { wert: "bereich", label: "Bereiche", hinweis: "Gesellschaft und Bereiche — passt auf ein Blatt" }, { wert: "abteilung", label: "bis Abteilung", hinweis: "eine Ebene tiefer" }, { wert: "team", label: "bis Team", hinweis: "die vollständige Struktur, ohne Namen" }, { wert: "personen", label: "mit Personen", hinweis: "jede Einheit mit ihrer Belegschaft" }, ]; const EBENE: Record = { Gesellschaft: 0, Bereich: 1, Abteilung: 2, Team: 3 }; const MAX_EBENE: Record = { bereich: 1, abteilung: 2, team: 3, personen: 3 }; type Format = "a4" | "a3"; const FORMATE: Record = { a4: { label: "A4 quer", css: "A4 landscape", breite: "297mm" }, a3: { label: "A3 quer", css: "A3 landscape", breite: "420mm" }, }; export function PrintChart({ model, asOf, today }: { model: PrintModel; asOf: string; today: string }) { const [format, setFormat] = useState("a4"); const [tiefe, setTiefe] = useState("team"); const [gewaehlt, setGewaehlt] = useState>(() => new Set(model.divisions.map((d) => d.id))); const [schritt, setSchritt] = useState<"auswahl" | "vorschau">("auswahl"); const bereiche = useMemo(() => model.divisions.filter((d) => gewaehlt.has(d.id)), [model.divisions, gewaehlt]); const mitPersonen = tiefe === "personen"; const seiten = 1 + (mitPersonen || tiefe === "team" ? bereiche.length : 0); function umschalten(id: string) { setGewaehlt((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } return (
{/* ── Auswahl ─────────────────────────────────────────── */}

Was soll aufs Papier?

Das vollständige Organigramm sind {model.totals.people} Personen. Gedruckt wird selten alles — hier steht, wie tief und welche Bereiche.

Stand {fmtDate(asOf)} {asOf !== today && " · abweichender Stichtag"}

Tiefe

{TIEFEN.map((t) => ( ))}

Bereiche ({gewaehlt.size} von {model.divisions.length})

{model.divisions.map((d) => ( ))}

{gewaehlt.size === 0 ? ( Kein Bereich gewählt — es bliebe nur die Übersicht. ) : ( <> Ergibt {seiten} {seiten === 1 ? "Seite" : "Seiten"} )}

Zurück
{schritt === "vorschau" && ( <>

{seiten} {seiten === 1 ? "Seite" : "Seiten"} · so kommt es aus dem Drucker

{(Object.keys(FORMATE) as Format[]).map((f) => ( ))}

Im Druckdialog „Als PDF speichern“ wählen. Hintergrundgrafiken müssen dort eingeschaltet sein, sonst fehlen die farbigen Kopfzeilen — in Chrome unter „Weitere Einstellungen“.

{(tiefe === "team" || mitPersonen) && bereiche.map((b, i) => ( ))}
)}
); } function Blatt({ children, breite }: { children: React.ReactNode; breite: string }) { return (
{children}
); } function Kopf({ titel, unterzeile, seite }: { titel: string; unterzeile: string; seite: string }) { return (

{titel}

{unterzeile}

{seite}

); } function Baum({ wurzel, bereiche, maxEbene, mitPersonen, }: { wurzel: PrintUnit | null; /** Auf der Übersicht nur die gewählten Bereiche unter die Wurzel hängen. */ bereiche: PrintUnit[] | null; maxEbene: number; mitPersonen: boolean; }) { if (!wurzel) return

Keine Organisationsstruktur vorhanden.

; const kinder = bereiche ?? wurzel.children; return (
); } function Knoten({ unit, kinder, maxEbene, mitPersonen, }: { unit: PrintUnit; kinder?: PrintUnit[]; maxEbene: number; mitPersonen: boolean; }) { const naechste = kinder ?? unit.children; const zeigeKinder = EBENE[unit.unitType] < maxEbene && naechste.length > 0; return (
  • {zeigeKinder && (
      {naechste.map((k) => ( ))}
    )}
  • ); } function Kasten({ unit, mitPersonen }: { unit: PrintUnit; mitPersonen: boolean }) { const ebene = EBENE[unit.unitType]; // Je höher die Einheit, desto kräftiger — so ist die Rangfolge auch in // Graustufen erkennbar, und viele Ausdrucke sind schwarzweiss. const kopf = ebene === 0 ? "bg-brand-500 text-white" : ebene === 1 ? "bg-brand-100 text-ink" : ebene === 2 ? "bg-brand-50 text-ink" : "bg-surface text-ink"; return (

    {unit.name}

    {unit.unitType} · {unit.totalPeople}

    {unit.chief ? ( ) : (

    Leitung unbesetzt

    )} {mitPersonen && unit.members.length > 0 && (
    {unit.members.map((p) => ( ))}
    )} {mitPersonen && unit.vacancies .filter((v) => !v.isChief) .map((v) => (

    offen · {v.jobTitle}

    ))} {!mitPersonen && unit.members.length > 0 && (

    {unit.members.length} {unit.members.length === 1 ? "Person" : "Personen"}

    )}
    ); } function PersonZeile({ p, leitung }: { p: PrintPerson; leitung?: boolean }) { return (

    {p.name} {p.absent && · abwesend}
    {p.jobTitle}

    ); }