/* FLOWLINE — views: Overview (region), UnitOrgChart (any unit, two tiers) */

/* =====================  OVERVIEW (departments → child units + people)  ===================== */
function RegionView({ t, filterSev, showFlagged, onOpenUnit, onOpenEntity }) {
  const FL = window.FL;
  const dimIf = (sev) => (filterSev && sev !== filterSev ? " dim" : "");
  const sum = FL.summary();
  const depts = FL.topUnits();

  // Pre-compute counts for the filter banner (units that appear as cards in this view)
  let flaggedUnitCount = 0;
  let totalUnitCount = 0;
  depts.forEach((d) => {
    const childUnits = FL.childUnitsOf(d.id);
    const directPeople = FL.peopleInUnit(d.id);
    // Mirror the self-card rule below, or the banner counts a card nobody sees.
    const selfSt = FL.unitSelfStatus(d.id);
    if (
      childUnits.length === 0 ||
      (directPeople.length > 0 && selfSt.sev !== "green")
    ) {
      totalUnitCount++;
      if (selfSt.sev !== "green") flaggedUnitCount++;
    }
    childUnits.forEach((cu) => {
      totalUnitCount++;
      if (FL.unitStatus(cu.id).sev !== "green") flaggedUnitCount++;
    });
  });

  return (
    <div className="canvas">
      <div className="view-head">
        <div>
          <div className="view-title">{FL.orgName}</div>
          <div className="view-desc">
            Org-wide roll-up · {FL.DEPTS.length} departments · {FL.SHOPS.length}{" "}
            shops · {FL.PEOPLE.length} people
          </div>
          {showFlagged && (
            <div
              className="view-desc"
              style={{
                marginTop: 4,
                color: flaggedUnitCount > 0 ? "var(--orange)" : "var(--green)",
              }}
            >
              {flaggedUnitCount > 0
                ? flaggedUnitCount +
                  " of " +
                  totalUnitCount +
                  " units need attention"
                : "All units are in flow right now"}
            </div>
          )}
        </div>
        <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
          {sum.sev.red > 0 && <SevTag sev="red">{sum.sev.red} critical</SevTag>}
          {sum.sev.orange > 0 && (
            <SevTag sev="orange">{sum.sev.orange} escalated</SevTag>
          )}
          {sum.sev.yellow > 0 && (
            <SevTag sev="yellow">{sum.sev.yellow} flagged</SevTag>
          )}
          {sum.open === 0 && <SevTag sev="green">All in flow</SevTag>}
        </div>
      </div>

      {showFlagged && flaggedUnitCount === 0 && (
        <div
          style={{
            padding: "48px 24px",
            textAlign: "center",
            color: "var(--ink-3)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            gap: 10,
          }}
        >
          <Dot sev="green" style="filled" />
          All departments and shops are in flow. Nothing needs attention right
          now.
        </div>
      )}

      {depts.map((d) => {
        const dst = FL.unitStatus(d.id);
        const childUnits = FL.childUnitsOf(d.id);
        const directPeople = FL.peopleInUnit(d.id);
        // The self-card speaks for the department's OWN roster only — its
        // children sit beside it as their own cards and the section header
        // above already carries the rollup.
        const selfSt = FL.unitSelfStatus(d.id);
        // A department with no child units IS its roster, so it always gets a
        // card. One that has child units only earns a self-card when someone
        // reporting to it directly needs attention: otherwise it is a card
        // whose whole content is "these people are fine", sitting among the
        // child cards that carry the actual work. Regional managers park at
        // the department level (their shops span containers), so this would
        // otherwise be a permanent empty card on every such department.
        const showSelfCard =
          childUnits.length === 0 ||
          (directPeople.length > 0 && selfSt.sev !== "green");

        // When "out of flow only" is active, hide green units entirely
        let visibleChildUnits = childUnits;
        let selfVisible = showSelfCard;
        if (showFlagged) {
          visibleChildUnits = childUnits.filter(
            (cu) => FL.unitStatus(cu.id).sev !== "green",
          );
          selfVisible = showSelfCard && selfSt.sev !== "green";
          if (!selfVisible && visibleChildUnits.length === 0) return null;
        }

        return (
          <div key={d.id}>
            <div className="section-label">
              <Avatar
                kind="dept"
                sev={dst.sev}
                dotStyle={t.dotStyle}
                size={22}
                square
                pulse={t.pulse}
              />
              {d.name}
              <button
                className="btn ghost sm"
                onClick={() => onOpenEntity({ kind: "dept", id: d.id })}
              >
                Why this color ›
              </button>
            </div>
            <div className="region-grid">
              {selfVisible && (
                <UnitCard
                  key={d.id + ":self"}
                  unit={d}
                  t={t}
                  dimIf={dimIf}
                  selfOnly
                  onOpen={() => onOpenUnit(d.id)}
                  onBreakdown={() => onOpenEntity({ kind: "dept", id: d.id })}
                />
              )}
              {visibleChildUnits.map((u) => (
                <UnitCard
                  key={u.id}
                  unit={u}
                  t={t}
                  dimIf={dimIf}
                  onOpen={() => onOpenUnit(u.id)}
                  onBreakdown={() =>
                    onOpenEntity({
                      kind: u.type === "shop" ? "shop" : "dept",
                      id: u.id,
                    })
                  }
                />
              ))}
              {!selfVisible && visibleChildUnits.length === 0 && (
                <div className="reasonline">
                  <Dot sev="green" style="filled" />
                  No teams in this department.
                </div>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// `selfOnly` renders a unit's own roster without its child units: the severity,
// reasons and group count all ignore the children, which are shown as sibling
// cards right next to it.
function UnitCard({ unit, t, dimIf, onOpen, onBreakdown, selfOnly }) {
  const FL = window.FL;
  const st = selfOnly ? FL.unitSelfStatus(unit.id) : FL.unitStatus(unit.id);
  const c = st.counts;
  const ppl = FL.peopleInUnit(unit.id);
  const childUnits = selfOnly ? [] : FL.childUnitsOf(unit.id);
  const bars = ppl.map((p) => FL.personStatus(p.id).sev);
  const order = { red: 0, orange: 1, yellow: 2, green: 3 };
  bars.sort((a, b) => order[a] - order[b]);
  const kind = unit.type === "shop" ? "shop" : "dept";
  const metaBits = [];
  if (unit.deptName) metaBits.push(unit.deptName);
  if (selfOnly) metaBits.push("Reports directly");
  metaBits.push(ppl.length + (ppl.length === 1 ? " person" : " people"));
  if (childUnits.length > 0)
    metaBits.push(
      childUnits.length + (childUnits.length === 1 ? " group" : " groups"),
    );
  return (
    <div
      className={"card shopcard lvl-" + st.sev + (dimIf ? dimIf(st.sev) : "")}
      onClick={onOpen}
    >
      <div className="shopcard-head">
        <div style={{ display: "flex", gap: 12, alignItems: "center" }}>
          <Avatar
            kind={kind}
            sev={st.sev}
            dotStyle={t.dotStyle}
            size={44}
            square
            pulse={t.pulse}
          />
          <div>
            <div className="shopcard-title">{unit.name}</div>
            <div className="shopcard-meta">{metaBits.join(" · ")}</div>
          </div>
        </div>
        <SevTag sev={st.sev} />
      </div>

      {st.reasons.slice(0, 3).map((r, i) => (
        <div key={i} className="reasonline">
          <Dot sev={r.sev} style="filled" />
          {r.text}
        </div>
      ))}
      {st.reasons.length === 0 && (
        <div className="reasonline">
          <Dot sev="green" style="filled" />
          Everyone in flow. Nothing escalated.
        </div>
      )}

      {bars.length > 0 && (
        <div className="minibars" title="Each segment = one person's status">
          {bars.map((sv, i) => (
            <span
              key={i}
              className="minibar"
              style={{
                background: `var(--${sv})`,
                opacity: sv === "green" ? 0.4 : 1,
              }}
            />
          ))}
        </div>
      )}

      <div className="headcount">
        <span className="hc">
          <Dot sev="red" />R<span className="n">{c.red}</span>
        </span>
        <span className="hc">
          <Dot sev="orange" />O<span className="n">{c.orange}</span>
        </span>
        <span className="hc">
          <Dot sev="yellow" />Y<span className="n">{c.yellow}</span>
        </span>
        <span className="hc">
          <Dot sev="green" />
          Flow<span className="n">{c.green}</span>
        </span>
        <button
          className="btn ghost sm"
          style={{ marginLeft: "auto" }}
          onClick={(e) => {
            e.stopPropagation();
            onBreakdown();
          }}
        >
          Why this color ›
        </button>
      </div>
    </div>
  );
}

/* =====================  SHOP / BRANCH LENS (additive grouping)  ===================== */
function BranchView({ t, filterSev, onOpenEntity }) {
  const FL = window.FL;
  const dimIf = (sev) => (filterSev && sev !== filterSev ? " dim" : "");
  const groups = FL.branchGroups();
  const flagged = groups.filter(
    (g) => FL.branchStatus(g).sev !== "green",
  ).length;
  return (
    <div className="canvas">
      <div className="view-head">
        <div>
          <div className="view-title">By shop / branch</div>
          <div className="view-desc">
            The same people and trucks, grouped by their home branch ·{" "}
            {groups.length} branches
          </div>
          <div className="view-desc" style={{ marginTop: 4 }}>
            Display lens only — supervision, routing and escalation are
            unchanged.
          </div>
        </div>
        <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
          {flagged > 0 ? (
            <SevTag sev="orange">{flagged} need attention</SevTag>
          ) : (
            <SevTag sev="green">All in flow</SevTag>
          )}
        </div>
      </div>
      <div className="region-grid">
        {groups.map((g) => (
          <BranchCard
            key={g.id}
            group={g}
            t={t}
            dimIf={dimIf}
            onOpenEntity={onOpenEntity}
          />
        ))}
        {groups.length === 0 && (
          <div className="reasonline">
            <Dot sev="green" style="filled" />
            No branches to show.
          </div>
        )}
      </div>
    </div>
  );
}

function BranchCard({ group, t, dimIf, onOpenEntity }) {
  const FL = window.FL;
  const st = FL.branchStatus(group);
  const c = st.counts;
  const order = { red: 0, orange: 1, yellow: 2, green: 3 };
  const people = group.peopleIds.map((id) => FL.peopleById[id]).filter(Boolean);
  const bars = people.map((p) => FL.personStatus(p.id).sev);
  bars.sort((a, b) => order[a] - order[b]);
  const flaggedPeople = people
    .filter((p) => FL.personStatus(p.id).sev !== "green")
    .sort(
      (a, b) =>
        order[FL.personStatus(a.id).sev] - order[FL.personStatus(b.id).sev],
    );
  const inFlowCount = people.length - flaggedPeople.length;
  const metaBits = [];
  metaBits.push(people.length + (people.length === 1 ? " person" : " people"));
  if (group.vehicles.length > 0)
    metaBits.push(
      group.vehicles.length +
        (group.vehicles.length === 1 ? " truck" : " trucks"),
    );
  return (
    <div
      className={"card shopcard lvl-" + st.sev + (dimIf ? dimIf(st.sev) : "")}
    >
      <div className="shopcard-head">
        <div style={{ display: "flex", gap: 12, alignItems: "center" }}>
          <Avatar
            kind="shop"
            sev={st.sev}
            dotStyle={t.dotStyle}
            size={44}
            square
            pulse={t.pulse}
          />
          <div>
            <div className="shopcard-title">{group.name}</div>
            <div className="shopcard-meta">{metaBits.join(" · ")}</div>
          </div>
        </div>
        <SevTag sev={st.sev} />
      </div>

      {flaggedPeople.slice(0, 5).map((p) => {
        const ps = FL.personStatus(p.id);
        return (
          <button
            key={p.id}
            className="reasonline"
            style={{
              width: "100%",
              textAlign: "left",
              background: "none",
              border: "none",
              cursor: "pointer",
              padding: "2px 0",
            }}
            onClick={() => onOpenEntity({ kind: "person", id: p.id })}
          >
            <Dot sev={ps.sev} style="filled" />
            {p.name}
            <span style={{ color: "var(--ink-3)", marginLeft: 6 }}>
              {p.role}
            </span>
          </button>
        );
      })}
      {flaggedPeople.length > 5 && (
        <div className="reasonline">
          <Dot sev="orange" style="filled" />+{flaggedPeople.length - 5} more
          flagged
        </div>
      )}
      {flaggedPeople.length === 0 && (
        <div className="reasonline">
          <Dot sev="green" style="filled" />
          Everyone in flow. Nothing escalated.
        </div>
      )}
      {flaggedPeople.length > 0 && inFlowCount > 0 && (
        <div className="reasonline" style={{ color: "var(--ink-3)" }}>
          <Dot sev="green" style="filled" />
          {inFlowCount} more in flow
        </div>
      )}

      {bars.length > 0 && (
        <div className="minibars" title="Each segment = one person's status">
          {bars.map((sv, i) => (
            <span
              key={i}
              className="minibar"
              style={{
                background: "var(--" + sv + ")",
                opacity: sv === "green" ? 0.4 : 1,
              }}
            />
          ))}
        </div>
      )}

      {group.vehicles.length > 0 && (
        <div className="headcount" style={{ flexWrap: "wrap", gap: 6 }}>
          {group.vehicles.map((v) => (
            <button
              key={v.id}
              className="btn ghost sm"
              title={v.operatorName ? "Driven by " + v.operatorName : v.name}
              onClick={() => {
                if (v.operatorId)
                  onOpenEntity({ kind: "person", id: v.operatorId });
              }}
            >
              {v.name}
            </button>
          ))}
        </div>
      )}

      <div className="headcount">
        <span className="hc">
          <Dot sev="red" />R<span className="n">{c.red}</span>
        </span>
        <span className="hc">
          <Dot sev="orange" />O<span className="n">{c.orange}</span>
        </span>
        <span className="hc">
          <Dot sev="yellow" />Y<span className="n">{c.yellow}</span>
        </span>
        <span className="hc">
          <Dot sev="green" />
          Flow<span className="n">{c.green}</span>
        </span>
      </div>
    </div>
  );
}

/* =====================  CARD EDIT MODAL  ===================== */
/* In-app edit of a person card's display fields (name / job title). Optimistic:
   the local caches update immediately; a failed save rolls them back. Edits are
   Flowline-authoritative — the server marks them so external syncs never
   overwrite them. */
function CardEditModal({ person, onClose, onSaved }) {
  const FL = window.FL;
  const [name, setName] = useState(person.name || "");
  const [title, setTitle] = useState(
    person.role && person.role !== "Team member" ? person.role : "",
  );
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);

  const submit = async function (e) {
    e.preventDefault();
    setErr("");
    var newName = name.trim();
    if (!newName) {
      setErr("Name can't be empty.");
      return;
    }
    var newTitle = title.trim();
    setBusy(true);
    // Optimistic local update so the chart reflects the edit immediately.
    var prev = { name: person.name, role: person.role };
    var node = FL.nodeById[person.id];
    var prevNode = node ? { name: node.name, role: node.role } : null;
    person.name = newName;
    person.role = newTitle || "Team member";
    if (node) {
      node.name = newName;
      node.role = newTitle || null;
    }
    try {
      await API.updateEntityCard(person.id, {
        name: newName,
        jobTitle: newTitle || null,
      });
      onSaved();
    } catch (e2) {
      // Roll back the optimistic update and surface the error.
      person.name = prev.name;
      person.role = prev.role;
      if (node && prevNode) {
        node.name = prevNode.name;
        node.role = prevNode.role;
      }
      setErr(e2.message || "Failed to save the card.");
      setBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div
        className="auth-card"
        data-testid="card-edit-modal"
        style={{ position: "relative", zIndex: 1 }}
        onClick={function (e) {
          e.stopPropagation();
        }}
      >
        <div className="auth-title">Edit card</div>
        <div className="auth-desc">
          Display info only — permissions and alert routing are unchanged. Edits
          made here stick, even when Samsara or Timetracker re-sync.
        </div>
        {err && <div className="auth-msg err">{err}</div>}
        <form onSubmit={submit}>
          <div className="field">
            <label>Name</label>
            <input
              className="input"
              value={name}
              onChange={function (e) {
                setName(e.target.value);
              }}
              required
            />
          </div>
          <div className="field">
            <label>Job title</label>
            <input
              className="input"
              value={title}
              placeholder="e.g. Lube tech"
              onChange={function (e) {
                setTitle(e.target.value);
              }}
            />
          </div>
          <button
            className="btn primary auth-btn"
            type="submit"
            disabled={busy}
          >
            {busy ? "Saving…" : "Save card"}
          </button>
        </form>
        <button
          className="btn ghost auth-btn"
          style={{ marginTop: 8 }}
          type="button"
          disabled={busy}
          onClick={onClose}
        >
          Cancel
        </button>
      </div>
    </div>
  );
}

/* =====================  UNIT ORG CHART (shop OR department)  ===================== */
function UnitOrgChart({
  unitId,
  t,
  filterSev,
  selectedId,
  onSelectPerson,
  onSelectEntity,
  onOpenUnit,
  quietGreens,
  showLogins,
  user,
  onReload,
}) {
  const FL = window.FL;
  const unit = FL.unitById[unitId];
  const unitSt = FL.unitStatus(unitId);
  const mgmt = FL.managementOf(unitId);
  const childUnits = FL.childUnitsOf(unitId);
  const vehicles = FL.vehiclesOf(unitId);
  const managers = mgmt.managers;
  const reports = mgmt.reports;

  // ---- pan/zoom ----
  const Z = { min: 0.5, max: 1.8, step: 0.15 };
  const isTouch =
    typeof window !== "undefined" &&
    window.matchMedia &&
    window.matchMedia("(pointer: coarse)").matches;
  const [scale, setScale] = useState(isTouch ? 0.7 : 1);
  const vpRef = useRef(null);
  const scaleRef = useRef(scale);
  useEffect(() => {
    scaleRef.current = scale;
  }, [scale]);
  useEffect(() => {
    setScale(isTouch ? 0.7 : 1);
  }, [unitId]);
  const [collapsed, setCollapsed] = useState({});
  useEffect(() => {
    setCollapsed({});
  }, [unitId]);
  const clampZ = (v) => Math.min(Z.max, Math.max(Z.min, v));

  // ---- card management (admins + in-scope managers only) ----
  // Client-side gating is display sugar; the server re-enforces both the role
  // and the shop scope on every offboard/edit call.
  const canManage = function (personId) {
    return user ? FL.canManagePerson(user, personId) : false;
  };
  const canManageHere =
    !!user &&
    (managers.some(function (p) {
      return canManage(p.id);
    }) ||
      reports.some(function (p) {
        return canManage(p.id);
      }));
  const [editPerson, setEditPerson] = useState(null);
  // drag state: { id, name, x, y } while a card is being dragged
  const [drag, setDrag] = useState(null);
  const [overTrash, setOverTrash] = useState(false);
  const trashRef = useRef(null);
  const overTrashRef = useRef(false);

  const confirmOffboard = async function (person) {
    const ok = window.confirm(
      "Remove " +
        person.name +
        " from the org chart?\n\nTheir card comes off the chart and any linked login is disabled. Past exceptions and audit history are kept.",
    );
    if (!ok) return;
    try {
      await API.offboardEntity(person.id);
      if (onReload) await onReload();
    } catch (e) {
      window.alert(e.message || "Failed to remove this person.");
    }
  };

  // Hand-rolled drag (no HTML5 DnD): mousedown on a manageable card arms a
  // 6px-threshold drag; past it, a ghost follows the pointer and the trash
  // target lights up while hovered. Mouseup over the trash offboards (after
  // the same confirmation as the click path). A sub-threshold press falls
  // through to the card's normal click (select person).
  const startCardDrag = function (person, ev) {
    if (ev.button !== undefined && ev.button !== 0) return;
    const startX = ev.clientX;
    const startY = ev.clientY;
    let moved = false;
    const isOverTrash = function (x, y) {
      const el = trashRef.current;
      if (!el) return false;
      const r = el.getBoundingClientRect();
      return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
    };
    const onMove = function (e) {
      if (
        !moved &&
        Math.abs(e.clientX - startX) < 6 &&
        Math.abs(e.clientY - startY) < 6
      )
        return;
      moved = true;
      e.preventDefault();
      setDrag({ id: person.id, name: person.name, x: e.clientX, y: e.clientY });
      const over = isOverTrash(e.clientX, e.clientY);
      overTrashRef.current = over;
      setOverTrash(over);
    };
    const onUp = function (e) {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      const dropOnTrash =
        moved && (overTrashRef.current || isOverTrash(e.clientX, e.clientY));
      overTrashRef.current = false;
      setOverTrash(false);
      setDrag(null);
      if (dropOnTrash) confirmOffboard(person);
    };
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  };

  useEffect(() => {
    const el = vpRef.current;
    if (!el) return;
    const onWheel = (e) => {
      if (e.ctrlKey || e.metaKey) {
        e.preventDefault();
        setScale((s) => clampZ(s - e.deltaY * 0.0022));
      }
    };
    let pinchStart = 0,
      pinchScale = 1;
    const dist = (tt) =>
      Math.hypot(tt[0].clientX - tt[1].clientX, tt[0].clientY - tt[1].clientY);
    const onTS = (e) => {
      if (e.touches.length === 2) {
        pinchStart = dist(e.touches);
        pinchScale = scaleRef.current;
      }
    };
    const onTM = (e) => {
      if (e.touches.length === 2 && pinchStart) {
        e.preventDefault();
        setScale(clampZ((pinchScale * dist(e.touches)) / pinchStart));
      }
    };
    const onTE = (e) => {
      if (e.touches.length < 2) pinchStart = 0;
    };
    el.addEventListener("wheel", onWheel, { passive: false });
    el.addEventListener("touchstart", onTS, { passive: false });
    el.addEventListener("touchmove", onTM, { passive: false });
    el.addEventListener("touchend", onTE);
    return () => {
      el.removeEventListener("wheel", onWheel);
      el.removeEventListener("touchstart", onTS);
      el.removeEventListener("touchmove", onTM);
      el.removeEventListener("touchend", onTE);
    };
  }, []);

  if (!unit)
    return (
      <div className="canvas">
        <div className="empty">Unit not found.</div>
      </div>
    );

  const kindLabel = unit.type === "shop" ? "Shop" : "Department";
  const entityKind = unit.type === "shop" ? "shop" : "dept";
  const selfSelected =
    selectedId === "unit:" + unitId ||
    selectedId === "shop:" + unitId ||
    selectedId === "dept:" + unitId;
  const hasPeople = managers.length > 0 || reports.length > 0;

  const roleGroups = {};
  reports.forEach((p) => {
    const key = p.role || "Unspecified";
    if (!roleGroups[key]) roleGroups[key] = [];
    roleGroups[key].push(p);
  });
  const sevOrd = { red: 0, orange: 1, yellow: 2, green: 3 };
  const groupWorstSev = {};
  Object.keys(roleGroups).forEach((role) => {
    let best = 3;
    roleGroups[role].forEach((p) => {
      const s = sevOrd[FL.personStatus(p.id).sev];
      if (s < best) best = s;
    });
    groupWorstSev[role] = ["red", "orange", "yellow", "green"][best];
  });
  const roleList = Object.keys(roleGroups).sort((a, b) => {
    const da = sevOrd[groupWorstSev[a]];
    const db = sevOrd[groupWorstSev[b]];
    if (da !== db) return da - db;
    return a.localeCompare(b);
  });

  return (
    <div className="canvas chart-canvas" ref={vpRef}>
      <div className="view-head">
        <div>
          <div className="view-title">{unit.name}</div>
          <div className="view-desc">
            {unit.deptName ? unit.deptName + " · " : ""}org chart · click any
            profile to drill into its exception(s)
          </div>
        </div>
        <div style={{ marginLeft: "auto" }}>
          <SevTag sev={unitSt.sev}>
            {kindLabel} {SevText[unitSt.sev]}
          </SevTag>
        </div>
      </div>

      <div className="zoomwrap" style={{ zoom: scale }}>
        <div className="tree">
          <UnitNode
            unit={unit}
            st={unitSt}
            t={t}
            filterSev={filterSev}
            selected={selfSelected}
            onClick={() => onSelectEntity({ kind: entityKind, id: unitId })}
          />

          {vehicles.length > 0 && <Connector />}
          {vehicles.length > 0 && (
            <div
              className="section-label"
              style={{ alignSelf: "stretch", margin: "4px 0 14px" }}
            >
              Fleet · {vehicles.length}
            </div>
          )}
          {vehicles.length > 0 && (
            <div
              className="tier"
              style={{ gap: 14, justifyContent: "flex-start" }}
            >
              {vehicles.map((v) => (
                <VehicleCard
                  key={v.id}
                  vehicle={v}
                  t={t}
                  onClick={() => {
                    if (v.operatorId) onSelectPerson(v.operatorId);
                  }}
                />
              ))}
            </div>
          )}

          {childUnits.length > 0 && <Connector />}
          {childUnits.length > 0 && (
            <div
              className="section-label"
              style={{ alignSelf: "stretch", margin: "4px 0 14px" }}
            >
              Groups · {childUnits.length}
            </div>
          )}
          {childUnits.length > 0 && (
            <div className="tier" style={{ gap: 14 }}>
              {childUnits.map((cu) => (
                <ChildUnitNode
                  key={cu.id}
                  unit={cu}
                  t={t}
                  filterSev={filterSev}
                  onClick={() => onOpenUnit(cu.id)}
                />
              ))}
            </div>
          )}

          {managers.length > 0 && <Connector />}
          {managers.length > 0 && (
            <div
              className="section-label"
              style={{ alignSelf: "stretch", margin: "4px 0 14px" }}
            >
              Management · {managers.length}
            </div>
          )}
          {managers.length > 0 && (
            <div className="tier" style={{ gap: 14 }}>
              {managers.map((p) => (
                <NodeCard
                  key={p.id}
                  person={p}
                  t={t}
                  quietGreens={quietGreens}
                  filterSev={filterSev}
                  selected={selectedId === "person:" + p.id}
                  onClick={() => onSelectPerson(p.id)}
                  showLogins={showLogins}
                  manage={canManage(p.id)}
                  onEdit={() => setEditPerson(p)}
                  onOffboard={() => confirmOffboard(p)}
                  onDragStart={(e) => startCardDrag(p, e)}
                />
              ))}
            </div>
          )}

          {hasPeople && <Connector />}
          {hasPeople && (
            <div
              className="section-label"
              style={{ alignSelf: "stretch", margin: "4px 0 14px" }}
            >
              Team · {reports.length}
            </div>
          )}
          {hasPeople && reports.length === 0 && (
            <div className="tier" style={{ gap: 14 }}>
              <div className="reasonline">
                <Dot sev="green" style="filled" />
                Everyone here is a manager — no direct reports.
              </div>
            </div>
          )}
          {hasPeople && reports.length > 0 && (
            <div style={{ alignSelf: "stretch" }}>
              {roleList.map((role) => {
                const group = roleGroups[role];
                const worst = groupWorstSev[role];
                const isCollapsed = !!collapsed[role];
                const flagged = group.filter(
                  (p) => FL.personStatus(p.id).sev !== "green",
                ).length;
                return (
                  <div key={role} style={{ marginBottom: 20 }}>
                    <div
                      className="section-label"
                      style={{ alignSelf: "stretch", margin: "0 0 10px" }}
                    >
                      <Dot sev={worst} style="filled" />
                      {role}
                      {flagged > 0 && <SevTag sev={worst}>{flagged}</SevTag>}
                      <span
                        style={{
                          fontWeight: 400,
                          textTransform: "none",
                          letterSpacing: 0,
                          fontSize: 11,
                        }}
                      >
                        {group.length}{" "}
                        {group.length === 1 ? "person" : "people"}
                      </span>
                      <button
                        className="btn ghost sm"
                        style={{ padding: "0 7px", minWidth: 0 }}
                        onClick={(e) => {
                          e.stopPropagation();
                          setCollapsed((prev) =>
                            Object.assign({}, prev, { [role]: !prev[role] }),
                          );
                        }}
                      >
                        {isCollapsed ? "▸ Show" : "▾ Hide"}
                      </button>
                    </div>
                    {!isCollapsed && (
                      <div
                        className="tier"
                        style={{ gap: 14, justifyContent: "flex-start" }}
                      >
                        {group.map((p) => (
                          <NodeCard
                            key={p.id}
                            person={p}
                            t={t}
                            quietGreens={quietGreens}
                            filterSev={filterSev}
                            selected={selectedId === "person:" + p.id}
                            onClick={() => onSelectPerson(p.id)}
                            showLogins={showLogins}
                            manage={canManage(p.id)}
                            onEdit={() => setEditPerson(p)}
                            onOffboard={() => confirmOffboard(p)}
                            onDragStart={(e) => startCardDrag(p, e)}
                          />
                        ))}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          )}

          {!hasPeople && childUnits.length === 0 && vehicles.length === 0 && (
            <div className="empty">No people in this unit.</div>
          )}
        </div>
      </div>

      <div className="zoombar" onWheel={(e) => e.stopPropagation()}>
        <button
          className="zbtn"
          title="Zoom out"
          onClick={() => setScale((s) => clampZ(s - Z.step))}
          disabled={scale <= Z.min + 0.001}
        >
          −
        </button>
        <button
          className="zlevel"
          title="Reset"
          onClick={() => setScale(isTouch ? 0.7 : 1)}
        >
          {Math.round(scale * 100)}%
        </button>
        <button
          className="zbtn"
          title="Zoom in"
          onClick={() => setScale((s) => clampZ(s + Z.step))}
          disabled={scale >= Z.max - 0.001}
        >
          +
        </button>
      </div>

      {canManageHere && (
        <div
          ref={trashRef}
          data-testid="trash-target"
          title="Drag a person's card here to remove them"
          style={{
            position: "fixed",
            right: 24,
            bottom: 74,
            zIndex: 60,
            padding: "12px 16px",
            borderRadius: 12,
            border:
              "2px dashed " +
              (overTrash ? "var(--red)" : "var(--border-strong)"),
            background: overTrash
              ? "rgba(220,60,60,.12)"
              : "var(--panel, #fff)",
            color: overTrash ? "var(--red)" : "var(--ink-3)",
            boxShadow: "0 4px 16px rgba(0,0,0,.12)",
            textAlign: "center",
            maxWidth: 190,
            pointerEvents: "none",
            transition: "border-color .12s, background .12s",
          }}
        >
          <div style={{ fontSize: 18, lineHeight: 1 }}>{"\uD83D\uDDD1"}</div>
          <div style={{ fontSize: 12, fontWeight: 600, marginTop: 4 }}>
            {overTrash ? "Release to remove" : "Remove person"}
          </div>
          <div style={{ fontSize: 11, marginTop: 2 }}>
            Drag a card here, or use the {"\uD83D\uDDD1"} on a card
          </div>
        </div>
      )}

      {drag && (
        <div
          data-testid="drag-ghost"
          style={{
            position: "fixed",
            left: drag.x + 10,
            top: drag.y + 10,
            zIndex: 70,
            padding: "6px 12px",
            borderRadius: 8,
            background: "var(--panel, #fff)",
            border: "1px solid var(--border-strong)",
            boxShadow: "0 6px 20px rgba(0,0,0,.2)",
            fontSize: 12,
            fontWeight: 600,
            pointerEvents: "none",
          }}
        >
          {drag.name}
        </div>
      )}

      {editPerson && (
        <CardEditModal
          person={editPerson}
          onClose={() => setEditPerson(null)}
          onSaved={async () => {
            setEditPerson(null);
            if (onReload) await onReload();
          }}
        />
      )}
    </div>
  );
}

function Connector() {
  return (
    <div className="connector">
      <span
        style={{
          position: "absolute",
          left: "50%",
          top: 0,
          bottom: 0,
          width: 2,
          background: "var(--border-strong)",
          transform: "translateX(-50%)",
        }}
      />
    </div>
  );
}

function UnitNode({ unit, st, t, filterSev, selected, onClick }) {
  const dim = filterSev && st.sev !== filterSev ? " dim" : "";
  const kind = unit.type === "shop" ? "shop" : "dept";
  const label =
    unit.type === "shop" ? "shop composite" : "department composite";
  return (
    <button
      className={"node lvl-" + st.sev + (selected ? " sel" : "") + dim}
      style={{ maxWidth: 320, minWidth: 260 }}
      onClick={onClick}
    >
      <div className="node-top">
        <Avatar
          kind={kind}
          sev={st.sev}
          dotStyle={t.dotStyle}
          size={40}
          square
          pulse={t.pulse}
        />
        <div style={{ flex: 1 }}>
          <div className="node-name">
            {unit.name}{" "}
            <span
              style={{ color: "var(--ink-3)", fontWeight: 400, fontSize: 11 }}
            >
              ({label})
            </span>
          </div>
          <div className="node-role">
            {unit.deptName || "Top-level department"}
          </div>
        </div>
        <SevTag sev={st.sev} />
      </div>
      {st.reasons[0] && (
        <div className="node-reason">
          <Dot sev={st.reasons[0].sev} style="filled" />
          <span>
            {st.reasons[0].text}
            {st.reasons.length > 1 ? ` · +${st.reasons.length - 1} more` : ""}
          </span>
        </div>
      )}
    </button>
  );
}

function ChildUnitNode({ unit, t, filterSev, onClick }) {
  const FL = window.FL;
  const st = FL.unitStatus(unit.id);
  const dim = filterSev && st.sev !== filterSev ? " dim" : "";
  const ppl = FL.peopleInUnit(unit.id);
  const kids = FL.childUnitsOf(unit.id);
  const kind = unit.type === "shop" ? "shop" : "dept";
  const metaBits = [ppl.length + (ppl.length === 1 ? " person" : " people")];
  if (kids.length > 0)
    metaBits.push(kids.length + (kids.length === 1 ? " group" : " groups"));
  return (
    <button
      className={"node lvl-" + st.sev + dim}
      style={{ maxWidth: 260, minWidth: 220 }}
      onClick={onClick}
      title={"Open " + unit.name}
    >
      <div className="node-top">
        <Avatar
          kind={kind}
          sev={st.sev}
          dotStyle={t.dotStyle}
          size={38}
          square
          pulse={t.pulse}
        />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="node-name">{unit.name}</div>
          <div className="node-role">{metaBits.join(" · ")}</div>
        </div>
        <SevTag sev={st.sev} />
      </div>
      {st.reasons[0] && (
        <div className="node-reason">
          <Dot sev={st.reasons[0].sev} style="filled" />
          <span>
            {st.reasons[0].text}
            {st.reasons.length > 1 ? ` · +${st.reasons.length - 1} more` : ""}
          </span>
        </div>
      )}
    </button>
  );
}

/**
 * Small login-status badge shown on org chart person cards (owner only —
 * the data behind it comes from an owner-gated admin endpoint). Hover
 * reveals the linked account email and its status.
 */
function LoginBadge({ login }) {
  const status = login.status;
  const style =
    status === "active"
      ? { color: "var(--green)", label: "Active login" }
      : status === "disabled"
        ? { color: "var(--ink-3)", label: "Login disabled" }
        : { color: "var(--orange)", label: "Invite pending" };
  return (
    <span
      className="login-badge"
      title={login.email + " · " + style.label}
      aria-label={"Login: " + style.label + " (" + login.email + ")"}
      style={{
        display: "inline-flex",
        alignItems: "center",
        justifyContent: "center",
        width: 18,
        height: 18,
        borderRadius: 9,
        flex: "0 0 auto",
        border: "1px solid " + style.color,
        color: style.color,
        background: "transparent",
        fontSize: 10,
        lineHeight: 1,
      }}
    >
      {status === "active"
        ? "\u2713"
        : status === "disabled"
          ? "\u2715"
          : "\u2709"}
    </span>
  );
}

function NodeCard({
  person,
  t,
  selected,
  onClick,
  quietGreens,
  filterSev,
  forcedSev,
  reasonOverride,
  showLogins,
  manage,
  onEdit,
  onOffboard,
  onDragStart,
}) {
  const FL = window.FL;
  const ps = FL.personStatus(person.id);
  const sev = forcedSev || ps.sev;
  const quiet = quietGreens && sev === "green";
  const dim = filterSev && sev !== filterSev ? " dim" : "";
  const reason =
    reasonOverride !== undefined
      ? reasonOverride
      : ps.reasons[0]
        ? ps.reasons[0].text
        : null;
  // Owner-only login badge, double-gated: the shell passes showLogins only
  // for the owner role AND FL.loginByEntity is only populated by an
  // owner-gated endpoint (and cleared on sign-out / non-owner sessions).
  const login =
    showLogins && FL.loginByEntity ? FL.loginByEntity[person.id] : null;
  return (
    <button
      className={
        "node lvl-" +
        sev +
        (selected ? " sel" : "") +
        (quiet ? " green-quiet" : "") +
        dim
      }
      onClick={onClick}
      onMouseDown={manage && onDragStart ? onDragStart : undefined}
      style={manage ? { cursor: "grab" } : undefined}
    >
      <div className="node-top">
        <Avatar
          kind="person"
          sev={sev}
          dotStyle={t.dotStyle}
          size={36}
          pulse={t.pulse}
        />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="node-name">{person.name}</div>
          <div className="node-role">{person.role}</div>
        </div>
        {manage && (
          <span
            style={{ display: "inline-flex", gap: 2, flex: "0 0 auto" }}
            onMouseDown={function (e) {
              // Keep the manage buttons clickable without arming a card drag.
              e.stopPropagation();
            }}
          >
            <span
              role="button"
              tabIndex={0}
              data-testid="card-edit"
              title={"Edit " + person.name + "'s card"}
              aria-label={"Edit " + person.name + "'s card"}
              style={{
                fontSize: 12,
                padding: "1px 4px",
                borderRadius: 4,
                color: "var(--ink-3)",
                cursor: "pointer",
              }}
              onClick={function (e) {
                e.stopPropagation();
                if (onEdit) onEdit();
              }}
            >
              {"\u270E"}
            </span>
            <span
              role="button"
              tabIndex={0}
              data-testid="card-offboard"
              title={"Remove " + person.name + " from the org chart"}
              aria-label={"Remove " + person.name + " from the org chart"}
              style={{
                fontSize: 12,
                padding: "1px 4px",
                borderRadius: 4,
                color: "var(--ink-3)",
                cursor: "pointer",
              }}
              onClick={function (e) {
                e.stopPropagation();
                if (onOffboard) onOffboard();
              }}
            >
              {"\uD83D\uDDD1"}
            </span>
          </span>
        )}
        {login && <LoginBadge login={login} />}
      </div>
      {reason && sev !== "green" && (
        <div className="node-reason">
          <Dot sev={sev} style="filled" />
          <span>{reason}</span>
        </div>
      )}
      {sev !== "green" && (
        <div className="node-foot">
          <SevTag sev={sev} />
          {ps.exceptions && ps.exceptions[0] && (
            <span className="route-when">
              {fmtElapsed(ps.exceptions[0].elapsedMin)} open
            </span>
          )}
        </div>
      )}
    </button>
  );
}

function VehicleCard({ vehicle, t, onClick }) {
  const sev = vehicle.status || "green";
  return (
    <button
      className={"node lvl-" + sev}
      style={{ minWidth: 176, maxWidth: 210 }}
      onClick={onClick}
      title={
        vehicle.operatorName ? "Open " + vehicle.operatorName : vehicle.name
      }
    >
      <div className="node-top">
        <Avatar
          kind="vehicle"
          sev={sev}
          dotStyle={t.dotStyle}
          size={36}
          square
          pulse={t.pulse}
        />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="node-name">{vehicle.name}</div>
          <div className="node-role">
            {vehicle.operatorName
              ? "Operated by " + vehicle.operatorName
              : "Unassigned"}
          </div>
        </div>
      </div>
    </button>
  );
}

Object.assign(window, {
  RegionView,
  UnitOrgChart,
  UnitCard,
  NodeCard,
  VehicleCard,
  Connector,
  UnitNode,
  ChildUnitNode,
});
