const { useState, useEffect, useMemo, useRef } = React;
const {
  Folder, FileText, CheckCircle2, Archive, XCircle, Clock,
  Plus, Search, X, ChevronRight, History, Layers, Trash2, AlertCircle,
  Cloud, CloudUpload, Upload, Download, ArrowLeftRight, Eye, RefreshCw, ShieldCheck
} = Icons;

// ---- Band Alpin Erscheinungsbild ----
// Werte aus ci/bandalpin-ci.css. Bis 16.08.2026 lief die App auf einem
// eigenen Blau (#3D83FC) - damit war sie das einzige Modul ausserhalb des
// Erscheinungsbildes. Alpenblau/Teal ist der einzige bunte Ton.
const PRIMARY = "#0A7E8C";        // --ba-primary
const PRIMARY_DARK = "#086874";   // --ba-primary-deep
const PRIMARY_TINT = "#EAF6F7";   // --ba-hervor-bg
const INK = "#0D1418";            // --ba-ink
const SLATE = "#38444B";          // --ba-ink-secondary
const SLATE_SOFT = "#66757D";     // --ba-ink-mute
const HAIRLINE = "#E2E6E8";       // --ba-hairline
const CANVAS = "#F2F4F5";         // --ba-canvas-soft

const STATUS = {
  gueltig:        { label: "Gültig",         dot: "#0F7B4F", bg: "#E8F4EE", fg: "#0F7B4F", icon: CheckCircle2 },
  ersetzt:        { label: "Ersetzt",        dot: "#66757D", bg: "#F2F4F5", fg: "#38444B", icon: Archive },
  ungueltig:      { label: "Ungültig",       dot: "#C2274B", bg: "#FBEAEE", fg: "#C2274B", icon: XCircle },
  in_bearbeitung: { label: "In Bearbeitung", dot: "#B57314", bg: "#FBF2E6", fg: "#B57314", icon: Clock },
};
const STATUS_ORDER = ["gueltig", "ersetzt", "in_bearbeitung", "ungueltig"];

const CHAPTERS = [
  { id: "00", name: "QM-Handbuch" },
  { id: "04", name: "Kontext der Organisation" },
  { id: "05", name: "Führung" },
  { id: "06", name: "Planung" },
  { id: "07", name: "Unterstützung" },
  { id: "08", name: "Betrieb" },
  { id: "09", name: "Bewertung der Leistung" },
  { id: "10", name: "Verbesserung" },
  { id: "FO", name: "Formulare & Vorlagen" },
];

const SEED = [
  ["BA-QM-00-01", "QM-Handbuch Band Alpin GmbH", "00", "Handbuch"],
  ["BA-QM-04-01", "Kontext- und Klimawandel-Relevanzanalyse (4.1)", "04", "Analyse"],
  ["BA-QM-04-02", "Interessierte Parteien & deren Anforderungen (4.2)", "04", "Analyse"],
  ["BA-QM-04-03", "Geltungsbereich des QM-Systems (4.3)", "04", "Dokument"],
  ["BA-QM-05-01", "Qualitätspolitik", "05", "Dokument"],
  ["BA-QM-05-02", "Organigramm & Verantwortlichkeiten", "05", "Dokument"],
  ["BA-QM-06-01", "Risikobewertung und Risikomaßnahmen (6.1.2)", "06", "Verfahrensanweisung"],
  ["BA-QM-06-02", "Chancenbewertung und Chancenmaßnahmen (6.1.3)", "06", "Verfahrensanweisung"],
  ["BA-QM-06-03", "Qualitätsziele und Planung zu deren Erreichung (6.2)", "06", "Dokument"],
  ["BA-QM-06-04", "Planung von Änderungen (6.3)", "06", "Verfahrensanweisung"],
  ["BA-QM-07-01", "Kompetenzmatrix / Schulungsplan", "07", "Dokument"],
  ["BA-QM-07-02", "Infrastruktur- und Prozessumgebungskonzept (7.1.3/7.1.4)", "07", "Dokument"],
  ["BA-QM-07-03", "VA Lenkung dokumentierter Information", "07", "Verfahrensanweisung"],
  ["BA-QM-08-01", "VA Auftragsabwicklung", "08", "Verfahrensanweisung"],
  ["BA-QM-08-02", "VA Beschaffung & Lieferantenbewertung", "08", "Verfahrensanweisung"],
  ["BA-QM-08-03", "VA Reklamationsbearbeitung", "08", "Verfahrensanweisung"],
  ["BA-QM-08-04", "Produkt-/Dienstleistungsfreigabe", "08", "Verfahrensanweisung"],
  ["BA-QM-09-01", "VA Internes Audit", "09", "Verfahrensanweisung"],
  ["BA-QM-09-02", "VA Managementbewertung", "09", "Verfahrensanweisung"],
  ["BA-QM-09-03", "Kundenzufriedenheitsanalyse", "09", "Dokument"],
  ["BA-QM-10-01", "VA Korrektur- & Vorbeugemaßnahmen", "10", "Verfahrensanweisung"],
  ["BA-QM-10-02", "KVP-Verbesserungsprozess", "10", "Verfahrensanweisung"],
  ["BA-FO-01", "Auditcheckliste", "FO", "Formular"],
  ["BA-FO-02", "Reklamationsformular", "FO", "Formular"],
  ["BA-FO-03", "Schulungsnachweis", "FO", "Formular"],
].map(([docNr, name, chapter, category], i) => ({
  id: `seed-${i}`,
  docNr, name, chapter, category,
  version: "1.0",
  status: "in_bearbeitung",
  creator: "", checker: "", approver: "", approvalDate: "",
  notes: "",
  history: [],
  hasFile: false,
}));

// Vergibt automatisch die nächste freie Dokumenten-Nr. je Kapitel (verhindert doppelte Nummern).
function nextDocNumber(chapter, docs) {
  const prefix = chapter === "FO" ? "BA-FO-" : `BA-QM-${chapter}-`;
  let max = 0;
  docs.forEach((d) => {
    if (d.docNr && d.docNr.startsWith(prefix)) {
      const num = parseInt(d.docNr.slice(prefix.length), 10);
      if (!isNaN(num) && num > max) max = num;
    }
  });
  return prefix + String(max + 1).padStart(2, "0");
}

function normalizeName(s) {
  return (s || "").trim().toLowerCase().replace(/\s+/g, " ");
}

// Prüft, ob bereits ein Dokument mit identischem Namen existiert (verhindert Doppelbenennungen).
function findDuplicateName(docs, name, excludeId) {
  const n = normalizeName(name);
  if (!n) return null;
  return docs.find((d) => d.id !== excludeId && normalizeName(d.name) === n) || null;
}

// Die Gipfel-A-Wortmarke, inline gesetzt. Nur inline erbt sie die Schrift der
// Seite (Archivo) und über currentColor die Farbe ihrer Umgebung. Als <img>
// fiele Archivo aus, und es stünde ein anderes Logo da. Geometrie unverändert
// aus ci/logo-wortmarke.svg — wer sie nachzeichnet, rechnet von der Silhouette
// zurück, nicht von der Pfadgeometrie.
function Wortmarke() {
  return (
    <svg className="ba-wortmarke-svg" viewBox="0 0 172 19" role="img" aria-label="BandAlpin">
      <text x="0" y="18" className="ink" style={{ fontFamily: "'Archivo', system-ui, sans-serif", fontWeight: 700, letterSpacing: "6.4px", fontSize: "20px" }}>BAND</text>
      <path d="M83.93 18 L91.89 3.15 L99.85 18" fill="none" className="akzent-strich" strokeWidth="2.5" />
      <text x="105.28" y="18" className="akzent" style={{ fontFamily: "'Archivo', system-ui, sans-serif", fontWeight: 700, letterSpacing: "6.4px", fontSize: "20px" }}>LPIN</text>
    </svg>
  );
}

function initials(text) {
  if (!text) return "—";
  const parts = text.trim().split(/\s+/);
  return (parts[0][0] + (parts[1]?.[0] || "")).toUpperCase();
}

