/* ShiftBud — shared React kit: icons, store, components.  Reads window.SB. */
/* eslint-disable */
const { useState, useEffect, useRef, useCallback } = React;
const SBD = window.SB;
const T = (k, p) => window.SBI.t(k, p);
const DOW = (d) => window.SBI.dow(d);

/* ----------------------------------------------------------------
   Icons
---------------------------------------------------------------- */
const ICONS = {
  calendar: "M8 2v3M16 2v3M3.5 9.5h17M5 4.5h14a1.5 1.5 0 0 1 1.5 1.5V19A1.5 1.5 0 0 1 19 20.5H5A1.5 1.5 0 0 1 3.5 19V6A1.5 1.5 0 0 1 5 4.5Z",
  home: "M4 11.5 12 4l8 7.5M6 10v9.5h12V10",
  users: "M16 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 17.5V19M10 10.5A3.25 3.25 0 1 0 10 4a3.25 3.25 0 0 0 0 6.5ZM20 19v-1.5a3.5 3.5 0 0 0-2.6-3.4M15.5 4.2a3.25 3.25 0 0 1 0 6.1",
  chat: "M20 11.5a7.5 7.5 0 0 1-10.9 6.7L4 19.5l1.3-4.1A7.5 7.5 0 1 1 20 11.5Z",
  check: "M4 12.5 9 17.5 20 6.5",
  plus: "M12 5v14M5 12h14",
  bell: "M18 8.5a6 6 0 1 0-12 0c0 7-2.5 8.5-2.5 8.5h17S18 15.5 18 8.5ZM10.5 21a2 2 0 0 0 3 0",
  swap: "M7 4 4 7l3 3M4 7h11a4 4 0 0 1 0 8M17 20l3-3-3-3M20 17H9a4 4 0 0 1 0-8",
  alert: "M12 4 2.5 20h19L12 4ZM12 10v4M12 17.5h.01",
  clock: "M12 7v5l3.5 2M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
  x: "M6 6l12 12M18 6 6 18",
  arrowR: "M5 12h14M13 6l6 6-6 6",
  search: "M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14ZM20 20l-4-4",
  settings: "M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM19.4 13a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2V21a2 2 0 0 1-4 0v-.1A1.7 1.7 0 0 0 7 19.2l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1A1.7 1.7 0 0 0 5.4 13H5a2 2 0 0 1 0-4h.1A1.7 1.7 0 0 0 7 4.8L6.9 4.7a2 2 0 1 1 2.8-2.8l.1.1A1.7 1.7 0 0 0 12 3.1V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 2.9 1.2l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0 .4 2.8h.4a2 2 0 0 1 0 4H21a1.7 1.7 0 0 0-1.6 1Z",
  phone: "M21 16.5v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 1 3.7 2 2 0 0 1 3 1.5h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.7a2 2 0 0 1-.5 2.1L7 9.3a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.7.7a2 2 0 0 1 1.7 2Z",
  leaf: "M11 20.5C6 20.5 3.5 16 3.5 11.5 3.5 6 8 3.5 20.5 3.5c0 12.5-2.5 17-9.5 17ZM4 20 13 11",
  grid: "M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z",
  trash: "M5 7h14M10 7V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2M6.5 7l.8 12a1 1 0 0 0 1 .9h7.4a1 1 0 0 0 1-.9l.8-12",
  userplus: "M14 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-4A3.5 3.5 0 0 0 3 17.5V19M8.5 10.5a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM18 8v6M21 11h-6",
  moon: "M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5Z",
  sun: "M12 4V2M12 22v-2M6 6 4.5 4.5M19.5 19.5 18 18M4 12H2M22 12h-2M6 18l-1.5 1.5M19.5 4.5 18 6M12 16.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9Z",
  lock: "M7 11V8a5 5 0 0 1 10 0v3M6 11h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z",
  user: "M5 20v-1a5 5 0 0 1 5-5h4a5 5 0 0 1 5 5v1M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z",
  logout: "M15 17l5-5-5-5M20 12H9M9 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h3",
  chevR: "M9 6l6 6-6 6",
  help: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z",
  globe: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.5 12h17M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18",
  sparkle: "M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3Z",
  billing: "M2 7.5A2.5 2.5 0 0 1 4.5 5h15A2.5 2.5 0 0 1 22 7.5v9a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 16.5v-9ZM2 10h20M6 14h4",
};
function Icon({ name, style, className }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"
      strokeLinecap="round" strokeLinejoin="round" style={style} className={className}>
      <path d={ICONS[name]} />
    </svg>
  );
}