// Datei zur Dokumenten-Nr. hochladen, verschieben oder herunterladen — die reale Ablage
// (dokumente/Band_Alpin_ISO9001/...) wird vom Server automatisch nach Statusordner sortiert.
async function uploadDocFile(doc, file) {
  const form = new FormData();
  form.append("file", file);
  form.append("chapter", doc.chapter);
  form.append("docNr", doc.docNr);
  form.append("name", doc.name);
  form.append("version", doc.version);
  form.append("status", doc.status);
  const res = await fetch(`/api/documents/${doc.id}/upload`, { method: "POST", body: form });
  if (!res.ok) {
    // Der Server weist eine Word-Datei zurück, wenn sich ihr Inhalt nicht
    // vollständig ins Erscheinungsbild setzen lässt (Status 422). Dann steht
    // in `meldungen`, woran es liegt — das gehört dem Bearbeiter gesagt, sonst
    // rät er.
    let text = "Upload fehlgeschlagen";
    try {
      const fehler = await res.json();
      text = fehler.error || text;
      if (fehler.meldungen && fehler.meldungen.length) {
        text += "\n\n" + fehler.meldungen.map((m) => "• " + m).join("\n");
      }
    } catch { /* keine JSON-Antwort — beim Standardtext bleiben */ }
    throw new Error(text);
  }
  return res.json();
}

// Öffnet das Dokument als Vorschau im Browser. Der Server liefert es
// inline aus, es landet also nicht im Downloadordner. Wir haben die
// Dokumente bereits — sie ein zehntes Mal zu speichern, hilft niemandem.
function oeffneVorschau(doc) {
  // "v" erzwingt bei jedem Öffnen eine neue URL. Ohne das kann ein Handy-
  // Browser eine einmal geladene PDF beliebig lange lokal weiterzeigen (so
  // am 16.08.2026 beobachtet: Datei auf dem Server längst korrigiert, das
  // Handy zeigte trotzdem noch die alte Fassung) — der Cache-Control-Header
  // allein hilft nicht gegen bereits lokal gespeicherte Kopien.
  const q = new URLSearchParams({ chapter: doc.chapter, docNr: doc.docNr, status: doc.status, v: String(Date.now()) });
  window.open(`/api/documents/${doc.id}/file?${q.toString()}`, "_blank", "noopener");
}

// Die bearbeitbare Word-Quelle. Ausgeliefert und im Audit vorgelegt wird die
// PDF; bearbeitet wird hier. Die Auditor-Ansicht kennt diesen Weg nicht.
function downloadDocSource(doc) {
  const q = new URLSearchParams({ chapter: doc.chapter, docNr: doc.docNr });
  window.open(`/api/documents/${doc.id}/quelle?${q.toString()}`, "_blank");
}

// PDF aus der vorhandenen Word-Quelle neu erzeugen, ohne Upload.
async function rerenderDocFile(doc) {
  const res = await fetch(`/api/documents/${doc.id}/neu-setzen`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      chapter: doc.chapter, docNr: doc.docNr, name: doc.name,
      version: doc.version, status: doc.status,
    }),
  });
  const antwort = await res.json().catch(() => ({}));
  if (!res.ok) {
    let text = antwort.error || "Neu setzen fehlgeschlagen";
    if (antwort.meldungen && antwort.meldungen.length) {
      text += "\n\n" + antwort.meldungen.map((m) => "• " + m).join("\n");
    }
    throw new Error(text);
  }
  return antwort;
}

async function moveDocFile(doc, fromStatus, toStatus, toVersion) {
  const res = await fetch(`/api/documents/${doc.id}/move`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chapter: doc.chapter, docNr: doc.docNr, name: doc.name, fromStatus, toStatus, toVersion }),
  });
  if (!res.ok) return { ok: false };
  return res.json();
}

// Trägt eine echte Freigabe ein — nicht nur als Feld in der App-Datenbank,
// sondern in der Word-Quelle selbst (Kopftabelle) und rendert die PDF neu.
// Datum/Uhrzeit kommen vom Server, nicht vom Client, damit sich eine Freigabe
// nicht rückdatieren lässt.
async function freigebenSenden(doc, name, rolle) {
  const res = await fetch(`/api/documents/${doc.id}/freigeben`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      chapter: doc.chapter, docNr: doc.docNr, name: doc.name,
      version: doc.version, status: doc.status, freigeber: name, rolle,
      // Der Prüfer stand bisher nur in der App-Datenbank; im Dokument
      // blieb die Zelle leer. Er wandert jetzt mit ins Dokument.
      pruefer: doc.checker || "",
    }),
  });
  const antwort = await res.json().catch(() => ({}));
  if (!res.ok) {
    let text = antwort.error || "Freigabe fehlgeschlagen.";
    if (antwort.meldungen && antwort.meldungen.length) {
      text += "\n\n" + antwort.meldungen.map((m) => "• " + m).join("\n");
    }
    throw new Error(text);
  }
  return antwort;
}

function StatusPill({ status, onClick, compact }) {
  const s = STATUS[status] || STATUS.in_bearbeitung;
  const Icon = s.icon;
  return (
    <button
      onClick={onClick}
      className="inline-flex items-center gap-1.5 rounded-full py-1 pl-2 pr-2.5 text-xs font-semibold transition hover:brightness-95 active:scale-95"
      style={{ backgroundColor: s.bg, color: s.fg }}
    >
      <Icon size={12} strokeWidth={2.75} />
      {!compact && s.label}
    </button>
  );
}

function Modal({ title, subtitle, onClose, children }) {
  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center p-4"
      style={{ backgroundColor: "rgba(17,22,31,0.45)", backdropFilter: "blur(2px)" }}
      onClick={onClose}
    >
      <div
        className="w-full max-w-lg overflow-hidden rounded-2xl bg-white shadow-2xl"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-start justify-between px-6 py-5" style={{ borderBottom: `1px solid ${HAIRLINE}` }}>
          <div>
            <h3 className="text-[15px] font-bold tracking-tight" style={{ color: INK }}>{title}</h3>
            {subtitle && <p className="mt-0.5 text-xs" style={{ color: SLATE_SOFT }}>{subtitle}</p>}
          </div>
          <button onClick={onClose} className="rounded-full p-1.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600">
            <X size={16} />
          </button>
        </div>
        <div className="max-h-[70vh] overflow-y-auto px-6 py-5">{children}</div>
      </div>
    </div>
  );
}

function Field({ label, children }) {
  return (
    <label className="mb-3.5 block">
      <span className="mb-1.5 block text-[11px] font-bold uppercase tracking-wide" style={{ color: SLATE_SOFT }}>{label}</span>
      {children}
    </label>
  );
}

const inputCls =
  "w-full rounded-lg border px-3 py-2 text-sm outline-none transition focus:border-[#3D83FC] focus:ring-2 focus:ring-[#3D83FC]/15";