/* ----------------------------------------------------------------
   Store
---------------------------------------------------------------- */
function useShiftBud() {
  // ── Live data (from API) ───────────────────────────────────────────────────
  const [shifts, setShifts] = useState([]);
  const [broadcastShiftIds, setBroadcastShiftIds] = useState(new Set());
  const [tasks, setTasks]   = useState([]);
  const [alerts, setAlerts] = useState([]);
  const [dismissedAlertIds, setDismissedAlertIds] = useState(new Set());
  const [messages]          = useState(() => SBD.MESSAGES.map((m) => ({ ...m })));
  const [toasts, setToasts] = useState([]);
  const [weekOffset, setWeekOffset]   = useState(0);
  const [categories, setCategories]   = useState(() => SBD.CATEGORIES.map((c) => ({ ...c })));
  const [unavail, setUnavail]         = useState(() => SBD.UNAVAIL.map((u) => ({ ...u })));
  const [weekdayHours, setWeekdayHours] = useState(() => {
    const o = {}; SBD.DOW_KEYS.forEach((d) => { o[d] = (SBD.WEEKDAY_HOURS[d] || []).map((b) => ({ ...b })); }); return o;
  });
  const [staffV, setStaffV]           = useState(0);
  const [billing, setBilling]         = useState(null);
  const [loading, setLoading]         = useState(false);
  const [theme, setThemeState]        = useState(() => { try { return localStorage.getItem("sb_theme") || "light"; } catch (e) { return "light"; } });
  const [mondaySchedule, setMondaySchedule] = useState(true);
  const [restaurantName, setRestaurantNameState] = useState(SBD.RESTAURANT.name);
  const [ownerName, setOwnerName] = useState("");
  const [ownerEmail, setOwnerEmail] = useState("");
  const setRestaurantName = useCallback((name) => {
    setRestaurantNameState(name);
  }, []);
  const [authed, setAuthed]           = useState(false);
  const [authChecked, setAuthChecked] = useState(false);

  const unavailRef = useRef(unavail);
  useEffect(() => { unavailRef.current = unavail; }, [unavail]);
  const nextId = useRef(9000);

  // ── Auth ──────────────────────────────────────────────────────────────────
  // Check session on mount
  useEffect(() => {
    SBAPI.checkAuth().then(({ authed: a, owner }) => {
      setAuthed(a);
      setAuthChecked(true);
      if (owner) {
        if (owner.business_name) setRestaurantName(owner.business_name);
        if (owner.name) setOwnerName(owner.name);
        if (owner.email) setOwnerEmail(owner.email);
      }
    });
  }, []);

  const login = useCallback(async (email, password) => {
    const result = await SBAPI.login(email, password);
    setAuthed(true);
    if (result) {
      if (result.business_name) setRestaurantName(result.business_name);
      if (result.name) setOwnerName(result.name);
      if (result.email) setOwnerEmail(result.email);
    }
    return result;
  }, []);

  const logout = useCallback(async () => {
    await SBAPI.logout();
    setAuthed(false);
    setShifts([]);
    setTasks([]);
    // Keep SBD.STAFF intact as fallback — cleared on next loadStaff()
  }, []);

  // ── Load data when authenticated ──────────────────────────────────────────
  const loadStaff = useCallback(async () => {
    try {
      const list = await SBAPI.fetchStaff();
      // Sync into SBD.STAFF so existing UI helpers (staffById etc.) still work
      SBD.STAFF.splice(0, SBD.STAFF.length, ...list);
      setStaffV(v => v + 1);
    } catch (e) { console.error("loadStaff", e); }
  }, []);

  const loadShifts = useCallback(async (offset) => {
    try {
      const list = await SBAPI.fetchShiftsForWeek(offset);
      setShifts(prev => {
        const other = prev.filter(s => s.week !== offset);
        return [...other, ...list];
      });
    } catch (e) { console.error("loadShifts", e); }
  }, []);

  // Alerts reaktiv aus Schichten ableiten
  useEffect(() => {
    const derived = shifts
      .filter(s => ["absent","declined","swap_requested"].includes(s.status))
      .filter(s => !dismissedAlertIds.has(s.id))
      .map(s => {
        const staff = SBD.staffById(s.staffId);
        const time = SBD.fmtRange(s.start, s.end);
        const day = SBD.WEEK[s.day] || { dow: "Mon", num: "" };
        return {
          id: s.id,
          kind: s.status === "swap_requested" ? "swap" : "absence",
          staffId: s.staffId,
          shiftId: s.id,
          day: s.day,
          time,
          reason: s.notes || (s.status === "absent" ? "Krank gemeldet" : s.status === "declined" ? "Abgelehnt" : "Tauschanfrage"),
          at: `${SBD.WEEK[s.day]?.dow || ""} ${time}`,
          urgent: s.status === "absent",
        };
      });
    setAlerts(derived);
  }, [shifts, dismissedAlertIds]);

  const loadTasks = useCallback(async () => {
    try {
      const list = await SBAPI.fetchTasks();
      setTasks(list);
    } catch (e) { console.error("loadTasks", e); }
  }, []);

  const loadOperatingHours = useCallback(async () => {
    try {
      const hours = await SBAPI.fetchOperatingHours();
      setWeekdayHours(hours);
      // Kalender-Range aus echten Betriebszeiten ableiten
      const allBlocks = Object.values(hours).flat();
      if (allBlocks.length > 0) {
        SBD.RESTAURANT.openHour  = Math.min(...allBlocks.map(b => b.start));
        SBD.RESTAURANT.closeHour = Math.max(...allBlocks.map(b => b.end));
      } else {
        SBD.RESTAURANT.openHour  = 0;
        SBD.RESTAURANT.closeHour = 24;
      }
    } catch (e) { console.error("loadOperatingHours", e); }
  }, []);

  const loadCategories = useCallback(async () => {
    try {
      const list = await SBAPI.fetchCategories();
      if (list && list.length > 0) {
        setCategories(list);
      } else {
        // Erste Anmeldung: Demo-Kategorien einmalig in DB speichern
        const defaults = SBD.CATEGORIES.filter(c => c.label);
        const saved = await Promise.all(defaults.map(c => SBAPI.createCategory(c.label)));
        setCategories(saved);
      }
    } catch (e) { console.error("loadCategories", e); }
  }, []);

  const loadBilling = useCallback(async () => {
    try {
      const data = await fetch("/api/billing/status", { credentials: "include" });
      if (data.ok) setBilling(await data.json());
    } catch (e) { console.error("loadBilling", e); }
  }, []);

  const [holidays, setHolidays] = useState([]);
  const loadHolidays = useCallback(async () => {
    try {
      const list = await SBAPI.fetchHolidays();
      setHolidays(list || []);
    } catch (e) { console.error("loadHolidays", e); }
  }, []);
  const saveHolidays = useCallback(async (list) => {
    try {
      const saved = await SBAPI.saveHolidays(list);
      setHolidays(saved);
    } catch (e) { console.error("saveHolidays", e); }
  }, []);

  const [generalMsgs, setGeneralMsgs] = useState([]);
  const loadGeneralMsgs = useCallback(async () => {
    try {
      const r = await fetch("/api/messages/general", { credentials: "include" });
      if (r.ok) setGeneralMsgs(await r.json());
    } catch (e) { console.error("loadGeneralMsgs", e); }
  }, []);
  const dismissGeneralMsg = useCallback(async (id) => {
    setGeneralMsgs((m) => m.filter((x) => x.id !== id));
    await fetch(`/api/messages/general/${id}/read`, { method: "POST", credentials: "include" });
  }, []);

  useEffect(() => {
    if (!authed) return;
    loadStaff();
    loadShifts(0);
    loadTasks();
    loadCategories();
    loadOperatingHours();
    loadBilling();
    loadGeneralMsgs();
    loadHolidays();
  }, [authed, loadStaff, loadShifts, loadTasks, loadOperatingHours, loadBilling, loadGeneralMsgs]);

  // ── Week navigation: load from API on change ──────────────────────────────
  const gotoWeek = useCallback((offset) => {
    setWeekOffset(offset);
    loadShifts(offset);
  }, [loadShifts]);

  // ── Theme ──────────────────────────────────────────────────────────────────
  const isBlocked = useCallback((staffId, day, start, end) => {
    return unavailRef.current.find((u) => u.staffId === staffId && u.day === day && start < u.end && u.start < end) || null;
  }, []);
  const hasOverlap = useCallback((staffId, day, start, end, excludeId = null) => {
    return shifts.find((s) =>
      s.staffId === staffId &&
      s.day === day &&
      s.week === weekOffset &&
      s.status !== "cancelled" &&
      s.id !== excludeId &&
      start < s.end && s.start < end
    ) || null;
  }, [shifts, weekOffset]);
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    try { localStorage.setItem("sb_theme", theme); } catch (e) {}
  }, [theme]);
  const setTheme = useCallback((t) => setThemeState(t), []);
  const toggleTheme = useCallback(() => setThemeState((t) => (t === "dark" ? "light" : "dark")), []);
  useEffect(() => { window.__SB_CATS = categories; }, [categories]);

  const pushToast = useCallback((text, icon = "check") => {
    const id = Date.now() + Math.random();
    setToasts((t) => [...t, { id, text, icon }]);
    setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2900);
  }, []);

  const confirmShift = useCallback(async (id) => {
    setShifts((arr) => arr.map((s) => s.id === id ? { ...s, status: "confirmed", draft: false } : s));
    try {
      await SBAPI.patchShift(id, { status: "confirmed" });
      pushToast(T("t_confirm"));
    } catch (e) {
      loadShifts(weekOffset);
      pushToast("Fehler beim Bestätigen", "alert");
    }
  }, [pushToast, weekOffset, loadShifts]);

  const coverShift = useCallback(async (shiftId, newStaffId, alertId) => {
    // Broadcast-Modus zurücksetzen + Mitarbeiter zuweisen
    setShifts((arr) => arr.map((s) => {
      if (s.id !== shiftId) return s;
      return { ...s, staffId: newStaffId, status: "draft", draft: true, broadcastMode: false, broadcastAsked: 0 };
    }));
    if (alertId != null) setAlerts((a) => a.filter((x) => x.id !== alertId));
    const who = SBD.staffById(newStaffId) || _stub(newStaffId);
    try {
      await SBAPI.patchShift(shiftId, { staff_id: newStaffId, status: "draft", broadcast_mode: false });
      pushToast(T("t_cover", { name: who.name.split(" ")[0] }), "swap");
    } catch (e) {
      loadShifts(weekOffset);
      pushToast("Fehler beim Zuweisen", "alert");
    }
  }, [pushToast, weekOffset, loadShifts]);

  const deleteWeek = useCallback(async (weekOffset) => {
    try {
      await SBAPI.deleteWeekShifts(weekOffset);
      setShifts((arr) => arr.filter((s) => s.week !== weekOffset));
      pushToast("Alle Schichten der Woche gelöscht", "trash");
    } catch (e) {
      pushToast("Fehler beim Löschen der Woche", "alert");
    }
  }, [pushToast]);

  const deleteShiftSilent = useCallback(async (id, notify) => {
    let nm = "";
    setShifts((arr) => { const s = arr.find((x) => x.id === id); if (s) { const st = SBD.staffById(s.staffId); nm = st ? st.name.split(" ")[0] : "Staff"; } return arr.filter((x) => x.id !== id); });
    try { await SBAPI.deleteShift(id); } catch (_) {}
    if (notify) pushToast(T("t_deleted_notified", { name: nm }), "chat");
    else pushToast(T("t_deleted"), "trash");
  }, [pushToast]);

  const dismissAlert = useCallback((alertId, msg) => {
    setDismissedAlertIds(s => new Set([...s, alertId]));
    pushToast(msg || T("t_dismiss"));
  }, [pushToast]);

  const swapShifts = useCallback(async (reqShiftId, toShiftId, alertId) => {
    let aStaffId, bStaffId;
    setShifts((arr) => {
      const a = arr.find((s) => s.id === reqShiftId);
      const b = arr.find((s) => s.id === toShiftId);
      if (!a || !b) return arr;
      aStaffId = a.staffId; bStaffId = b.staffId;
      return arr.map((s) => {
        if (s.id === reqShiftId) return { ...s, staffId: bStaffId, status: "confirmed", notes: undefined };
        if (s.id === toShiftId) return { ...s, staffId: aStaffId, status: "confirmed", notes: undefined };
        return s;
      });
    });
    if (alertId != null) setAlerts((a) => a.filter((x) => x.id !== alertId));
    try {
      await Promise.all([
        SBAPI.patchShift(reqShiftId, { staff_id: bStaffId, status: "confirmed" }),
        SBAPI.patchShift(toShiftId,  { staff_id: aStaffId, status: "confirmed" }),
      ]);
      pushToast(T("t_swapped"), "swap");
    } catch (e) {
      loadShifts(weekOffset);
      pushToast("Fehler beim Tauschen", "alert");
    }
  }, [pushToast, weekOffset, loadShifts]);

  const declineSwap = useCallback(async (alertId, staffId, shiftId) => {
    setShifts((arr) => arr.map((s) => s.id === shiftId ? { ...s, status: "confirmed", notes: undefined } : s));
    setAlerts((a) => a.filter((x) => x.id !== alertId));
    const who = SBD.staffById(staffId);
    try {
      await SBAPI.patchShift(shiftId, { status: "confirmed" });
      pushToast(T("t_swap_declined", { name: who ? who.name.split(" ")[0] : "" }));
    } catch (e) {
      loadShifts(weekOffset);
      pushToast("Fehler", "alert");
    }
  }, [pushToast, weekOffset, loadShifts]);

  const toggleTask = useCallback(async (id) => {
    setTasks(arr => arr.map(t => t.id === id ? { ...t, done: true } : t)); // optimistic
    try {
      await SBAPI.completeTask(id);
    } catch (e) {
      loadTasks(); // revert
    }
  }, [loadTasks]);

  const addShift = useCallback(async (data) => {
    const who = SBD.staffById(data.staffId);
    const nm = who ? who.name.split(" ")[0] : "staff";
    try {
      const created = await SBAPI.createShift(
        data.week != null ? data.week : weekOffset,
        data.day, data.start, data.end, data.staffId, data.notes
      );
      setShifts(arr => [...arr, created]);
      pushToast(T("t_added_draft", { name: nm }), "calendar");
    } catch (e) {
      console.error("createShift error:", e.status, e.message, e);
      if (e.status === 409) pushToast("⚠️ Overlap: shift not created", "alert");
      else pushToast(`Error ${e.status || "?"}: ${e.message || "shift not created"}`, "alert");
    }
  }, [pushToast, weekOffset]);

  const deleteShift = useCallback(async (id) => {
    setShifts(arr => arr.filter(s => s.id !== id)); // optimistic
    try {
      await SBAPI.deleteShift(id);
      pushToast(T("t_deleted"), "trash");
    } catch (e) {
      loadShifts(weekOffset); // revert on error
      pushToast("Error deleting shift", "alert");
    }
  }, [pushToast, weekOffset, loadShifts]);

  const sendShift = useCallback(async (id) => {
    try {
      const updated = await SBAPI.publishShift(id);
      setShifts((arr) => arr.map((s) => s.id === id ? updated : s));
      const who = SBD.staffById(updated.staffId);
      pushToast(T("t_sent_one", { name: who ? who.name.split(" ")[0] : "" }), "chat");
    } catch (e) {
      if (e.status === 409) pushToast("Schicht wurde bereits gesendet", "alert");
      else pushToast("Fehler beim Senden", "alert");
    }
  }, [pushToast]);

  const sendAllDrafts = useCallback(async (week) => {
    try {
      const result = await SBAPI.publishWeek(week);
      await loadShifts(week);
      if (result.skipped_uninvited > 0) {
        pushToast(`${result.published} gesendet · ${result.skipped_uninvited} übersprungen (nicht eingeladen)`, "alert");
      } else {
        pushToast(T("t_sent_all", { n: result.published }), "chat");
      }
    } catch (e) {
      pushToast("Error publishing week", "alert");
    }
  }, [pushToast, loadShifts]);

  const copyWeek = useCallback(async () => {
    try {
      // Always copy from the current week (sourceOffset=0) into the displayed week
      const created = await SBAPI.copyLastWeek(weekOffset, 0);
      setShifts((prev) => [...prev.filter((s) => s.week !== weekOffset), ...created]);
      pushToast(T("t_copied", { n: created.length }), "calendar");
    } catch (e) {
      pushToast("Fehler beim Kopieren", "alert");
    }
  }, [weekOffset, pushToast]);

  // gotoWeek defined above (uses loadShifts from API)

  const moveShift = useCallback(async (id, day, start) => {
    let endHour = null;
    setShifts((arr) => arr.map((s) => {
      if (s.id !== id) return s;
      const dur = s.end - s.start;
      endHour = start + dur;
      return { ...s, day, start, end: endHour };
    }));
    if (endHour === null) return;
    try {
      await SBAPI.updateShift(id, weekOffset, day, start, endHour);
    } catch (e) {
      loadShifts(weekOffset);
      pushToast("Error moving shift", "alert");
    }
  }, [weekOffset, loadShifts, pushToast]);

  const addStaff = useCallback(async (data) => {
    try {
      const created = await SBAPI.createStaff(data.name, data.phone || "", data.role || "staff");
      SBD.STAFF.push(created);
      setStaffV(v => v + 1);
      pushToast(T("t_staffadd", { name: created.name.split(" ")[0] }), "users");
    } catch (e) {
      if (e.status === 409) pushToast("Phone already registered", "alert");
      else pushToast("Error adding staff", "alert");
    }
  }, [pushToast]);

  const inviteStaff = useCallback(async (id) => {
    try {
      const updated = await SBAPI.inviteStaff(id);
      const idx = SBD.STAFF.findIndex(s => s.id === id);
      if (idx >= 0) SBD.STAFF[idx] = updated;
      setStaffV(v => v + 1);
      pushToast(`✅ Einladung an ${updated.name.split(" ")[0]} gesendet`, "chat");
      return true;   // ← Erfolg
    } catch (e) {
      pushToast("Fehler beim Einladen: " + (e.message || ""), "alert");
      return false;  // ← Fehler — bleibt uneingeladen
    }
  }, [pushToast]);

  const removeStaff = useCallback(async (id) => {
    const member = SBD.staffById(id);
    const name = member ? member.name.split(" ")[0] : "Staff";
    try {
      await SBAPI.deactivateStaff(id);
      const idx = SBD.STAFF.findIndex(s => s.id === id);
      if (idx >= 0) SBD.STAFF.splice(idx, 1);
      setShifts(arr => arr.filter(s => s.staffId !== id));
      setTasks(arr => arr.filter(t => t.staffId !== id));
      setAlerts(arr => arr.filter(a => a.staffId !== id));
      setStaffV(v => v + 1);
      pushToast(T("t_staffdel", { name }));
    } catch (e) {
      pushToast("Error removing staff", "alert");
    }
  }, [pushToast]);

  // categories — persisted to DB
  const addCategory = useCallback(async (label) => {
    try {
      const created = await SBAPI.createCategory(label);
      setCategories((arr) => [...arr, created]);
    } catch (e) { console.error("addCategory", e); }
  }, []);
  const removeCategory = useCallback(async (id) => {
    // Demo-Kategorien haben String-IDs (z.B. "kueche") — nur lokal entfernen
    if (!isNaN(Number(id))) {
      try { await SBAPI.deleteCategory(id); } catch (e) { console.error("removeCategory", e); return; }
    }
    setCategories((arr) => arr.filter((c) => c.id !== id));
    setShifts((arr) => arr.map((s) => (s.category === id ? { ...s, category: null } : s)));
  }, []);
  const renameCategory = useCallback(async (id, label) => {
    try {
      await SBAPI.renameCategory(id, label);
      setCategories((arr) => arr.map((c) => (c.id === id ? { ...c, label } : c)));
    } catch (e) { console.error("renameCategory", e); }
  }, []);

  // weekday hours editing — each change is persisted immediately
  const _saveHours = useCallback((newHours) => {
    SBAPI.saveOperatingHours(newHours).catch((e) => console.error("saveOperatingHours", e));
  }, []);
  const setDayBlock = useCallback((dow, idx, patch) => {
    setWeekdayHours((h) => {
      const next = { ...h, [dow]: h[dow].map((b, i) => (i === idx ? { ...b, ...patch } : b)) };
      _saveHours(next); return next;
    });
  }, [_saveHours]);
  const addDayBlock = useCallback((dow) => {
    setWeekdayHours((h) => {
      const next = { ...h, [dow]: [...h[dow], { start: 12, end: 18 }] };
      _saveHours(next); return next;
    });
  }, [_saveHours]);
  const removeDayBlock = useCallback((dow, idx) => {
    setWeekdayHours((h) => {
      const next = { ...h, [dow]: h[dow].filter((_, i) => i !== idx) };
      _saveHours(next); return next;
    });
  }, [_saveHours]);

  const addUnavail = useCallback((data) => {
    setUnavail((arr) => [...arr, { id: Date.now(), ...data }]);
  }, []);
  const removeUnavail = useCallback((id) => {
    setUnavail((arr) => arr.filter((u) => u.id !== id));
  }, []);

  return { shifts, tasks, alerts, messages, toasts, staffV, weekOffset, categories, weekdayHours, pushToast,
    holidays, loadHolidays, saveHolidays,
    inviteStaff,
    theme, setTheme, toggleTheme, mondaySchedule, setMondaySchedule,
    restaurantName, setRestaurantName, ownerName, ownerEmail, authed, authChecked, loading, login, logout,
    confirmShift, coverShift, dismissAlert, toggleTask, addShift, moveShift, addStaff, removeStaff,
    deleteShift, deleteShiftSilent, deleteWeek, sendShift, sendAllDrafts, copyWeek, gotoWeek, swapShifts, declineSwap, isBlocked, hasOverlap,
    addCategory, removeCategory, renameCategory, setDayBlock, addDayBlock, removeDayBlock,
    unavail, addUnavail, removeUnavail, billing, loadBilling,
    generalMsgs, dismissGeneralMsg, loadGeneralMsgs, loadShifts,
    broadcastShiftIds, setBroadcastShiftIds };
}

/* ----------------------------------------------------------------
   Primitives
---------------------------------------------------------------- */
function Avatar({ staff, size = 36, dot = false, mono = false }) {
  if (!staff) return null;
  return (
    <span className="avatar" style={{ "--sz": size + "px", background: mono ? "#465a4f" : staff.color }} title={staff.name}>
      {SBD.initials(staff.name)}
      {dot && <span className="on-dot" />}
    </span>
  );
}
function AvatarStack({ ids, max = 5, size = 30, mono = false }) {
  const shown = ids.slice(0, max);
  const extra = ids.length - shown.length;
  return (
    <div className="avatar-stack">
      {shown.map((id) => <Avatar key={id} staff={SBD.staffById(id) || _stub(id)} size={size} mono={mono} />)}
      {extra > 0 && (
        <span className="avatar" style={{ "--sz": size + "px", background: "var(--bg-deep)", color: "var(--muted)" }}>+{extra}</span>
      )}
    </div>
  );
}
const STATUS_LABEL = { asking: "st_asking", pending: "st_pending", confirmed: "st_confirmed", swap_requested: "st_swap", cancelled: "st_absent", covered: "st_covered" };
function StatusBadge({ status }) {
  return <span className={"badge st-" + status}><span className="dot" />{T(STATUS_LABEL[status]) || status}</span>;
}
function CoverageBar({ pct }) {
  const cls = pct >= 85 ? "" : pct >= 65 ? "warn" : "bad";
  return <div className="cov-track"><div className={"cov-fill " + cls} style={{ width: pct + "%" }} /></div>;
}
function Stat({ value, label, sub, subColor }) {
  return (
    <div className="stat">
      <div className="s-val">{value}</div>
      <div className="s-lab">{label}</div>
      {sub && <div className="s-sub" style={{ color: subColor || "var(--green)" }}>{sub}</div>}
    </div>
  );
}