/* ------------------------------------------------------------------ */
/* Login / Passwort festlegen — die App ist über Tailscale auch von     */
/* außerhalb des Firmennetzes erreichbar, deshalb ist sie geschützt.    */
/* ------------------------------------------------------------------ */
function AuthScreen({ modus, aufErfolg, aufWechsel }) {
  const [pw, setPw] = useState("");
  const [pw2, setPw2] = useState("");
  const [code, setCode] = useState("");
  const [fehler, setFehler] = useState("");
  const [busy, setBusy] = useState(false);
  const istSetup = modus === "setup";
  const istReset = modus === "reset";

  const absenden = async () => {
    setFehler("");
    if (istReset && !code.trim()) return setFehler("Wiederherstellungscode eingeben.");
    if (!pw) return setFehler("Passwort eingeben.");
    if (istSetup || istReset) {
      if (pw.length < 8) return setFehler("Mindestens 8 Zeichen.");
      if (pw !== pw2) return setFehler("Passwörter stimmen nicht überein.");
    }
    setBusy(true);
    try {
      const url = istSetup ? "/api/auth/setup" : istReset ? "/api/auth/reset" : "/api/auth/login";
      const body = istReset ? { code: code.trim(), password: pw } : { password: pw };
      const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
      const j = await r.json().catch(() => ({}));
      if (r.ok) { aufErfolg(j.recoveryCode || null); return; }
      setFehler(j.error || "Fehler.");
    } catch {
      setFehler("Keine Verbindung zum Server.");
    }
    setBusy(false);
  };

  return (
    <div className="flex min-h-screen items-center justify-center px-4" style={{ backgroundColor: CANVAS, fontFamily: "'Inter', -apple-system, 'Segoe UI', sans-serif" }}>
      <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-sm" style={{ border: `1px solid ${HAIRLINE}` }}>
        <h2 className="mb-1 text-lg font-bold tracking-tight" style={{ color: INK }}>
          {istSetup ? "Passwort festlegen" : istReset ? "Passwort zurücksetzen" : "Anmelden"}
        </h2>
        <p className="mb-4 text-sm" style={{ color: SLATE_SOFT }}>
          {istSetup
            ? "Das ISO 9001 QM-System ist auch außerhalb des Firmennetzes erreichbar. Leg ein Passwort fest, das nur ihr kennt — es wird ausschließlich auf diesem Rechner gespeichert."
            : istReset
            ? "Wiederherstellungscode eingeben (wurde bei der letzten Passwortvergabe einmalig angezeigt) und ein neues Passwort festlegen."
            : "Das ISO 9001 QM-System ist passwortgeschützt."}
        </p>
        {istReset && (
          <label className="mb-3 block text-sm">
            <span className="font-medium" style={{ color: SLATE }}>Wiederherstellungscode</span>
            <input type="text" value={code} autoFocus placeholder="ABCD-EFGH-IJKL-MNOP"
              onChange={(e) => setCode(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") absenden(); }}
              className={`mt-1 font-mono uppercase ${inputCls}`} style={{ borderColor: HAIRLINE }} />
          </label>
        )}
        <label className="mb-3 block text-sm">
          <span className="font-medium" style={{ color: SLATE }}>{istReset ? "Neues Passwort" : "Passwort"}</span>
          <input type="password" value={pw} autoFocus={!istReset}
            autoComplete={istSetup || istReset ? "new-password" : "current-password"}
            onChange={(e) => setPw(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") absenden(); }}
            className={`mt-1 ${inputCls}`} style={{ borderColor: HAIRLINE }} />
        </label>
        {(istSetup || istReset) && (
          <label className="mb-3 block text-sm">
            <span className="font-medium" style={{ color: SLATE }}>Passwort bestätigen</span>
            <input type="password" value={pw2} autoComplete="new-password"
              onChange={(e) => setPw2(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") absenden(); }}
              className={`mt-1 ${inputCls}`} style={{ borderColor: HAIRLINE }} />
          </label>
        )}
        {fehler && <div className="mb-3 rounded-lg px-3 py-2 text-sm" style={{ backgroundColor: "#FDECEC", color: "#B93330", border: "1px solid #F5C6C5" }}>{fehler}</div>}
        <button onClick={absenden} disabled={busy}
          className="w-full rounded-lg px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:brightness-110 disabled:opacity-40"
          style={{ backgroundColor: PRIMARY }}>
          {istSetup ? "Festlegen" : istReset ? "Passwort zurücksetzen" : "Anmelden"}
        </button>
        {modus === "login" && (
          <button onClick={() => aufWechsel("reset")}
            className="mt-3 w-full text-center text-xs underline underline-offset-2" style={{ color: PRIMARY_DARK }}>
            Passwort vergessen?
          </button>
        )}
        {istReset && (
          <button onClick={() => aufWechsel("login")}
            className="mt-3 w-full text-center text-xs underline underline-offset-2" style={{ color: SLATE_SOFT }}>
            Zurück zur Anmeldung
          </button>
        )}
      </div>
    </div>
  );
}

function RecoveryCodeScreen({ code, weiter }) {
  const [kopiert, setKopiert] = useState(false);
  const kopieren = async () => {
    try {
      await navigator.clipboard.writeText(code);
      setKopiert(true);
      setTimeout(() => setKopiert(false), 2000);
    } catch { /* Zwischenablage evtl. nicht verfügbar — Code steht ja sichtbar da */ }
  };

  return (
    <div className="flex min-h-screen items-center justify-center px-4" style={{ backgroundColor: CANVAS, fontFamily: "'Inter', -apple-system, 'Segoe UI', sans-serif" }}>
      <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-sm" style={{ border: `1px solid ${HAIRLINE}` }}>
        <h2 className="mb-1 text-lg font-bold tracking-tight" style={{ color: INK }}>Wiederherstellungscode</h2>
        <p className="mb-4 text-sm" style={{ color: SLATE_SOFT }}>
          Diesen Code jetzt sicher aufschreiben oder ablegen (z. B. im Passwort-Tresor). Er wird nur dieses eine Mal angezeigt und ist der einzige Weg, das Passwort zurückzusetzen, ohne direkten Zugriff auf den Server-Rechner zu haben.
        </p>
        <div className="mb-3 rounded-lg px-3 py-3 text-center font-mono text-lg font-bold tracking-wider" style={{ backgroundColor: CANVAS, border: `1px dashed ${HAIRLINE}`, color: PRIMARY_DARK }}>
          {code}
        </div>
        <button onClick={kopieren} className="mb-3 w-full rounded-lg px-4 py-2 text-sm font-semibold hover:bg-slate-200" style={{ backgroundColor: CANVAS, color: SLATE }}>
          {kopiert ? "Kopiert ✓" : "In Zwischenablage kopieren"}
        </button>
        <button onClick={weiter} className="w-full rounded-lg px-4 py-2 text-sm font-semibold text-white shadow-sm hover:brightness-110" style={{ backgroundColor: PRIMARY }}>
          Ich habe den Code gespeichert — weiter
        </button>
      </div>
    </div>
  );
}

function App() {
  const [authState, setAuthState] = useState(null); // null = wird geladen
  const [authModus, setAuthModus] = useState("login"); // "login" | "reset", solange nicht angemeldet
  const [recoveryCode, setRecoveryCode] = useState(null); // einmalig anzuzeigender Code nach Setup/Reset
  const [documents, setDocuments] = useState(SEED);
  const [loaded, setLoaded] = useState(false);
  const [saveState, setSaveState] = useState("idle");
  const [activeChapter, setActiveChapter] = useState("ALLE");
  const [activeStatus, setActiveStatus] = useState("ALLE");
  const [query, setQuery] = useState("");
  const [selectedDoc, setSelectedDoc] = useState(null);
  const [showNewDoc, setShowNewDoc] = useState(false);
  const [showVersionModal, setShowVersionModal] = useState(null);
  const [statusMenuFor, setStatusMenuFor] = useState(null);
  const [showAuditorZugang, setShowAuditorZugang] = useState(false);

  const authPruefen = () => {
    fetch("/api/auth/status")
      .then((r) => r.json())
      .then((s) => { setAuthState(s); setAuthModus("login"); })
      .catch(() => setAuthState({ configured: true, authenticated: false, verbindungsFehler: true }));
  };

  // Nach erfolgreichem Setup/Login/Reset: bei Setup & Reset kommt ein neuer
  // Wiederherstellungscode zurück, der erst einmalig gezeigt werden muss,
  // bevor es zum eigentlichen Auth-Status-Check weitergeht.
  const authErfolg = (neuerRecoveryCode) => {
    if (neuerRecoveryCode) { setRecoveryCode(neuerRecoveryCode); return; }
    authPruefen();
  };

  const nachRecoveryCode = () => {
    setRecoveryCode(null);
    authPruefen();
  };

  const abmelden = async () => {
    await fetch("/api/auth/logout", { method: "POST" });
    setLoaded(false);
    authPruefen();
  };

  useEffect(() => { authPruefen(); }, []);

  useEffect(() => {
    if (!authState?.authenticated) return;
    (async () => {
      try {
        const r = await fetch("/api/daten");
        if (r.status === 401) return setAuthState({ configured: true, authenticated: false });
        const json = await r.json();
        if (json && json.value) {
          const parsed = JSON.parse(json.value);
          if (Array.isArray(parsed) && parsed.length) setDocuments(parsed);
        }
      } catch (e) {
        // kein gespeicherter Stand — Seed bleibt aktiv
      } finally {
        setLoaded(true);
      }
    })();
  }, [authState]);

  useEffect(() => {
    if (!loaded || !authState?.authenticated) return;
    setSaveState("saving");
    const t = setTimeout(async () => {
      try {
        const res = await fetch("/api/daten", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ value: JSON.stringify(documents) }),
        });
        if (res.status === 401) { setAuthState({ configured: true, authenticated: false }); return; }
        setSaveState(res.ok ? "saved" : "error");
      } catch (e) {
        setSaveState("error");
      }
    }, 400);
    return () => clearTimeout(t);
  }, [documents, loaded]);

  const counts = useMemo(() => {
    const c = { gueltig: 0, ersetzt: 0, ungueltig: 0, in_bearbeitung: 0 };
    documents.forEach((d) => { c[d.status] = (c[d.status] || 0) + 1; });
    return c;
  }, [documents]);

  const chapterCounts = useMemo(() => {
    const c = {};
    documents.forEach((d) => { c[d.chapter] = (c[d.chapter] || 0) + 1; });
    return c;
  }, [documents]);

  const filtered = useMemo(() => {
    return documents.filter((d) => {
      if (activeChapter !== "ALLE" && d.chapter !== activeChapter) return false;
      if (activeStatus !== "ALLE" && d.status !== activeStatus) return false;
      if (query.trim()) {
        const q = query.toLowerCase();
        if (!d.name.toLowerCase().includes(q) && !d.docNr.toLowerCase().includes(q)) return false;
      }
      return true;
    });
  }, [documents, activeChapter, activeStatus, query]);

  // Nach Normkapitel gruppiert, in der Reihenfolge der Norm — nicht
  // alphabetisch und nicht nach Anlagedatum. Wer ein QM-System sucht, sucht
  // in Kapitel 8, nicht unter B.
  const gruppiert = useMemo(() => {
    const nach = new Map(CHAPTERS.map((c) => [c.id, []]));
    filtered.forEach((d) => {
      if (!nach.has(d.chapter)) nach.set(d.chapter, []);
      nach.get(d.chapter).push(d);
    });
    return [...nach.entries()]
      .filter(([, docs]) => docs.length)
      .map(([kap, docs]) => [kap, docs.sort((a, b) => a.docNr.localeCompare(b.docNr))]);
  }, [filtered]);

  // Eingeklappte Kapitel. Standard ist offen: bei einer Suche will man das
  // Ergebnis sehen, nicht erst aufklappen.
  const [zuKapitel, setZuKapitel] = useState({});
  const toggleKapitel = (kap) => setZuKapitel((z) => ({ ...z, [kap]: !z[kap] }));

  function updateDoc(id, patch) {
    setDocuments((docs) => docs.map((d) => (d.id === id ? { ...d, ...patch } : d)));
  }
  async function setStatus(id, status) {
    const doc = documents.find((d) => d.id === id);
    setStatusMenuFor(null);
    if (!doc || doc.status === status) return;
    updateDoc(id, { status });
    if (doc.hasFile) {
      const res = await moveDocFile(doc, doc.status, status, doc.version);
      if (!res.ok || res.moved === false) return; // keine Datei hinterlegt — Metadaten reichen aus
    }
  }
  // Echte GF-Freigabe: trägt sie ins Dokument selbst ein (freigebenSenden),
  // übernimmt danach die vom Server bestätigten Werte in die App-Datenbank
  // und hebt ein noch in Bearbeitung befindliches Dokument automatisch auf
  // "Gültig" — eine unterschriebene Fassung ist die aktuell gültige.
  async function signieren(id, name, rolle) {
    const doc = documents.find((d) => d.id === id);
    if (!doc) return;
    const antwort = await freigebenSenden(doc, name, rolle);
    updateDoc(id, {
      approver: antwort.approver,
      approvalDate: antwort.approvalDate,
      signature: { name, rolle, timestamp: antwort.timestamp },
      history: [{
        version: doc.version,
        date: antwort.approvalDate,
        changedBy: antwort.approver,
        status: "gueltig",
        description: `Digital freigegeben durch ${antwort.approver}.`,
      }, ...(doc.history || [])],
    });
    if (doc.status === "in_bearbeitung") {
      await setStatus(id, "gueltig");
    }
  }
  function deleteDoc(id) {
    setDocuments((docs) => docs.filter((d) => d.id !== id));
    setSelectedDoc(null);
  }
  function addDocument({ name, chapter, category }) {
    setDocuments((docs) => {
      const docNr = nextDocNumber(chapter, docs);
      const newDoc = {
        id: `d-${docs.length}-${docNr}`,
        docNr,
        name: name.trim(),
        chapter,
        category,
        version: "1.0",
        status: "in_bearbeitung",
        creator: "", checker: "", approver: "", approvalDate: "",
        notes: "",
        history: [],
        hasFile: false,
      };
      return [...docs, newDoc];
    });
    setShowNewDoc(false);
  }
  async function addNewVersion(id, { version, date, changedBy, description, file }) {
    const old = documents.find((d) => d.id === id);
    if (!old) return;
    if (old.hasFile) {
      await moveDocFile(old, old.status, "ersetzt", old.version);
    }
    // Die Datei zuerst, der Statuswechsel danach. Scheiterte das Hochladen,
    // wurde bis zum 18.08.2026 trotzdem auf "gültig" gestellt — mit
    // hasFile: false. Das Register hätte dann eine Freigabe ausgewiesen,
    // hinter der keine Datei liegt. Bricht das Hochladen ab, muss auch die
    // Freigabe abbrechen; die bereits archivierte Vorgängerfassung wird
    // dafür zurückgeholt, damit kein Dokument ohne Datei zurückbleibt.
    let hasFile = false;
    if (file) {
      try {
        await uploadDocFile({ ...old, version, status: "gueltig" }, file);
        hasFile = true;
      } catch (e) {
        if (old.hasFile) await moveDocFile(old, "ersetzt", old.status, old.version);
        alert("Die neue Dateiversion konnte nicht abgelegt werden — die Freigabe wurde nicht durchgeführt.\n\n" + e.message);
        return;
      }
    }
    setDocuments((docs) =>
      docs.map((d) => {
        if (d.id !== id) return d;
        const historyEntry = {
          version: d.version,
          date: d.approvalDate || "—",
          changedBy: d.approver || d.creator || "—",
          status: "ersetzt",
          description: d.notes || "Vorgängerversion",
        };
        return {
          ...d,
          version,
          status: "gueltig",
          approvalDate: date,
          approver: changedBy || d.approver,
          notes: description,
          history: [historyEntry, ...(d.history || [])],
          hasFile,
        };
      })
    );
    setShowVersionModal(null);
  }
  async function handleFileUpload(id, file) {
    const doc = documents.find((d) => d.id === id);
    if (!doc || !file) return;
    try {
      await uploadDocFile(doc, file);
      updateDoc(id, { hasFile: true });
    } catch (e) {
      alert("Datei-Upload fehlgeschlagen: " + e.message);
    }
  }

  const chapterName = (id) => CHAPTERS.find((c) => c.id === id)?.name || id;
  const total = documents.length || 1;
  const produktionsUrl = `http://${window.location.hostname}:4173`;

  if (!authState) return <div className="flex min-h-screen items-center justify-center" style={{ color: SLATE_SOFT }}>Lade…</div>;
  if (recoveryCode) return <RecoveryCodeScreen code={recoveryCode} weiter={nachRecoveryCode} />;
  if (!authState.configured) return <AuthScreen modus="setup" aufErfolg={authErfolg} aufWechsel={() => {}} />;
  if (!authState.authenticated) return <AuthScreen modus={authModus} aufErfolg={authErfolg} aufWechsel={setAuthModus} />;
  if (!loaded) return <div className="flex min-h-screen items-center justify-center" style={{ color: SLATE_SOFT }}>Daten werden geladen…</div>;

  return (
    <div className="flex h-full min-h-[760px] w-full flex-col" style={{ backgroundColor: CANVAS, fontFamily: "'Inter', -apple-system, 'Segoe UI', sans-serif", color: INK }}>
      {/* Top bar */}
      <header className="ba-kopfband sticky top-0 z-30 ba-umbruch ba-luecke-y flex items-center gap-x-5 px-4 py-3.5 sm:px-6 sm:py-4">
        <div className="ba-kopf-marke flex w-full min-w-0 items-center gap-3.5">
          <Wortmarke />
          <span className="h-6 w-px" style={{ backgroundColor: "#24343D" }} />
          <div className="leading-tight">
            <h1 className="text-[13px] font-bold tracking-tight">Dokumentenlenkung</h1>
            <p className="ba-kopf-unter ba-marke mt-0.5">ISO 9001:2015</p>
          </div>
        </div>

        <div className="ba-zeile-2 ba-kopf-suche relative w-full">
          <Search size={14} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2" style={{ color: SLATE_SOFT }} />
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Dokument oder Nr. suchen…"
            className="w-full rounded-lg border py-2 pl-8 pr-3 text-[13px] outline-none transition focus:ring-2"
            style={{ borderColor: HAIRLINE, backgroundColor: CANVAS }}
          />
        </div>

        <div className="ba-zeile-3 ba-kopf-aktionen ba-umbruch ba-luecke-y flex w-full min-w-0 items-center justify-end gap-x-3">
          <a
            href="/audit.html"
            target="_blank"
            rel="noreferrer"
            className="hidden items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12px] font-semibold transition hover:bg-slate-50 sm:flex"
            style={{ color: SLATE, border: `1px solid ${HAIRLINE}` }}
          >
            <Eye size={13} /> Auditor-Ansicht
          </a>
          <button
            onClick={() => setShowAuditorZugang(true)}
            className="hidden items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12px] font-semibold transition hover:bg-slate-50 sm:flex"
            style={{ color: SLATE, border: `1px solid ${HAIRLINE}` }}
          >
            <ShieldCheck size={13} /> Auditor-Zugang
          </button>
          <a
            href={produktionsUrl}
            target="_blank"
            rel="noreferrer"
            className="hidden items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12px] font-semibold transition hover:bg-slate-50 md:flex"
            style={{ color: SLATE, border: `1px solid ${HAIRLINE}` }}
          >
            <ArrowLeftRight size={13} /> SpritzgussQM Produktion
          </a>
          <span className="hidden items-center gap-1.5 text-[11px] sm:flex" style={{ color: SLATE_SOFT }}>
            {saveState === "saving" && <><CloudUpload size={13} className="animate-pulse" /> speichert…</>}
            {saveState === "saved" && <><Cloud size={13} /> gespeichert</>}
            {saveState === "error" && <span className="flex items-center gap-1 text-amber-600"><AlertCircle size={13} /> nicht gespeichert</span>}
          </span>
          <button
            onClick={() => setShowNewDoc(true)}
            className="flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-[13px] font-semibold text-white shadow-sm transition hover:brightness-110 active:scale-[0.98]"
            style={{ backgroundColor: PRIMARY }}
          >
            <Plus size={15} /> Neues Dokument
          </button>
          <button onClick={abmelden} className="text-[11px] underline underline-offset-2 whitespace-nowrap" style={{ color: SLATE_SOFT }}>
            Abmelden
          </button>
        </div>
      </header>

      {/* Signature status bar */}
      <div className="px-6 pt-6">
        {/* Kennzahlen als Kachelreihe. Die Zahl ist die Aussage, deshalb steht
            sie gross in Archivo; der Balken darüber ist nur noch eine Hairline,
            die die Verteilung zeigt — vorher war er ein Farbband, das den
            halben Blick auf sich zog, ohne mehr zu sagen. */}
        <div className="ba-flaeche">
          <div className="flex items-center justify-between px-5 pt-4">
            <span className="ba-marke">Status · gesamtes Dokumentensystem</span>
            <span className="ba-marke">{documents.length} Dokumente</span>
          </div>
          <div className="mt-3 flex w-full overflow-hidden" style={{ height: "3px", backgroundColor: HAIRLINE }}>
            {STATUS_ORDER.map((key) => {
              const pct = (counts[key] / total) * 100;
              if (!pct) return null;
              return <div key={key} style={{ width: `${pct}%`, backgroundColor: STATUS[key].dot }} title={`${STATUS[key].label}: ${counts[key]}`} />;
            })}
          </div>
          <div className="grid grid-cols-2 sm:grid-cols-4">
            {STATUS_ORDER.map((key) => {
              const s = STATUS[key];
              const active = activeStatus === key;
              return (
                <button
                  key={key}
                  onClick={() => setActiveStatus(active ? "ALLE" : key)}
                  className="px-5 py-4 text-left transition"
                  style={{
                    borderRight: `1px solid ${HAIRLINE}`,
                    backgroundColor: active ? PRIMARY_TINT : "transparent",
                    boxShadow: active ? `inset 0 -2px 0 ${PRIMARY}` : "none",
                  }}
                >
                  <span className="block">
                    {/* Groesse als Inline-Wert: Tailwind ist hier ein statischer
                        Build, beliebige Klassen wie text-[26px] stehen nicht
                        darin und wirken lautlos nicht. */}
                    <div className="ba-num font-bold leading-none" style={{ color: counts[key] ? INK : SLATE_SOFT, fontSize: "26px" }}>
                      {counts[key] || 0}
                    </div>
                    <div className="ba-marke mt-1.5 flex items-center gap-1.5">
                      <span className="inline-block h-1.5 w-1.5 rounded-full" style={{ backgroundColor: s.dot }} />
                      {s.label}
                    </div>
                  </span>
                </button>
              );
            })}
          </div>
        </div>
      </div>

      {/* Am Handy untereinander, ab Tablet nebeneinander. Vorher stand hier
          ein starres flex mit fester Seitenbreite — auf einem schmalen Schirm
          wurde die Dokumentenliste dadurch aus dem Bild geschoben und war
          nicht erreichbar. */}
      <div className="ba-hauptbereich flex-1 px-4 py-5">

        {/* Main */}
        <main className="flex-1 overflow-y-auto">
          <div className="mb-3 flex items-center justify-between px-1">
            <h2 className="text-[13px] font-semibold" style={{ color: SLATE }}>
              {activeChapter === "ALLE" ? "Alle Kapitel" : `${activeChapter} · ${chapterName(activeChapter)}`}
              {activeStatus !== "ALLE" && ` · ${STATUS[activeStatus].label}`}
            </h2>
            <span className="text-[12px]" style={{ color: SLATE_SOFT }}>{filtered.length} Dokument(e)</span>
          </div>

          <div className="overflow-hidden rounded-2xl bg-white shadow-sm" style={{ border: `1px solid ${HAIRLINE}` }}>
            {filtered.length === 0 ? (
              <div className="flex flex-col items-center justify-center gap-2 px-6 py-16 text-center">
                <FileText size={28} style={{ color: HAIRLINE }} />
                <p className="text-sm font-semibold" style={{ color: SLATE }}>Keine Dokumente in dieser Ansicht</p>
                <p className="text-xs" style={{ color: SLATE_SOFT }}>Filter anpassen oder ein neues Dokument anlegen.</p>
              </div>
            ) : (
              // Redaktioneller Aufbau statt Dateiregister: jedes Normkapitel
              // ist ein eigener Abschnitt mit Kennung, Überschrift und
              // Vorspann, die Dokumente darunter sind Karten. Das ist dieselbe
              // Gliederung, die auch die Homepage benutzt — Auszeichnung,
              // Luft, eine Aussage je Block. Vorher standen hier Zeilen von
              // 13 px Höhe mit Ordnersymbolen davor.
              <div className="ba-kapitelfolge">
                {gruppiert.map(([kap, docs]) => {
                  const frei = docs.filter((d) => d.status === "gueltig").length;
                  const zu = zuKapitel[kap];
                  return (
                    <section key={kap} className="ba-kapitel">
                      <button onClick={() => toggleKapitel(kap)} className="ba-kapitelkopf">
                        <span className="ba-marke ba-kapitelnr">Kapitel {kap}</span>
                        <h2 className="ba-kapiteltitel">{chapterName(kap)}</h2>
                        <p className="ba-kapitelvorspann">
                          {docs.length} {docs.length === 1 ? "Dokument" : "Dokumente"}
                          {" · "}
                          {frei === 0 ? "keines freigegeben" : `${frei} freigegeben`}
                        </p>
                        <span className="ba-kapitelpfeil" style={{ transform: zu ? "rotate(0deg)" : "rotate(90deg)" }}>
                          <ChevronRight size={16} />
                        </span>
                      </button>

                      {!zu && (
                        <div className="ba-kartenraster">
                          {docs.map((d) => (
                            <article key={d.id} className="ba-karte" onClick={() => setSelectedDoc(d)}>
                              <p className="ba-marke ba-karte-nr">{d.docNr}</p>
                              <h3 className="ba-karte-titel">{d.name}</h3>
                              <div className="ba-karte-fuss" onClick={(e) => e.stopPropagation()}>
                                <span className="ba-karte-status">
                                  <span className="ba-punkt" style={{ backgroundColor: STATUS[d.status].dot }} />
                                  {STATUS[d.status].label}
                                </span>
                                <span className="ba-karte-aktionen">
                                  <span className="ba-num ba-karte-version">V{d.version}</span>
                                  {d.hasFile && (
                                    <button onClick={() => oeffneVorschau(d)} className="ba-karte-pdf">Ansehen</button>
                                  )}
                                </span>
                              </div>
                            </article>
                          ))}
                        </div>
                      )}
                    </section>
                  );
                })}
              </div>
            )}
          </div>

          {/* Die beiden Vorlagen standen bisher unter dem linken Verzeichnis.
              Mit dem Wegfall der Spalte sitzen sie hier — unter dem Baum, wo
              man sie sucht, wenn man ein neues Dokument anlegt. */}
          <div className="mt-4 flex flex-wrap gap-x-6 gap-y-1 px-1 text-[12px]">
            <a href="/vorlagen/Dokumentenlenkungsliste.xlsx" className="font-semibold hover:underline" style={{ color: PRIMARY_DARK }}>↓ Dokumentenlenkungsliste.xlsx</a>
            <a href="/vorlagen/Vorlage_Word/BA-Vorlage_Dokumentvorlage_QM.docx" className="font-semibold hover:underline" style={{ color: PRIMARY_DARK }}>↓ Word-Vorlage QM-Dokument</a>
          </div>
        </main>
      </div>

      {selectedDoc && (
        <Modal title={selectedDoc.docNr} subtitle={selectedDoc.name} onClose={() => setSelectedDoc(null)}>
          <DocDetail
            doc={documents.find((d) => d.id === selectedDoc.id) || selectedDoc}
            documents={documents}
            onChange={(patch) => updateDoc(selectedDoc.id, patch)}
            onDelete={() => deleteDoc(selectedDoc.id)}
            onNewVersion={() => setShowVersionModal(selectedDoc.id)}
            onUploadFile={(file) => handleFileUpload(selectedDoc.id, file)}
            onFreigeben={(name, rolle) => signieren(selectedDoc.id, name, rolle)}
          />
        </Modal>
      )}

      {showNewDoc && (
        <Modal title="Neues Dokument anlegen" subtitle="Dokumenten-Nr. wird automatisch vergeben" onClose={() => setShowNewDoc(false)}>
          <NewDocForm documents={documents} onSubmit={addDocument} onCancel={() => setShowNewDoc(false)} />
        </Modal>
      )}

      {showVersionModal && (
        <Modal title="Neue Version freigeben" onClose={() => setShowVersionModal(null)}>
          <NewVersionForm
            current={documents.find((d) => d.id === showVersionModal)}
            onSubmit={(payload) => addNewVersion(showVersionModal, payload)}
            onCancel={() => setShowVersionModal(null)}
          />
        </Modal>
      )}

      {showAuditorZugang && (
        <Modal title="Auditor-Zugang" subtitle="Eigenes, getrenntes Passwort — nur Lesen, kein Zugriff auf die Schreib-APIs" onClose={() => setShowAuditorZugang(false)}>
          <AuditorZugangForm />
        </Modal>
      )}
    </div>
  );
}

// Getrennte Anmeldung für die Auditor-Ansicht (seit 18.08.2026). Vorher
// teilte sich audit.html die Sitzung mit dieser App — wer das Auditor-
// Passwort hatte, konnte mit derselben Sitzung auch die Schreib-APIs direkt
// ansprechen (Upload, Freigabe, Löschen), nur die Oberfläche versteckte das.
// Dieses Formular vergibt ein eigenes Passwort, das serverseitig (nicht nur
// in der Oberfläche) auf Lesen beschränkt ist.
function AuditorZugangForm() {
  const [status, setStatus] = useState(null);
  const [pw, setPw] = useState("");
  const [pw2, setPw2] = useState("");
  const [busy, setBusy] = useState(false);
  const [fehler, setFehler] = useState("");
  const [erfolg, setErfolg] = useState(false);

  useEffect(() => {
    fetch("/api/auth/auditor-status").then((r) => r.json()).then(setStatus).catch(() => setStatus({ configured: false }));
  }, []);

  const auditorUrl = `${window.location.protocol}//${window.location.hostname}:${window.location.port || "4174"}/audit.html`;

  async function speichern() {
    setFehler(""); setErfolg(false);
    if (pw.length < 4) return setFehler("Mindestens 4 Zeichen.");
    if (pw !== pw2) return setFehler("Passwörter stimmen nicht überein.");
    setBusy(true);
    try {
      const r = await fetch("/api/auth/auditor-setup", {
        method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: pw }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { setFehler(j.error || "Fehler."); return; }
      setStatus({ configured: true });
      setPw(""); setPw2(""); setErfolg(true);
    } catch {
      setFehler("Keine Verbindung zum Server.");
    } finally {
      setBusy(false);
    }
  }

  if (!status) return <p className="text-xs" style={{ color: SLATE_SOFT }}>Lade…</p>;

  return (
    <div>
      <p className="mb-3 text-xs" style={{ color: SLATE_SOFT }}>
        {status.configured
          ? "Ein Auditor-Passwort ist eingerichtet. Zum Ändern einfach ein neues vergeben — das alte verliert dann sofort seine Gültigkeit."
          : "Noch kein Auditor-Passwort vergeben. Ohne dieses Passwort kann niemand die Auditor-Ansicht öffnen."}
      </p>
      <Field label="Neues Passwort">
        <input type="password" className={inputCls} style={{ borderColor: HAIRLINE }} value={pw} onChange={(e) => setPw(e.target.value)} autoComplete="new-password" />
      </Field>
      <Field label="Passwort bestätigen">
        <input type="password" className={inputCls} style={{ borderColor: HAIRLINE }} value={pw2} onChange={(e) => setPw2(e.target.value)} autoComplete="new-password" />
      </Field>
      {fehler && <p className="mb-2 text-[11px] font-medium" style={{ color: "#B93330" }}>{fehler}</p>}
      {erfolg && (
        <div className="mb-3 rounded-lg p-3 text-xs" style={{ backgroundColor: PRIMARY_TINT, color: PRIMARY_DARK }}>
          Gespeichert. Adresse für den Auditor: <span className="font-mono">{auditorUrl}</span>
          <br />Passwort separat übermitteln (nicht in derselben Nachricht wie die Adresse).
        </div>
      )}
      <button
        onClick={speichern}
        disabled={busy}
        className="mt-1 flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-xs font-semibold text-white shadow-sm transition hover:brightness-110 disabled:opacity-50"
        style={{ backgroundColor: PRIMARY }}
      >
        <ShieldCheck size={13} /> {busy ? "wird gespeichert…" : status.configured ? "Passwort ändern" : "Passwort festlegen"}
      </button>
    </div>
  );
}

function DocDetail({ doc, documents, onChange, onDelete, onNewVersion, onUploadFile, onFreigeben }) {
  const duplicate = useMemo(() => findDuplicateName(documents || [], doc.name, doc.id), [documents, doc.name, doc.id]);
  const fileInput = useRef(null);
  const [uploading, setUploading] = useState(false);

  async function handlePick(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file) return;
    setUploading(true);
    try {
      await onUploadFile(file);
    } finally {
      setUploading(false);
    }
  }

  return (
    <div>
      <div className="mb-4 flex items-center gap-3 rounded-xl p-3" style={{ backgroundColor: CANVAS }}>
        <div className="flex h-9 w-9 items-center justify-center rounded-full text-[11px] font-bold text-white" style={{ backgroundColor: SLATE }}>
          {initials(doc.creator || doc.docNr)}
        </div>
        <div className="min-w-0 flex-1 text-xs" style={{ color: SLATE_SOFT }}>
          Version <strong style={{ color: INK }}>{doc.version}</strong> · Kapitel {doc.chapter} · {doc.category}
        </div>
        <StatusPill status={doc.status} />
      </div>

      <div className="mb-4 flex items-center gap-2 rounded-xl p-3" style={{ border: `1px dashed ${HAIRLINE}` }}>
        <FileText size={16} style={{ color: doc.hasFile ? PRIMARY : SLATE_SOFT }} />
        <span className="flex-1 text-xs" style={{ color: doc.hasFile ? INK : SLATE_SOFT }}>
          {doc.hasFile ? "Datei hinterlegt in der realen Ablage" : "Noch keine Datei hinterlegt"}
        </span>
        {doc.hasFile && (
          <button onClick={() => oeffneVorschau(doc)} className="flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs font-semibold hover:bg-slate-100" style={{ color: SLATE }}>
            <Eye size={13} /> Vorschau
          </button>
        )}
        {doc.hasFile && (
          <button
            onClick={() => downloadDocSource(doc)}
            title="Bearbeitbare Word-Quelle herunterladen. Nach der Änderung über „Ersetzen“ wieder hochladen — die PDF wird dabei neu gesetzt."
            className="flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs font-semibold hover:bg-slate-100"
            style={{ color: SLATE }}
          >
            <FileText size={13} /> Word-Quelle
          </button>
        )}
        {doc.hasFile && (
          <button
            onClick={async () => {
              try {
                await rerenderDocFile(doc);
                alert("PDF neu gesetzt.");
              } catch (e) {
                alert(e.message);
              }
            }}
            title="PDF aus der vorhandenen Word-Quelle neu erzeugen — für den Fall, dass direkt im Quellordner gearbeitet wurde."
            className="flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs font-semibold hover:bg-slate-100"
            style={{ color: SLATE }}
          >
            <RefreshCw size={13} /> Neu setzen
          </button>
        )}
        <input ref={fileInput} type="file" className="hidden" onChange={handlePick} />
        <button
          onClick={() => fileInput.current && fileInput.current.click()}
          disabled={uploading}
          className="flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-white shadow-sm hover:brightness-110 disabled:opacity-50"
          style={{ backgroundColor: PRIMARY }}
        >
          <Upload size={13} /> {uploading ? "lädt hoch…" : doc.hasFile ? "Ersetzen" : "Hochladen"}
        </button>
      </div>

      <Field label="Dokumentname">
        <input className={inputCls} style={{ borderColor: duplicate ? "#C2274B" : HAIRLINE }} value={doc.name} onChange={(e) => onChange({ name: e.target.value })} />
        {duplicate && (
          <p className="mt-1.5 flex items-center gap-1 text-[11px] font-medium" style={{ color: "#B93330" }}>
            <AlertCircle size={12} /> Achtung: identischer Name bereits vergeben bei {duplicate.docNr}
          </p>
        )}
      </Field>
      <div className="grid grid-cols-2 gap-x-3">
        <Field label="Ersteller">
          <input className={inputCls} style={{ borderColor: HAIRLINE }} value={doc.creator} onChange={(e) => onChange({ creator: e.target.value })} />
        </Field>
        <Field label="Geprüft von (QMB)">
          <input className={inputCls} style={{ borderColor: HAIRLINE }} value={doc.checker} onChange={(e) => onChange({ checker: e.target.value })} />
        </Field>
      </div>
      <FreigabeBlock doc={doc} onFreigeben={onFreigeben} />
      <Field label="Bemerkung">
        <textarea className={inputCls} style={{ borderColor: HAIRLINE }} rows={2} value={doc.notes} onChange={(e) => onChange({ notes: e.target.value })} />
      </Field>

      <div className="mt-2 flex items-center gap-2">
        <button
          onClick={onNewVersion}
          className="flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-xs font-semibold text-white shadow-sm transition hover:brightness-110"
          style={{ backgroundColor: PRIMARY }}
        >
          <Plus size={13} /> Neue Version freigeben
        </button>
        <button
          onClick={onDelete}
          className="ml-auto flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-semibold text-red-500 transition hover:bg-red-50"
        >
          <Trash2 size={13} /> Löschen
        </button>
      </div>

      {doc.history && doc.history.length > 0 && (
        <div className="mt-5 pt-4" style={{ borderTop: `1px solid ${HAIRLINE}` }}>
          <p className="mb-2.5 flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wide" style={{ color: SLATE_SOFT }}>
            <History size={13} /> Änderungshistorie
          </p>
          <ul className="space-y-2">
            {doc.history.map((h, i) => (
              <li key={i} className="rounded-xl p-2.5 text-xs" style={{ backgroundColor: CANVAS }}>
                <span className="font-mono font-bold">V{h.version}</span> <span style={{ color: SLATE_SOFT }}>· {h.date} · {h.changedBy}</span>
                <div className="mt-0.5" style={{ color: SLATE_SOFT }}>{h.description}</div>
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

// Echte digitale Freigabe: keine frei tippbaren Felder mehr (die ließen sich
// beliebig rückdatieren und behaupten, ohne dass je jemand geprüft hätte),
// sondern eine bewusste Handlung mit serverseitigem Zeitstempel. Trägt sich
// zugleich in die Word-Quelle ein — siehe freigebenSenden() in server.js.
function FreigabeBlock({ doc, onFreigeben }) {
  const [offen, setOffen] = useState(false);
  const [name, setName] = useState("Philipp Schötzer");
  const [rolle, setRolle] = useState("Geschäftsführer");
  const [bestaetigt, setBestaetigt] = useState(false);
  const [busy, setBusy] = useState(false);
  const [fehler, setFehler] = useState("");

  if (doc.signature && doc.signature.name) {
    const zeit = doc.signature.timestamp ? new Date(doc.signature.timestamp) : null;
    const zeitText = zeit
      ? zeit.toLocaleString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" }) + " Uhr"
      : doc.approvalDate;
    return (
      <div className="mt-3 flex items-center gap-3 rounded-xl p-3" style={{ backgroundColor: PRIMARY_TINT, border: `1px solid ${PRIMARY}22` }}>
        <ShieldCheck size={20} style={{ color: PRIMARY_DARK, flexShrink: 0 }} />
        <div className="min-w-0 flex-1">
          <p className="italic" style={{ fontFamily: "Archivo, sans-serif", fontSize: 17, color: INK, lineHeight: 1.2 }}>
            {doc.signature.name}
          </p>
          <p className="text-[11px]" style={{ color: SLATE_SOFT }}>
            {doc.signature.rolle} · digital bestätigt am {zeitText}
          </p>
        </div>
        <button
          onClick={() => setOffen(true)}
          className="flex-shrink-0 text-[11px] font-medium underline decoration-dotted"
          style={{ color: SLATE_SOFT }}
        >
          erneut freigeben
        </button>
        {offen && (
          <FreigabeForm
            name={name} setName={setName} rolle={rolle} setRolle={setRolle}
            bestaetigt={bestaetigt} setBestaetigt={setBestaetigt}
            busy={busy} fehler={fehler}
            onAbbrechen={() => setOffen(false)}
            onSubmit={async () => {
              setBusy(true); setFehler("");
              try { await onFreigeben(name, rolle); setOffen(false); setBestaetigt(false); }
              catch (e) { setFehler(e.message); }
              finally { setBusy(false); }
            }}
          />
        )}
      </div>
    );
  }

  return (
    <div className="mt-3 rounded-xl p-3" style={{ border: `1px dashed ${HAIRLINE}` }}>
      {!offen ? (
        <div className="flex items-center gap-3">
          <ShieldCheck size={18} style={{ color: SLATE_SOFT, flexShrink: 0 }} />
          <span className="flex-1 text-xs" style={{ color: SLATE_SOFT }}>Noch nicht freigegeben</span>
          <button
            onClick={() => setOffen(true)}
            className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:brightness-110"
            style={{ backgroundColor: PRIMARY }}
          >
            <ShieldCheck size={13} /> Jetzt digital unterschreiben
          </button>
        </div>
      ) : (
        <FreigabeForm
          name={name} setName={setName} rolle={rolle} setRolle={setRolle}
          bestaetigt={bestaetigt} setBestaetigt={setBestaetigt}
          busy={busy} fehler={fehler}
          onAbbrechen={() => setOffen(false)}
          onSubmit={async () => {
            setBusy(true); setFehler("");
            try { await onFreigeben(name, rolle); setOffen(false); setBestaetigt(false); }
            catch (e) { setFehler(e.message); }
            finally { setBusy(false); }
          }}
        />
      )}
    </div>
  );
}

function FreigabeForm({ name, setName, rolle, setRolle, bestaetigt, setBestaetigt, busy, fehler, onAbbrechen, onSubmit }) {
  const kannSenden = name.trim() && rolle.trim() && bestaetigt && !busy;
  return (
    <div className="mt-2 w-full">
      <div className="grid grid-cols-2 gap-x-3">
        <Field label="Name">
          <input className={inputCls} style={{ borderColor: HAIRLINE }} value={name} onChange={(e) => setName(e.target.value)} />
        </Field>
        <Field label="Rolle">
          <input className={inputCls} style={{ borderColor: HAIRLINE }} value={rolle} onChange={(e) => setRolle(e.target.value)} />
        </Field>
      </div>
      <label className="mt-2 flex items-start gap-2 text-[11px]" style={{ color: SLATE }}>
        <input type="checkbox" className="mt-0.5" checked={bestaetigt} onChange={(e) => setBestaetigt(e.target.checked)} />
        Ich habe dieses Dokument in der aktuellen Fassung geprüft und gebe es hiermit frei. Datum und Uhrzeit werden automatisch vom Server eingetragen.
      </label>
      {fehler && (
        <p className="mt-1.5 whitespace-pre-line text-[11px] font-medium" style={{ color: "#B93330" }}>{fehler}</p>
      )}
      <div className="mt-2 flex items-center gap-2">
        <button
          disabled={!kannSenden}
          onClick={onSubmit}
          className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:brightness-110 disabled:opacity-50"
          style={{ backgroundColor: PRIMARY }}
        >
          <ShieldCheck size={13} /> {busy ? "wird eingetragen…" : "Freigeben"}
        </button>
        <button onClick={onAbbrechen} className="text-xs font-medium" style={{ color: SLATE_SOFT }}>Abbrechen</button>
      </div>
    </div>
  );
}

function NewDocForm({ documents, onSubmit, onCancel }) {
  const [name, setName] = useState("");
  const [chapter, setChapter] = useState("00");
  const [category, setCategory] = useState("Dokument");

  const previewNr = useMemo(() => nextDocNumber(chapter, documents), [chapter, documents]);
  const duplicate = useMemo(() => findDuplicateName(documents, name), [documents, name]);
  const canSubmit = name.trim().length > 0 && !duplicate;

  return (
    <div>
      <Field label="Dokumenten-Nr. — automatisch vergeben">
        <div
          className="flex items-center justify-between rounded-lg border px-3 py-2 text-sm"
          style={{ borderColor: HAIRLINE, backgroundColor: CANVAS }}
        >
          <span className="font-mono font-bold" style={{ color: PRIMARY_DARK }}>{previewNr}</span>
          <span className="text-[11px]" style={{ color: SLATE_SOFT }}>nächste freie Nr. in diesem Kapitel</span>
        </div>
      </Field>

      <Field label="Dokumentname">
        <input
          autoFocus
          className={inputCls}
          style={{ borderColor: duplicate ? "#C2274B" : HAIRLINE }}
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="z. B. Verfahrensanweisung Wareneingang"
        />
        {duplicate && (
          <p className="mt-1.5 flex items-center gap-1 text-[11px] font-medium" style={{ color: "#B93330" }}>
            <AlertCircle size={12} /> Dieser Name existiert bereits bei {duplicate.docNr} — bitte eindeutig benennen.
          </p>
        )}
      </Field>

      <div className="grid grid-cols-2 gap-x-3">
        <Field label="Kapitel">
          <select className={inputCls} style={{ borderColor: HAIRLINE }} value={chapter} onChange={(e) => setChapter(e.target.value)}>
            {CHAPTERS.map((c) => <option key={c.id} value={c.id}>{c.id} · {c.name}</option>)}
          </select>
        </Field>
        <Field label="Kategorie">
          <select className={inputCls} style={{ borderColor: HAIRLINE }} value={category} onChange={(e) => setCategory(e.target.value)}>
            {["Handbuch", "Verfahrensanweisung", "Arbeitsanweisung", "Formular", "Analyse", "Dokument"].map((c) => <option key={c}>{c}</option>)}
          </select>
        </Field>
      </div>
      <div className="mt-4 flex justify-end gap-2">
        <button onClick={onCancel} className="rounded-lg px-3.5 py-2 text-sm font-medium text-slate-500 hover:bg-slate-100">Abbrechen</button>
        <button
          disabled={!canSubmit}
          onClick={() => onSubmit({ name, chapter, category })}
          className="rounded-lg px-4 py-2 text-sm font-semibold text-white shadow-sm disabled:opacity-40"
          style={{ backgroundColor: PRIMARY }}
        >
          Anlegen — {previewNr}
        </button>
      </div>
    </div>
  );
}

function NewVersionForm({ current, onSubmit, onCancel }) {
  const [version, setVersion] = useState(bumpVersion(current?.version));
  const [date, setDate] = useState("");
  const [changedBy, setChangedBy] = useState("");
  const [description, setDescription] = useState("");
  const [file, setFile] = useState(null);

  // Dieser Weg setzt ein Dokument auf "gültig" und schiebt die bisherige
  // Fassung ins Archiv. Bis zum 18.08.2026 war dabei ALLES außer der
  // Versionsnummer freiwillig — Datum, Freigeber und Datei. Genutzt ohne
  // Datei entstand ein freigegebenes Dokument, hinter dem nichts lag: Die
  // alte Datei war archiviert, eine neue gab es nicht, und im Register
  // stand trotzdem "gültig" (so geschehen bei BA-QM-05-01). Für ein
  // gelenktes QM-System ist das der schlimmste der möglichen Zustände,
  // weil das Register etwas behauptet, was das Dokument nicht hergibt.
  // Deshalb sind alle vier Angaben jetzt Pflicht.
  const fehlt = [];
  if (!version.trim()) fehlt.push("Versionsnummer");
  if (!date) fehlt.push("Freigabedatum");
  if (!changedBy.trim()) fehlt.push("Freigegeben von");
  if (!file) fehlt.push("die neue Dateiversion");

  return (
    <div>
      <p className="mb-4 rounded-xl p-3 text-xs" style={{ backgroundColor: PRIMARY_TINT, color: PRIMARY_DARK }}>
        Die aktuelle Version <strong>{current?.version}</strong> wird automatisch in die Änderungshistorie
        verschoben (Status „ersetzt"), die neue Version wird als „gültig" gesetzt. Die zugehörige Datei wird
        dabei automatisch von {STATUS[current?.status]?.label || "—"} nach „Ersetzt" verschoben.
      </p>
      <p className="mb-4 rounded-xl p-3 text-xs" style={{ backgroundColor: "#FBF2E6", color: "#8A5A10" }}>
        Dieser Weg ist für eine <strong>inhaltlich geänderte</strong> Fassung. Soll das Dokument unverändert
        freigegeben werden, nutze im Dokument den Knopf „Jetzt digital unterschreiben“ — der trägt Name und
        Zeitstempel in das Dokument selbst ein.
      </p>
      <Field label="Neue Version">
        <input className={inputCls} style={{ borderColor: HAIRLINE }} value={version} onChange={(e) => setVersion(e.target.value)} />
      </Field>
      <Field label="Freigabedatum">
        <input type="date" className={inputCls} style={{ borderColor: HAIRLINE }} value={date} onChange={(e) => setDate(e.target.value)} />
      </Field>
      <Field label="Freigegeben von">
        <input className={inputCls} style={{ borderColor: HAIRLINE }} value={changedBy} onChange={(e) => setChangedBy(e.target.value)} />
      </Field>
      <Field label="Beschreibung der Änderung">
        <textarea className={inputCls} style={{ borderColor: HAIRLINE }} rows={2} value={description} onChange={(e) => setDescription(e.target.value)} />
      </Field>
      <Field label="Neue Dateiversion">
        <input
          type="file"
          className="block w-full text-xs"
          onChange={(e) => setFile(e.target.files && e.target.files[0])}
        />
      </Field>
      {fehlt.length > 0 && (
        <p className="mt-1 text-[11px] font-medium" style={{ color: "#B93330" }}>
          Für eine Freigabe fehlt noch: {fehlt.join(", ")}.
        </p>
      )}
      <div className="mt-4 flex justify-end gap-2">
        <button onClick={onCancel} className="rounded-lg px-3.5 py-2 text-sm font-medium text-slate-500 hover:bg-slate-100">Abbrechen</button>
        <button
          disabled={fehlt.length > 0}
          onClick={() => onSubmit({ version, date, changedBy, description, file })}
          className="rounded-lg px-4 py-2 text-sm font-semibold text-white shadow-sm disabled:opacity-40"
          style={{ backgroundColor: PRIMARY }}
        >
          Freigeben &amp; archivieren
        </button>
      </div>
    </div>
  );
}

function bumpVersion(v) {
  if (!v) return "1.0";
  const parts = v.split(".").map(Number);
  if (parts.length === 2 && !isNaN(parts[1])) return `${parts[0]}.${parts[1] + 1}`;
  return v;
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