/* ----------------------------------------------------------------
   Week timeline — lane-packed, optional drag, status or staff colors
---------------------------------------------------------------- */
const TONE_BG = { green: "var(--st-green)", orange: "var(--st-orange)", red: "var(--st-red)" };
const CONFLICT_STATUSES = new Set(["cancelled", "absent", "declined", "swap_requested"]);
function shiftTone(s, all) {
  if (CONFLICT_STATUSES.has(s.status)) return "red";
  if (s.status === "confirmed") return "green";
  const clash = all.some((o) => o.id !== s.id && o.staffId === s.staffId && o.day === s.day && !CONFLICT_STATUSES.has(o.status) && s.start < o.end && o.start < s.end);
  if (clash) return "red";
  if (s.draft || s.status === "pending" || s.status === "asking") return "orange";
  return "green";
}
function computeStack(dayShifts) {
  // full-width bars stacked by start (later on top); same-start ties peek sideways
  const sorted = [...dayShifts].sort((a, b) => a.start - b.start || a.end - b.end);
  const info = {}; const tie = {};
  sorted.forEach((s, i) => {
    const tieIdx = tie[s.start] || 0; tie[s.start] = tieIdx + 1;
    const multi = sorted.some((o) => o.id !== s.id && s.start < o.end && o.start < s.end);
    info[s.id] = { z: 4 + i, tieIdx, multi };
  });
  return info;
}
const TIE_PEEK = 24; // px nudge for shifts that start at the exact same time
function WeekTimeline({ shifts, onShiftClick, draggable = false, onMove, height = 380, showAxis = true, stack = false, colorMode = "staff", nowHour = null }) {
  const open = SBD.RESTAURANT.openHour, close = SBD.RESTAURANT.closeHour, span = close - open;
  const tracksRef = useRef(null);
  const [drag, setDrag] = useState(null); // {id, day, start, dur}
  const [focus, setFocus] = useState(null); // shiftId brought to front

  const hourMarks = [];
  for (let h = open; h <= close; h += 2) hourMarks.push(h);

  const toY = (h) => ((h - open) / span) * height;
  const focusShift = focus != null ? shifts.find((s) => s.id === focus) : null;

  const beginDrag = (e, s) => {
    if (!draggable) return;
    e.preventDefault();
    setDrag({ id: s.id, day: s.day, start: s.start, dur: s.end - s.start });
  };

  useEffect(() => {
    if (!drag) return;
    const onMoveEvt = (e) => {
      const rect = tracksRef.current.getBoundingClientRect();
      const colW = rect.width / 7;
      let day = Math.floor((e.clientX - rect.left) / colW);
      day = Math.max(0, Math.min(6, day));
      let start = open + ((e.clientY - rect.top) / rect.height) * span;
      start = Math.round(start * 2) / 2; // snap 30 min
      start = Math.max(open, Math.min(close - drag.dur, start));
      setDrag((d) => ({ ...d, day, start }));
    };
    const onUp = () => {
      setDrag((d) => { if (d && onMove) onMove(d.id, d.day, d.start); return null; });
    };
    window.addEventListener("pointermove", onMoveEvt);
    window.addEventListener("pointerup", onUp);
    return () => { window.removeEventListener("pointermove", onMoveEvt); window.removeEventListener("pointerup", onUp); };
  }, [drag, onMove, open, close, span]);

  const renderBar = (s, opts = {}) => {
    const { ghost = false, z = 4, tieIdx = 0, multi = false } = opts;
    const staff = SBD.staffById(s.staffId) || { id: s.staffId, name: "?", role: "", color: "#aaa" };
    const top = toY(s.start), h = toY(s.end) - toY(s.start);
    const pending = s.status === "pending";
    const cancelled = CONFLICT_STATUSES.has(s.status);
    const swap = s.status === "swap_requested";
    const statusMode = colorMode === "status";
    const tone = statusMode ? shiftTone(s, shifts) : null;
    const focused = focus === s.id;
    const dim = !ghost && focusShift && !focused && focusShift.day === s.day &&
      s.start < focusShift.end && focusShift.start < s.end;
    const style = {
      top: top, height: Math.max(h, 22),
      background: statusMode ? TONE_BG[tone] : (cancelled ? "#cfccc0" : staff.color),
      opacity: dim ? 0.34 : ((!statusMode && pending) ? 0.62 : 1),
      borderStyle: (!statusMode && pending) ? "dashed" : "solid",
      borderColor: "rgba(0,0,0,.06)",
    };
    // full-width bars; same-start ties nudge sideways; focused jumps to front
    if (stack && multi && !ghost) {
      style.left = (5 + (focused ? 0 : tieIdx * TIE_PEEK)) + "px";
      style.right = "5px";
      style.zIndex = focused ? 30 : z;
    }
    if (focused && !ghost) { style.outline = "2.5px solid #fff"; style.boxShadow = "var(--shadow)"; }
    if (ghost) { Object.assign(style, { left: 4, right: 4, pointerEvents: "none", zIndex: 60, boxShadow: "var(--shadow-lg)", outline: "2px solid rgba(255,255,255,.75)" }); }
    const showName = focused || !multi;
    const onClick = () => {
      if (drag) return;
      if (stack && multi && !focused) setFocus(s.id);
      else onShiftClick && onShiftClick(s);
    };
    return (
      <div key={s.id} className={"shift-bar" + (cancelled ? " is-cancelled" : "") + (multi && !showName ? " compact" : "")}
        style={style}
        title={`${staff.name} · ${staff.role} · ${SBD.fmtRange(s.start, s.end)}`}
        onPointerDown={(e) => beginDrag(e, s)}
        onClick={onClick}>
        {swap && <span className="sb-flag">⇄</span>}
        {(cancelled || (tone === "red" && !swap)) && <span className="sb-flag">!</span>}
        <div className="sb-name">{showName ? staff.name.split(" ")[0] : SBD.initials(staff.name)}</div>
        {showName && h > 34 && <div className="sb-time">{SBD.fmtRange(s.start, s.end)}</div>}
      </div>
    );
  };

  return (
    <div className="tl" style={{ gridTemplateColumns: (showAxis ? "46px " : "") + "repeat(7, 1fr)" }}>
      {showAxis && (
        <div className="tl-axis" style={{ paddingTop: 47 }}>
          <div style={{ position: "relative", height }}>
            {hourMarks.map((h) => (
              <span key={h} className="hr" style={{ top: toY(h) }}>{SBD.fmtHour(h)}</span>
            ))}
          </div>
        </div>
      )}
      {SBD.WEEK.map((d) => {
        const dayShifts = shifts.filter((s) => s.day === d.index);
        const st = stack ? computeStack(dayShifts) : null;
        return (
          <div key={d.index} className={"tl-col" + (d.isToday ? " is-today" : "")}>
            <div className={"tl-colhead" + (d.isToday ? " is-today" : "")}>
              <div className="dow">{d.dow}</div>
              <div className="dnum">{d.num}{d.isToday && <span className="today-pip" />}</div>
            </div>
            <div className="tl-track" ref={d.index === 0 ? tracksRef : null} style={{ height }}
              onClick={(e) => { if (e.target === e.currentTarget || e.target.classList.contains("gl") || e.target.classList.contains("tl-gridlines")) setFocus(null); }}>
              <div className="tl-gridlines">
                {hourMarks.map((h) => <div key={h} className="gl" style={{ top: toY(h) }} />)}
              </div>
              {nowHour != null && d.isToday && (
                <div className="tl-now" style={{ top: toY(nowHour) }}><span className="tl-now-dot" /></div>
              )}
              {dayShifts.map((s) => (drag && drag.id === s.id) ? null
                : renderBar(s, st ? st[s.id] : {}))}
              {drag && drag.day === d.index && (() => {
                const s = shifts.find((x) => x.id === drag.id);
                return renderBar({ ...s, start: drag.start, end: drag.start + drag.dur }, { ghost: true });
              })()}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ----------------------------------------------------------------
   DayRoster — vertical day stack; one row per person; horizontal time bar
---------------------------------------------------------------- */
function DayRoster({ shifts, weekDays, weekdayHours, categories, onShiftClick, onSend, onConfirm, onDelete, draggable = false, onMove, colorMode = "status", nowHour = null, todayIndex = null, broadcastIds = new Set(), customHolidays = [] }) {
  const open = SBD.RESTAURANT.openHour, close = SBD.RESTAURANT.closeHour, span = close - open;
  const hourMarks = []; for (let h = open; h <= close; h += 2) hourMarks.push(h);
  const toX = (h) => ((h - open) / span) * 100;
  const pd = useRef(null);
  const [drag, setDrag] = useState(null); // {id, start}
  const days = weekDays || SBD.WEEK;
  const catLabel = (id) => { const c = (categories || []).find((x) => x.id === id); return c ? c.label : null; };

  useEffect(() => {
    if (!draggable) return;
    const move = (e) => {
      const p = pd.current; if (!p) return;
      if (!p.moved && Math.abs(e.clientX - p.x0) < 4) return;
      p.moved = true;
      const rect = p.track.getBoundingClientRect();
      let start = open + ((e.clientX - rect.left) / rect.width) * span;
      start = Math.round(start * 2) / 2;
      start = Math.max(open, Math.min(close - p.dur, start));
      p.curStart = start;
      setDrag({ id: p.id, start });
    };
    const up = () => {
      const p = pd.current;
      if (p && p.moved && onMove) onMove(p.id, p.day, p.curStart);
      pd.current = null; setDrag(null);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
    return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
  }, [draggable, onMove, open, close, span]);

  // Hours within operating-hours blocks not covered by any non-cancelled shift
  const openingGap = (dayShifts, dayHours) => {
    let n = 0;
    for (const b of dayHours) {
      for (let h = b.start; h < b.end; h += 0.5) {
        if (!dayShifts.some(s => s.status !== "cancelled" && s.start <= h && s.end > h)) n += 0.5;
      }
    }
    return n;
  };
  const peakConcurrent = (dayShifts) => {
    let mx = 0;
    for (let h = open; h < close; h++) {
      const n = dayShifts.filter((s) => s.status !== "cancelled" && s.start <= h && s.end > h).length;
      if (n > mx) mx = n;
    }
    return mx;
  };

  const renderRow = (s, isToday) => {
    const staff = SBD.staffById(s.staffId) || { id: s.staffId, name: "?", role: "", color: "#aaa" };
    const cancelled = CONFLICT_STATUSES.has(s.status);
    const swap = s.status === "swap_requested";
    const statusMode = colorMode === "status";
    const tone = statusMode ? shiftTone(s, shifts) : null;
    const isDraft = !!s.draft;
    // Broadcast-Ausschreibung: broadcast_mode aus API oder noch im client-seitigen Set
    const isBroadcast = (!!s.broadcastMode || broadcastIds.has(s.id)) && s.status === "asking";
    const bg = isDraft
      ? "#b7b3a6"
      : isBroadcast
        ? "#c9850a"   // sattes Amber-Gelb — klar von Orange/Grün unterscheidbar
        : (statusMode ? TONE_BG[tone] : (cancelled ? "#cfccc0" : staff.color));
    const start = (drag && drag.id === s.id) ? drag.start : s.start;
    const end = start + (s.end - s.start);
    const left = toX(start), width = toX(end) - toX(start);
    const wide = width > 16;
    const cat = catLabel(s.category);
    return (
      <div className="dr-row" key={s.id}>
        {/* Namenspalte: Broadcast zeigt 📢-Icon statt Mitarbeitername */}
        {isBroadcast ? (
          <div className="dr-name" style={{ cursor: "default" }}>
            <span style={{ fontSize: 17, lineHeight: 1, flexShrink: 0 }}>📢</span>
            <div style={{ minWidth: 0 }}>
              <div className="dr-name-t" style={{ color: "#92610a" }}>Vertretung</div>
              <div className="role-tag dr-name-r" style={{ color: "#b07820" }}>
                {s.broadcastAsked > 0 ? `${s.broadcastAsked} angefragt` : "gesucht"}
              </div>
            </div>
          </div>
        ) : (
          <div className="dr-name dr-name-click" onClick={() => window.__SB_openMember && window.__SB_openMember(s.staffId)} title={staff.name}>
            <Avatar staff={staff} size={24} mono />
            <div style={{ minWidth: 0 }}>
              <div className="dr-name-t">{staff.name.split(" ")[0]}</div>
              <div className="role-tag dr-name-r">{cat || staff.role}</div>
            </div>
          </div>
        )}
        <div className="dr-track" ref={(el) => { if (el) el._shift = s; }}>
          {(weekdayHours && weekdayHours[days[s.day].dow] || []).map((b, i) => (
            <span key={"b" + i} className="dr-hours" style={{ left: toX(b.start) + "%", width: (toX(b.end) - toX(b.start)) + "%" }} />
          ))}
          {hourMarks.map((h) => <span key={h} className="dr-gl" style={{ left: toX(h) + "%" }} />)}
          <div
            className={"dr-bar" + (cancelled ? " is-cancelled" : "") + (isDraft ? " is-draft" : "") + (isBroadcast ? " is-broadcast" : "")}
            style={{ left: left + "%", width: width + "%", background: bg }}
            title={isBroadcast
              ? `📢 Vertretung gesucht · ${SBD.fmtRange(s.start, s.end)}${s.broadcastAsked > 0 ? ` · ${s.broadcastAsked} angefragt` : ""}`
              : `${staff.name} · ${staff.role} · ${SBD.fmtRange(s.start, s.end)}${cat ? " · " + cat : ""}${isDraft ? " · " + T("draft") : ""}`}
            onPointerDown={(e) => { if (draggable) { const tr = e.currentTarget.parentElement; pd.current = { id: s.id, day: s.day, start: s.start, dur: s.end - s.start, track: tr, x0: e.clientX, moved: false, curStart: s.start }; } }}
            onClick={() => { if (pd.current && pd.current.moved) return; onShiftClick && onShiftClick(s); }}>
            {isBroadcast && <span className="dr-flag">📢</span>}
            {!isBroadcast && isDraft && <span className="dr-flag">✎</span>}
            {!isBroadcast && swap && <span className="dr-flag">⇄</span>}
            {!isBroadcast && (cancelled || (tone === "red" && !swap)) && !isDraft && <span className="dr-flag">!</span>}
            <span className="dr-bar-lab">
              {SBD.fmtRange(s.start, s.end)}
              {!isBroadcast && wide && cat ? " · " + cat : ""}
              {!isBroadcast && wide && cancelled ? " · " + T("lab_absent") : ""}
              {!isBroadcast && wide && swap ? " · " + T("lab_swap") : ""}
            </span>
          </div>
          <div className="dr-actions">
            {isDraft && onConfirm && <button className="dr-act conf" title={T("confirm")} onClick={(e) => { e.stopPropagation(); onConfirm(s.id); }}><Icon name="check" /></button>}
            {isDraft && onSend && staff.whatsappInvited && <button className="dr-act send" title={T("send_wa")} onClick={(e) => { e.stopPropagation(); onSend(s.id); }}><Icon name="chat" /></button>}
            {isDraft && onSend && !staff.whatsappInvited && <span title="Mitarbeiter noch nicht eingeladen" style={{ fontSize: 13, padding: "4px 6px", color: "var(--faint)" }}>📵</span>}
            {onDelete && <button className="dr-act del" title={T("delete")} onClick={(e) => { e.stopPropagation(); onDelete(s); }}><Icon name="trash" /></button>}
          </div>
        </div>
      </div>
    );
  };

  return (
    <div className="dr">
      <div className="dr-axis">
        <div className="dr-name dr-axis-corner" />
        <div className="dr-track dr-axis-track">
          {hourMarks.map((h) => <span key={h} className="dr-axis-hr" style={{ left: toX(h) + "%" }}>{SBD.fmtHour(h)}</span>)}
        </div>
      </div>
      {days.map((d) => {
        const dayShifts = shifts.filter((s) => s.day === d.index).sort((a, b) => a.start - b.start || a.end - b.end);
        const dayHours = (weekdayHours && weekdayHours[d.dow]) || [];
        const isClosed = !dayHours.length;
        // Custom holidays (owner-configured) haben Vorrang vor hardcoded
        const customHol = customHolidays.find(h => h.month === (d.monthIdx + 1) && h.day === d.num);
        const holiday = customHol
          ? customHol.name
          : (SBD.germanHoliday ? SBD.germanHoliday(d.year, d.monthIdx, d.num) : null);
        const gapH = isClosed ? 0 : openingGap(dayShifts, dayHours);
        const isToday = todayIndex != null && todayIndex === d.index;
        return (
          <div key={d.index} className={"dr-day" + (isToday ? " is-today" : "") + (isClosed ? " dr-day--closed" : "")}>
            <div className="dr-dayhead">
              <div className="dr-dh-label">
                <span className="dr-dh-dow">{DOW(d.dow)}</span>
                <span className="dr-dh-num">{d.num} {d.monthIdx != null ? window.SBI.monthName(d.monthIdx) : window.SBI.month()}</span>
                {isToday && <span className="dr-dh-today">{T("today")}</span>}
                {holiday && <span className="dr-dh-holiday">{holiday}</span>}
              </div>
              <div className="dr-dh-meta">
                <span className="role-tag">{T("working_peak", { n: dayShifts.filter(s=>s.status!=="cancelled").length, p: peakConcurrent(dayShifts) })}</span>
                {!isClosed && <span className={"dr-cov" + (gapH > 0 ? " warn" : " ok")}>{gapH === 0 ? "✓" : `${gapH}h offen`}</span>}
              </div>
            </div>
            {dayShifts.length ? dayShifts.map((s) => renderRow(s, isToday))
              : isClosed
              ? <div className="dr-row dr-row-empty">
                  <div className="dr-name dr-name-closed"><span>🔒</span><em>{T("closed")}</em></div>
                  <div className="dr-track">
                    {hourMarks.map((h) => <span key={h} className="dr-gl" style={{ left: toX(h)+"%" }} />)}
                  </div>
                </div>
              : <div className="dr-row dr-row-empty">
                  <div className="dr-name dr-name-warn">
                    <Icon name="alert" style={{ width: 14, height: 14, flexShrink: 0 }} />
                    {T("no_shifts")}
                  </div>
                  <div className="dr-track">
                    {dayHours.map((b, i) => <span key={"b"+i} className="dr-hours" style={{ left: toX(b.start)+"%", width: (toX(b.end)-toX(b.start))+"%" }} />)}
                    {hourMarks.map((h) => <span key={h} className="dr-gl" style={{ left: toX(h)+"%" }} />)}
                  </div>
                </div>
            }
          </div>
        );
      })}
    </div>
  );
}

/* ----------------------------------------------------------------
   Alerts
---------------------------------------------------------------- */
const _stub = (id) => ({ id, name: "?", role: "", color: "#aaa" });
function SwapModal({ alert, shifts, onApprove, onDecline, onClose }) {
  const reqStaff = SBD.staffById(alert.staffId) || _stub(alert.staffId);
  const toStaff  = SBD.staffById(alert.toStaffId) || _stub(alert.toStaffId);
  const reqShift = shifts.find((s) => s.id === alert.shiftId) || {};
  const toShift = shifts.find((s) => s.id === alert.toShiftId) || {};
  const catLabel = (s) => { const c = (window.__SB_CATS || []).find((x) => x.id === s.category); return c ? c.label : T("uncat"); };
  const dayStr = (s) => s.day != null ? DOW(SBD.WEEK[s.day].dow) + " " + SBD.WEEK[s.day].num : "";
  const timeStr = (s) => s.start != null ? SBD.fmtRange(s.start, s.end) : "";
  const SideCard = ({ staff, shift, label, tone }) => (
    <div className="swap-card">
      <div className="swap-card-lab">{label}</div>
      <div className="row gap10" style={{ marginTop: 8 }}>
        <Avatar staff={staff} size={38} mono />
        <div style={{ minWidth: 0 }}>
          <div className="swap-card-name">{staff.name}</div>
          <div className="role-tag">{staff.role}</div>
        </div>
      </div>
      <div className="swap-shift" style={{ borderLeftColor: tone }}>
        <div className="swap-shift-day">{dayStr(shift)}</div>
        <div className="swap-shift-time mono">{timeStr(shift)}</div>
        <div className="swap-shift-cat">{catLabel(shift)}</div>
      </div>
    </div>
  );
  return (
    <Modal title={T("swap_title")} onClose={onClose}
      foot={<>
        <button className="btn btn-ghost sd-del" onClick={() => { onDecline(alert.id, alert.staffId, alert.shiftId); onClose(); }}>{T("decline_swap")}</button>
        <div style={{ flex: 1 }} />
        <button className="btn btn-primary" onClick={() => { onApprove(alert.shiftId, alert.toShiftId, alert.id); onClose(); }}><Icon name="swap" />{T("approve_swap")}</button>
      </>}>
      <div className="modal-body">
        <div className="swap-grid">
          <SideCard staff={reqStaff} shift={reqShift} label={T("swap_gives")} tone="var(--st-orange)" />
          <div className="swap-mid"><span className="swap-mid-ico"><Icon name="swap" /></span><span className="swap-mid-t">{T("swap_arrow")}</span></div>
          <SideCard staff={toStaff} shift={toShift} label={T("swap_wants")} tone="var(--green)" />
        </div>
        <div className="alert-meta" style={{ textAlign: "center" }}>“{alert.reason}” · {T("via_wa", { t: alert.at })}</div>
        <div className="sb-note sb-note-info">
          <strong>{T("swap_after")}:</strong> {T("swap_after_d", {
            a: reqStaff.name.split(" ")[0], b: toStaff.name.split(" ")[0],
            aday: dayStr(reqShift), atime: timeStr(reqShift), acat: catLabel(reqShift),
            bday: dayStr(toShift), btime: timeStr(toShift), bcat: catLabel(toShift),
          })}
        </div>
      </div>
    </Modal>
  );
}
function AlertItem({ alert, shift, onCover, onDismiss, onSwap, onDeclineSwap, shifts, candidates, unavail }) {
  const staff   = SBD.staffById(alert.staffId)  || _stub(alert.staffId);
  const [open, setOpen] = useState(false);
  // Schicht-Zeiten für Verfügbarkeitscheck
  const shiftObj = shifts && shifts.find(s => s.id === alert.shiftId);
  const coverList = open && shiftObj
    ? staffCoverList(shifts, unavail || [], alert.day, shiftObj.start, shiftObj.end, alert.staffId)
    : null;
  const [swapOpen, setSwapOpen] = useState(false);
  const isAbsence = alert.kind === "absence";
  const isSwap = alert.kind === "swap" && alert.toShiftId != null;
  const toStaff = isSwap ? (SBD.staffById(alert.toStaffId) || _stub(alert.toStaffId)) : null;
  return (
    <div className={"alert-item" + (alert.urgent ? " urgent" : "")}>
      <div className="alert-ico alert-ico-click" style={{ background: isAbsence ? "var(--terra-bg)" : "var(--plum-bg)", color: isAbsence ? "var(--terra)" : "var(--plum)" }}
        onClick={() => window.__SB_openMember && window.__SB_openMember(alert.staffId)} title={staff.name}>
        <Icon name={isAbsence ? "alert" : "swap"} />
      </div>
      <div className="alert-body">
        <div className="row gap8" style={{ flexWrap: "wrap" }}>
          <span className="at">{isAbsence ? T("al_absence") : T("al_swap")}</span>
          {alert.urgent && <span className="badge st-cancelled" style={{ fontSize: 10 }}>{T("urgent")}</span>}
        </div>
        {isSwap ? (
          <div className="ad"><strong>{staff.name}</strong> · {DOW(SBD.WEEK[alert.day].dow)} {SBD.WEEK[alert.day].num} · {alert.time}<br />⇄ <strong>{toStaff.name}</strong> · {DOW(SBD.WEEK[SBD.SHIFTS.find(s=>s.id===alert.toShiftId)?.day ?? alert.day]?.dow || "")} {SBD.SHIFTS.find(s=>s.id===alert.toShiftId)?.day != null ? SBD.WEEK[SBD.SHIFTS.find(s=>s.id===alert.toShiftId).day].num : ""}</div>
        ) : (
          <div className="ad"><strong>{staff.name}</strong> · {DOW(SBD.WEEK[alert.day].dow)} {SBD.WEEK[alert.day].num} · {alert.time}<br />“{alert.reason}”</div>
        )}
        <div className="alert-meta">{T("via_wa", { t: alert.at })}</div>

        {isSwap ? (
          <div className="row gap8" style={{ marginTop: 10, flexWrap: "wrap" }}>
            <button className="btn btn-primary btn-sm" onClick={() => setSwapOpen(true)}><Icon name="swap" />{T("review_swap")}</button>
            <button className="btn btn-ghost btn-sm" onClick={() => onDismiss(alert.id)}>{T("dismiss")}</button>
          </div>
        ) : (!open && (
          <div className="row gap8" style={{ marginTop: 10, flexWrap: "wrap" }}>
            <button className="btn btn-primary btn-sm" onClick={() => {
              if (window.__SB_reqOpen) window.__SB_reqOpen(alert.shiftId);
              else setOpen(true);
            }}>
              <Icon name="users" />{T("find_cover")}
            </button>
            <button className="btn btn-ghost btn-sm" onClick={async () => {
              const r = await fetch(`/api/shifts/${alert.shiftId}/broadcast-cover`, {
                method: "POST", credentials: "include",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({}),
              });
              const d = await r.json();
              if (d.ok) {
                onDismiss(alert.id);
                if (window.__SB_loadShifts) window.__SB_loadShifts();
              }
            }}>
              <Icon name="bell" />{T("broadcast_cover")}
            </button>
            <button className="btn btn-ghost btn-sm" onClick={() => onDismiss(alert.id)}>{T("dismiss")}</button>
          </div>
        ))}
        {open && !isSwap && (
          <div className="fade-in" style={{ marginTop: 10 }}>
            <div className="eyebrow" style={{ marginBottom: 7 }}>{T("avail_cover")}</div>
            <div className="col" style={{ gap: 5 }}>
              {(coverList || candidates).map((c) => (
                <button key={c.id} className="row between"
                  onClick={() => onCover(alert.shiftId, c.id, alert.id)}
                  style={{ padding: "7px 9px", border: "1px solid var(--border-2)", borderRadius: 9,
                    background: "var(--surface-2)", cursor: "pointer", width: "100%",
                    opacity: c.avail === "blocked" ? 0.65 : 1 }}>
                  <span className="row gap8" style={{ minWidth: 0, flex: 1 }}>
                    <Avatar staff={c} size={26} />
                    <span style={{ fontWeight: 600, fontSize: 13 }}>{c.name}</span>
                    <span className="role-tag">{c.role}</span>
                  </span>
                  <span className="row gap8">
                    {c.avail && <AvailBadge avail={c.avail} hint={c.hint} />}
                    <Icon name="arrowR" style={{ width: 16, height: 16, color: "var(--green)", flexShrink: 0 }} />
                  </span>
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
      {swapOpen && <SwapModal alert={alert} shifts={shifts || SBD.SHIFTS} onApprove={onSwap} onDecline={onDeclineSwap} onClose={() => setSwapOpen(false)} />}
    </div>
  );
}

/* ----------------------------------------------------------------
   Tasks
---------------------------------------------------------------- */
function TaskRow({ task, onToggle }) {
  const staff = SBD.staffById(task.staffId) || _stub(task.staffId);
  return (
    <div className={"task-item" + (task.done ? " is-done" : "")}>
      <div className={"task-check" + (task.done ? " done" : "")} onClick={() => onToggle(task.id)}>
        <Icon name="check" />
      </div>
      <div className="task-text">
        <div className="tt">{task.title}</div>
        <div className="td">{staff.name.split(" ")[0]} · {task.dueLabel}{task.description ? " · " + task.description : ""}</div>
      </div>
      <Avatar staff={staff} size={26} />
    </div>
  );
}

/* ----------------------------------------------------------------
   Message thread (WhatsApp inbox)
---------------------------------------------------------------- */
function MessageThread({ messages }) {
  return (
    <div>
      {messages.map((m) => {
        const staff = SBD.staffById(m.staffId) || _stub(m.staffId);
        return (
          <div key={m.id} className={"msg-row " + (m.dir === "in" ? "in" : "out")}>
            {m.dir === "in" && <Avatar staff={staff} size={30} />}
            <div className="bubble">
              {m.dir === "in" && m.intent && <div style={{ marginBottom: 5 }}><span className="intent-chip">{m.intent.replace("_", " ")}</span></div>}
              {m.body}
              <span className="b-time">{m.dir === "in" ? staff.name.split(" ")[0] + " · " : ""}{m.time}</span>
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ----------------------------------------------------------------
   Modal + Add-shift form
---------------------------------------------------------------- */
function Modal({ title, children, foot, onClose }) {
  useEffect(() => {
    const h = (e) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
  }, [onClose]);
  return (
    <div className="modal-scrim" onMouseDown={onClose}>
      <div className="modal" onMouseDown={(e) => e.stopPropagation()}>
        <div className="modal-head row between">
          <h3>{title}</h3>
          <button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
        </div>
        {children}
        {foot && <div className="modal-foot">{foot}</div>}
      </div>
    </div>
  );
}
function TimeRange({ start, end, onChange }) {
  const OPEN = SBD.RESTAURANT.openHour, CLOSE = SBD.RESTAURANT.closeHour, SPAN = CLOSE - OPEN;
  const trackRef = useRef(null);
  const drag = useRef(null);
  const snap = (h) => Math.round(h * 4) / 4;
  const clamp = (h) => Math.max(OPEN, Math.min(CLOSE, h));
  const toPct = (h) => ((h - OPEN) / SPAN) * 100;
  const dur = end - start;
  const durLabel = (Number.isInteger(dur) ? dur : dur.toFixed(2).replace(/0+$/, "")) + " h";

  const hAt = (clientX) => {
    const r = trackRef.current.getBoundingClientRect();
    return snap(OPEN + ((clientX - r.left) / r.width) * SPAN);
  };
  const begin = (which) => (e) => {
    e.preventDefault(); e.stopPropagation();
    const startX = e.touches ? e.touches[0].clientX : e.clientX;
    const orig = { start, end };
    drag.current = which;
    const move = (ev) => {
      const cx = ev.touches ? ev.touches[0].clientX : ev.clientX;
      if (which === "start") onChange(clamp(Math.min(hAt(cx), orig.end - 0.25)), orig.end);
      else if (which === "end") onChange(orig.start, clamp(Math.max(hAt(cx), orig.start + 0.25)));
      else {
        const r = trackRef.current.getBoundingClientRect();
        let delta = snap(((cx - startX) / r.width) * SPAN);
        delta = Math.max(OPEN - orig.start, Math.min(CLOSE - orig.end, delta));
        onChange(orig.start + delta, orig.end + delta);
      }
    };
    const up = () => { drag.current = null; window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); window.removeEventListener("touchmove", move); window.removeEventListener("touchend", up); };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };
  const nudge = (which, delta) => {
    if (which === "start") onChange(clamp(snap(Math.min(start + delta, end - 0.25))), end);
    else onChange(start, clamp(snap(Math.max(end + delta, start + 0.25))));
  };
  const ticks = []; for (let h = OPEN; h <= CLOSE; h += 2) ticks.push(h);

  return (
    <div className="tr2">
      <div className="tr2-readout">
        <div className="tr2-card">
          <span className="tr2-card-lab">{T("sh_start")}</span>
          <div className="tr2-card-ctrl">
            <button className="tr2-step" onClick={() => nudge("start", -0.25)}>−</button>
            <span className="tr2-card-v mono">{SBD.fmtHour(start)}</span>
            <button className="tr2-step" onClick={() => nudge("start", 0.25)}>+</button>
          </div>
        </div>
        <span className="tr2-dur"><span className="tr2-dur-v">{durLabel}</span></span>
        <div className="tr2-card">
          <span className="tr2-card-lab">{T("sh_end")}</span>
          <div className="tr2-card-ctrl">
            <button className="tr2-step" onClick={() => nudge("end", -0.25)}>−</button>
            <span className="tr2-card-v mono">{SBD.fmtHour(end)}</span>
            <button className="tr2-step" onClick={() => nudge("end", 0.25)}>+</button>
          </div>
        </div>
      </div>
      <div className="tr2-track" ref={trackRef}>
        {ticks.map((h) => <span key={h} className="tr2-tick" style={{ left: toPct(h) + "%" }} />)}
        <div className="tr2-fill" style={{ left: toPct(start) + "%", width: (toPct(end) - toPct(start)) + "%" }} onPointerDown={begin("move")}>
          <span className="tr2-fill-dur mono">{durLabel}</span>
        </div>
        <div className="tr2-thumb" style={{ left: toPct(start) + "%" }} onPointerDown={begin("start")} role="slider" tabIndex={0}><i /><i /></div>
        <div className="tr2-thumb" style={{ left: toPct(end) + "%" }} onPointerDown={begin("end")} role="slider" tabIndex={0}><i /><i /></div>
      </div>
    </div>
  );
}
function AddShiftModal({ onClose, onAdd, categories, weekDays, weekOffset, weekLabel, isBlocked, hasOverlap, unavail, weekdayHours }) {
  const cats = categories || SBD.CATEGORIES;
  const days = weekDays || SBD.WEEK;
  // Nur eingeladene Mitarbeiter können Schichten zugewiesen bekommen
  const invitedStaff = SBD.STAFF.filter(s => s.whatsappInvited);
  const [staffId, setStaffId] = useState(invitedStaff[0]?.id ?? SBD.STAFF[0]?.id);
  const [day, setDay] = useState(SBD.TODAY_INDEX);
  const [start, setStart] = useState(8);
  const [end, setEnd] = useState(16);
  const [category, setCategory] = useState(cats[0] ? cats[0].id : "");
  const [notes, setNotes] = useState("");
  const [sendNow, setSendNow] = useState(false);
  const [showUn, setShowUn] = useState(false);
  const blocked = isBlocked ? isBlocked(+staffId, +day, +start, +end) : null;
  const overlap = hasOverlap ? hasOverlap(+staffId, +day, +start, +end) : null;
  const myUn = (unavail || []).filter((u) => u.staffId === +staffId);
  const submit = () => {
    onAdd({ staffId: +staffId, day: +day, start: +start, end: +end, category: category || null, notes: notes || undefined, send: sendNow, week: weekOffset });
    onClose();
  };
  return (
    <Modal title={T("sh_title")} onClose={onClose}
      foot={<>
        <label className="ap-check sh-sendcheck">
          <input type="checkbox" checked={sendNow} onChange={(e) => setSendNow(e.target.checked)} />
          <span className="ap-check-box"><Icon name="check" /></span>
          <span>{T("sh_send_now")}</span>
        </label>
        <div style={{ flex: 1 }} />
        <button className="btn btn-ghost" onClick={onClose}>{T("cancel")}</button>
        <button className="btn btn-primary" onClick={submit} style={overlap ? { background: "var(--st-orange)" } : {}}>
          {overlap ? <Icon name="alert" style={{ width: 14, height: 14 }} /> : null}{T("sh_create")}
        </button>
      </>}>
      <div className="modal-body">
        {weekLabel && <div className="sh-weektag mono"><Icon name="calendar" style={{ width: 13, height: 13 }} />{weekLabel}</div>}
        <div className="field">
          <label>{T("sh_staff")}</label>
          <select value={staffId} onChange={(e) => setStaffId(e.target.value)}>
            {invitedStaff.length === 0
              ? <option disabled>Noch keine Mitarbeiter eingeladen</option>
              : invitedStaff.map((s) => <option key={s.id} value={s.id}>{s.name} — {s.role}</option>)
            }
          </select>
        </div>
        <div className="field">
          <label>{T("sh_day")}</label>
          <select value={day} onChange={(e) => setDay(e.target.value)}>
            {days.map((d) => <option key={d.index} value={d.index}>
              {DOW(d.dow)} {d.num} {d.monthIdx != null ? window.SBI.monthName(d.monthIdx) : window.SBI.month()}
              {d.index === SBD.TODAY_INDEX && weekOffset === 0 ? " — Heute" : ""}
            </option>)}
          </select>
          {(() => {
            const DOW_KEYS = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
            const hours = weekdayHours && weekdayHours[DOW_KEYS[+day]];
            if (!hours) return null;
            if (hours.length === 0) return (
              <div style={{ marginTop: 6, fontSize: 12, color: "var(--muted)", display: "flex", alignItems: "center", gap: 5 }}>
                <Icon name="clock" style={{ width: 13, height: 13 }} /> Geschlossen
              </div>
            );
            const fmt = (h) => String(Math.floor(h)).padStart(2,"0") + ":" + String(Math.round((h%1)*60)).padStart(2,"0");
            return (
              <div style={{ marginTop: 6, fontSize: 12, color: "var(--green-deep)", display: "flex", alignItems: "center", gap: 5 }}>
                <Icon name="clock" style={{ width: 13, height: 13 }} />
                Öffnungszeiten: {hours.map((b, i) => <span key={i}>{i > 0 ? " · " : ""}{fmt(b.start)}–{fmt(b.end)}</span>)}
              </div>
            );
          })()}
        </div>
        <div className="field">
          <label>{T("sh_start")} – {T("sh_end")}</label>
          <TimeRange start={start} end={end} onChange={(s, e) => { setStart(s); setEnd(e); }} />
        </div>
        {overlap && (
          <div className="sh-overlap-warn">
            <Icon name="alert" style={{ width: 15, height: 15, flexShrink: 0 }} />
            <span>{T("sh_overlap", { name: (SBD.staffById(+staffId)||_stub(+staffId)).name.split(" ")[0], time: SBD.fmtRange(overlap.start, overlap.end) })}</span>
          </div>
        )}
        {blocked && (
          <div className="sh-blocked-wrap">
            <div className="sh-blocked">
              <Icon name="alert" style={{ width: 15, height: 15, flex: "none" }} />
              <span style={{ flex: 1 }}>{T("sh_blocked", { name: (SBD.staffById(+staffId)||_stub(+staffId)).name.split(" ")[0], reason: blocked.reason })}</span>
              <button type="button" className="sh-blocked-btn" onClick={() => setShowUn((v) => !v)}>{showUn ? T("hide") : T("show_unavail")}</button>
            </div>
            {showUn && (
              <div className="sh-unlist fade-in">
                {myUn.map((u) => (
                  <div key={u.id} className="sh-unrow">
                    <span className="sh-undot" />
                    <span className="sh-unday">{DOW(SBD.DOW_KEYS[u.day])}</span>
                    <span className="mono sh-untime">{SBD.fmtRange(u.start, u.end)}</span>
                    <span className="sh-unreason">{u.reason}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
        <div className="field">
          <label>{T("category")}</label>
          <select value={category} onChange={(e) => setCategory(e.target.value)}>
            {cats.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}
            <option value="">— {T("uncat")} —</option>
          </select>
        </div>
        <div className="field">
          <label>{T("sh_notes")}</label>
          <input value={notes} onChange={(e) => setNotes(e.target.value)} placeholder={T("sh_notes_ph")} />
        </div>
      </div>
    </Modal>
  );
}

/* ----------------------------------------------------------------
   Toasts
---------------------------------------------------------------- */
function Toasts({ toasts }) {
  return (
    <div className="toast-wrap">
      {toasts.map((t) => (
        <div key={t.id} className="toast"><Icon name={t.icon} />{t.text}</div>
      ))}
    </div>
  );
}

/* ----------------------------------------------------------------
   Shared derived helpers
---------------------------------------------------------------- */
function todayShifts(shifts) {
  return shifts.filter((s) => s.day === SBD.TODAY_INDEX && s.status !== "cancelled")
    .sort((a, b) => a.start - b.start);
}
/** Returns {label, state} for the weekly coverage chip in the Topbar.
 *  Checks all open days this week — not just today.
 *  state: "ok" | "gaps" | "none" | "closed" */
function coverageStatus(shifts, weekdayHours) {
  const DOW_KEYS = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
  const weekShifts = shifts.filter((s) => s.week === 0 && s.status !== "cancelled");

  if (!weekdayHours) return { label: window.SBI.t("cov_none"), state: "none" };

  let openDays = 0, coveredDays = 0;
  for (let i = 0; i < 7; i++) {
    const hours = weekdayHours[DOW_KEYS[i]] || [];
    if (!hours.length) continue; // geschlossen
    openDays++;
    const dayShifts = weekShifts.filter((s) => s.day === i);
    if (dayShifts.length > 0) coveredDays++;
  }

  if (openDays === 0) return { label: window.SBI.t("cov_closed"), state: "closed" };
  if (coveredDays === 0) return { label: window.SBI.t("cov_none"), state: "none" };
  if (coveredDays === openDays) return { label: window.SBI.t("cov_ok"), state: "ok" };
  const uncovered = openDays - coveredDays;
  return { label: window.SBI.t("cov_gaps", { n: uncovered }), state: "gaps" };
}
function onNowCount(shifts, hour = 13) {
  return shifts.filter((s) => s.day === SBD.TODAY_INDEX && s.status !== "cancelled" && s.start <= hour && s.end > hour).length;
}
function coverCandidates(shifts, day, excludeId) {
  const busy = new Set(shifts.filter((s) => s.day === day && s.status !== "cancelled").map((s) => s.staffId));
  return SBD.STAFF.filter((s) => s.id !== excludeId && !busy.has(s.id)).slice(0, 4);
}

/**
 * Gibt ALLE Mitarbeiter (außer excludeStaffId) zurück, je mit Verfügbarkeitsstatus:
 *   "free"    — keine Überschneidung, frei
 *   "overlap" — hat eine Schicht die sich zeitlich überschneidet
 *   "blocked" — hat eine Abwesenheitssperre in diesem Zeitraum
 * Sortierung: frei zuerst, dann overlap, dann blocked.
 */
function staffCoverList(shifts, unavail, day, start, end, excludeStaffId) {
  return SBD.STAFF
    .filter(s => s.id !== excludeStaffId)
    .map(s => {
      const overlapShift = shifts.find(o =>
        o.staffId === s.id && o.day === day &&
        o.status !== "cancelled" &&
        start < o.end && o.start < end
      );
      const blockedSlot = (unavail || []).find(u =>
        u.staffId === s.id && u.day === day &&
        start < u.end && u.start < end
      );
      const avail = overlapShift ? "overlap" : blockedSlot ? "blocked" : "free";
      const hint  = overlapShift
        ? SBD.fmtRange(overlapShift.start, overlapShift.end)
        : blockedSlot ? (blockedSlot.reason || "Gesperrt") : null;
      return { ...s, avail, hint };
    })
    .sort((a, b) => ({ free: 0, overlap: 1, blocked: 2 }[a.avail] - { free: 0, overlap: 1, blocked: 2 }[b.avail]));
}

/** Kleines Verfügbarkeits-Badge für Mitarbeiterlisten */
function AvailBadge({ avail, hint }) {
  if (avail === "free") return (
    <span style={{ fontSize: 11, fontWeight: 600, color: "var(--st-green)",
      background: "var(--green-mist)", borderRadius: 20, padding: "2px 8px", whiteSpace: "nowrap" }}>
      ✓ Frei
    </span>
  );
  if (avail === "overlap") return (
    <span style={{ fontSize: 11, fontWeight: 600, color: "#92610a",
      background: "#fde9a8", borderRadius: 20, padding: "2px 8px", whiteSpace: "nowrap" }}
      title={"Hat Schicht: " + hint}>
      ⚠ {hint}
    </span>
  );
  return (
    <span style={{ fontSize: 11, fontWeight: 600, color: "var(--st-red)",
      background: "var(--terra-bg)", borderRadius: 20, padding: "2px 8px", whiteSpace: "nowrap" }}
      title={hint}>
      🔒 {hint}
    </span>
  );
}

/* ---------- Shared shift detail modal ---------- */
// Hilfskonstanten für Broadcast-Antwort-Anzeige
const BROADCAST_RESP_ICON  = { waiting: "⏳", accepted: "✅", declined: "❌", expired: "⚫" };
const BROADCAST_RESP_LABEL = { waiting: "Ausstehend", accepted: "Angenommen", declined: "Abgelehnt", expired: "Abgelaufen" };
const BROADCAST_RESP_COLOR = { waiting: "var(--muted)", accepted: "var(--st-green)", declined: "var(--st-red)", expired: "var(--faint)" };

function ShiftDetailModal({ shift, store, onClose }) {
  const staff = SBD.staffById(shift.staffId) || { id: shift.staffId, name: "?", role: "", color: "#aaa" };
  const day = SBD.WEEK[shift.day];
  // Alle Mitarbeiter mit Verfügbarkeitsstatus (zeitbasiert)
  const cands = staffCoverList(store.shifts, store.unavail, shift.day, shift.start, shift.end, shift.staffId);
  const needsCover = ["absent","declined"].includes(shift.status);
  const [coverMode, setCoverMode] = useState(needsCover);
  const [broadcasting, setBroadcasting] = useState(false);
  // Auswahl für selektiven Broadcast: null = kein Broadcast-Modus, Set = ausgewählte IDs
  const [broadcastSel, setBroadcastSel] = useState(null);

  // Broadcast-Status: wer wurde gefragt, was hat er geantwortet?
  const isBroadcast = !!shift.broadcastMode || (store.broadcastShiftIds && store.broadcastShiftIds.has(shift.id));
  const [bcastStatus, setBcastStatus] = useState(null);   // null=lädt, []=[Ergebnisse]
  const [bcastError, setBcastError]   = useState(false);
  useEffect(() => {
    if (!isBroadcast) return;
    setBcastStatus(null); setBcastError(false);
    fetch(`/api/shifts/${shift.id}/broadcast-status`, { credentials: "include" })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(d => setBcastStatus(d))
      .catch(() => setBcastError(true));
  }, [shift.id, isBroadcast]);

  const toggleSel = (id) => setBroadcastSel(prev => {
    const s = new Set(prev);
    s.has(id) ? s.delete(id) : s.add(id);
    return s;
  });

  const doBroadcast = async (staffIds) => {
    setBroadcasting(true);
    try {
      const body = staffIds ? { staff_ids: staffIds } : {};
      const r = await fetch(`/api/shifts/${shift.id}/broadcast-cover`, {
        method: "POST", credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const d = await r.json();
      store.pushToast(`📢 ${d.asked} ${T("broadcast_cover_toast")}`, "bell");
      store.setBroadcastShiftIds(prev => new Set([...prev, shift.id]));
      await store.loadShifts(store.weekOffset);
      onClose();
    } finally { setBroadcasting(false); }
  };

  return (
    <Modal title={T("sd_title")} onClose={onClose}>
      <div className="modal-body">
        <div className="row gap12" style={{ alignItems: "center" }}>
          <Avatar staff={staff} size={48} />
          <div>
            <div style={{ fontWeight: 700, fontSize: 16 }}>{staff.name}</div>
            <div className="role-tag">{staff.role} · {staff.phone}</div>
          </div>
          <div style={{ marginLeft: "auto" }}><StatusBadge status={shift.status} /></div>
        </div>
        <div className="sb-detail-grid">
          <div><div className="eyebrow">{T("sd_day")}</div><div className="sb-dv">{DOW(day.dow)} {day.num} {window.SBI.month()}</div></div>
          <div><div className="eyebrow">{T("sd_time")}</div><div className="sb-dv mono">{SBD.fmtRange(shift.start, shift.end)}</div></div>
          <div><div className="eyebrow">{T("sd_hours")}</div><div className="sb-dv">{shift.end - shift.start}h</div></div>
        </div>
        {(() => { const c = (store.categories || []).find((x) => x.id === shift.category); return c ? <div className="row gap8" style={{ alignItems: "center" }}><span className="eyebrow">{T("category")}</span><span className="badge sb-cat-badge">{c.label}</span></div> : null; })()}
        {shift.draft && <div className="sb-note sb-note-draft">✎ {T("draft")} — {T("draft_note")}</div>}
        {shift.notes && <div className="sb-note">📝 {shift.notes}</div>}

        {/* ── Broadcast-Status-Panel ───────────────────────────────────── */}
        {isBroadcast && (
          <div style={{ marginTop: 14, padding: "12px 14px", background: "#fdf3e0", border: "1px solid #e9c46a", borderRadius: 11 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
              <span style={{ fontSize: 16 }}>📢</span>
              <span style={{ fontWeight: 700, fontSize: 13.5, color: "#92610a" }}>Broadcast — Vertretungsanfragen</span>
              {shift.broadcastAsked > 0 && (
                <span style={{ marginLeft: "auto", fontSize: 11.5, fontWeight: 600, color: "#b07820",
                  background: "#fde9a8", borderRadius: 20, padding: "2px 9px" }}>
                  {shift.broadcastAsked} angefragt
                </span>
              )}
            </div>
            {bcastError && (
              <div style={{ fontSize: 12.5, color: "var(--st-red)" }}>Fehler beim Laden der Anfragen.</div>
            )}
            {!bcastError && bcastStatus === null && (
              <div style={{ fontSize: 12.5, color: "var(--muted)" }}>Lädt…</div>
            )}
            {!bcastError && bcastStatus !== null && bcastStatus.length === 0 && (
              <div style={{ fontSize: 12.5, color: "var(--muted)", fontStyle: "italic" }}>Noch keine Anfragen gesendet.</div>
            )}
            {!bcastError && bcastStatus !== null && bcastStatus.length > 0 && (
              <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
                {bcastStatus.map(r => {
                  const mem = SBD.staffById(r.staff_id) || { name: r.name, color: "#aaa", role: "" };
                  return (
                    <div key={r.staff_id} style={{ display: "flex", alignItems: "center", gap: 10,
                      padding: "7px 10px", background: "#fff", borderRadius: 9,
                      border: "1px solid #e9c46a" }}>
                      <Avatar staff={mem} size={26} mono />
                      <span style={{ fontWeight: 600, fontSize: 13, flex: 1, minWidth: 0,
                        overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                        {r.name}
                      </span>
                      <span style={{ fontSize: 12, fontWeight: 600, color: BROADCAST_RESP_COLOR[r.response],
                        whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: 4 }}>
                        {BROADCAST_RESP_ICON[r.response]}{" "}
                        {BROADCAST_RESP_LABEL[r.response]}
                      </span>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        )}

        {coverMode && (
          <div className="fade-in">
            {cands.length === 0 && <div style={{ fontSize: 13, color: "var(--muted)", padding: "8px 0" }}>Keine Mitarbeiter vorhanden</div>}

            {/* Direkte Zuweisung (kein Broadcast-Modus) */}
            {broadcastSel === null && (
              <>
                <div className="eyebrow" style={{ marginBottom: 7 }}>{T("pick_cover")}</div>
                <div className="col" style={{ gap: 5 }}>
                  {cands.map((c) => (
                    <button key={c.id} className="row between sb-cand"
                      style={{ opacity: c.avail === "blocked" ? 0.65 : 1 }}
                      onClick={() => { store.coverShift(shift.id, c.id); onClose(); }}>
                      <span className="row gap8" style={{ minWidth: 0, flex: 1 }}>
                        <Avatar staff={c} size={26} />
                        <span style={{ fontWeight: 600, fontSize: 13 }}>{c.name}</span>
                        <span className="role-tag">{c.role}</span>
                      </span>
                      <span className="row gap8">
                        <AvailBadge avail={c.avail} hint={c.hint} />
                        <Icon name="arrowR" style={{ width: 16, height: 16, color: "var(--green)", flexShrink: 0 }} />
                      </span>
                    </button>
                  ))}
                  {cands.length > 0 && (
                    <div className="row gap8" style={{ marginTop: 8, borderTop: "1px solid var(--border)", paddingTop: 8 }}>
                      <button className="btn btn-ghost btn-sm" onClick={() => doBroadcast(null)} disabled={broadcasting}>
                        <Icon name="bell" />{T("broadcast_cover")}
                      </button>
                      <button className="btn btn-ghost btn-sm" onClick={() => setBroadcastSel(new Set())}>
                        <Icon name="users" />{T("broadcast_select")}
                      </button>
                    </div>
                  )}
                </div>
              </>
            )}

            {/* Selektiver Broadcast-Modus */}
            {broadcastSel !== null && (
              <>
                <div className="eyebrow" style={{ marginBottom: 7 }}>{T("broadcast_select")}</div>
                <div className="col" style={{ gap: 5 }}>
                  {cands.map((c) => {
                    const sel = broadcastSel.has(c.id);
                    return (
                      <button key={c.id} className="row gap8"
                        onClick={() => toggleSel(c.id)}
                        style={{ width: "100%", padding: "8px 10px", border: "1px solid " + (sel ? "var(--green)" : "var(--border-2)"),
                          borderRadius: 10, background: sel ? "var(--green-mist)" : "var(--surface-2)",
                          cursor: "pointer", transition: "all .13s", opacity: c.avail === "blocked" ? 0.65 : 1 }}>
                        <div style={{ width: 18, height: 18, borderRadius: 5, flexShrink: 0,
                          border: "2px solid " + (sel ? "var(--green)" : "var(--border-2)"),
                          background: sel ? "var(--green)" : "transparent",
                          display: "grid", placeItems: "center", transition: "all .13s" }}>
                          {sel && <Icon name="check" style={{ width: 11, height: 11, color: "#fff" }} />}
                        </div>
                        <Avatar staff={c} size={26} />
                        <span style={{ fontWeight: 600, fontSize: 13, flex: 1, textAlign: "left" }}>{c.name}</span>
                        <span className="role-tag">{c.role}</span>
                        <AvailBadge avail={c.avail} hint={c.hint} />
                      </button>
                    );
                  })}
                  <div className="row gap8" style={{ marginTop: 8, borderTop: "1px solid var(--border)", paddingTop: 8 }}>
                    <button className="btn btn-ghost btn-sm" onClick={() => setBroadcastSel(null)}>{T("cancel")}</button>
                    <button className="btn btn-primary btn-sm" disabled={broadcastSel.size === 0 || broadcasting}
                      onClick={() => doBroadcast([...broadcastSel])}>
                      <Icon name="bell" />{broadcasting ? "…" : T("broadcast_ask_selected", { n: broadcastSel.size })}
                    </button>
                  </div>
                </div>
              </>
            )}
          </div>
        )}
      </div>
      <div className="modal-foot">
        <button className="btn btn-ghost sd-del" onClick={() => { onClose(); if (window.__SB_reqDelete) window.__SB_reqDelete(shift); }}><Icon name="trash" />{T("delete")}</button>
        <div style={{ flex: 1 }} />
        {shift.draft && store.sendShift && <button className="btn btn-soft" onClick={() => { store.sendShift(shift.id); onClose(); }}><Icon name="chat" />{T("send_wa")}</button>}
        {shift.status !== "confirmed" && !needsCover && <button className="btn btn-soft" onClick={() => { store.confirmShift(shift.id); onClose(); }}><Icon name="check" />{T("confirm")}</button>}
        {!coverMode && <button className="btn btn-ghost" onClick={() => setCoverMode(true)}><Icon name="swap" />{T("find_cover")}</button>}
        <button className="btn btn-primary" onClick={onClose}>{T("done")}</button>
      </div>
    </Modal>
  );
}

/* ----------------------------------------------------------------
   ResizeHandle — drag to resize an adjacent panel (width or height)
---------------------------------------------------------------- */
function useResizable(initial, { min = 220, max = 560, axis = "x", invert = false } = {}) {
  const [size, setSize] = useState(initial);
  const start = useRef(null);
  const onPointerDown = (e) => {
    e.preventDefault();
    start.current = { pos: axis === "x" ? e.clientX : e.clientY, size };
    const move = (ev) => {
      if (!start.current) return;
      const cur = axis === "x" ? ev.clientX : ev.clientY;
      let delta = cur - start.current.pos;
      if (invert) delta = -delta;
      setSize(Math.max(min, Math.min(max, start.current.size + delta)));
    };
    const up = () => { start.current = null; window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); document.body.style.cursor = ""; };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
    document.body.style.cursor = axis === "x" ? "col-resize" : "row-resize";
  };
  return [size, onPointerDown, setSize];
}
function ResizeHandle({ onPointerDown, axis = "x" }) {
  return <div className={"resize-handle " + (axis === "x" ? "rh-x" : "rh-y")} onPointerDown={onPointerDown}><span className="rh-grip" /></div>;
}

Object.assign(window, {
  Icon, useShiftBud, Avatar, AvatarStack, StatusBadge, CoverageBar, Stat,
  WeekTimeline, DayRoster, AlertItem, TaskRow, MessageThread, Modal, AddShiftModal, Toasts,
  todayShifts, coverageStatus, onNowCount, coverCandidates, ShiftDetailModal, STATUS_LABEL,
  shiftTone, computeStack, TONE_BG, useResizable, ResizeHandle,
});
