const { useState, useEffect, useRef, useMemo } = React;

/* ============================ Config ============================ */
const POSITIONS = ["Motion Designer", "Graphic Designer", "Innovation Manager"];
const POS_STYLE = {
  "Motion Designer":     { bg: "var(--motion-bg)",  fg: "var(--motion-fg)",  dot: "var(--motion-dot)",  short: "Motion" },
  "Graphic Designer":    { bg: "var(--graphic-bg)", fg: "var(--graphic-fg)", dot: "var(--graphic-dot)", short: "Graphic" },
  "Innovation Manager":  { bg: "var(--innov-bg)",   fg: "var(--innov-fg)",   dot: "var(--innov-dot)",   short: "Innovation" },
};
const PRODUCT_COLORS = ["#2f66ff", "#8b5cf6", "#14b8a6", "#f5635b", "#0ea5e9", "#ec4899", "#65a30d"];
const STORE_KEY = "nove8_designers_v1";
const APP_VERSION = "1.92"; // keep in sync with story_points_system.html on every bump
const PEOPLE_API = "/api/people";

// Shared roster lives on the server (Supabase) so role/access changes made by
// an admin reach everyone. localStorage is kept as an instant-paint cache and
// for the cross-document integrations (nove8-ext.js reads it).
async function fetchServerState() {
  try {
    const r = await fetch(PEOPLE_API, { cache: "no-store" });
    if (!r.ok) return null;
    const j = await r.json();
    return (j && j.state && Array.isArray(j.state.designers)) ? j.state : null;
  } catch (e) { return null; }
}
async function saveServerState(state) {
  try {
    await fetch(PEOPLE_API, {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(state),
    });
  } catch (e) {}
}

let _id = Date.now();
const uid = (p) => `${p}_${(_id++).toString(36)}`;

/* ============================ Seed data ============================ */
function seed() {
  const products = [
    { id: "p_woofz",   name: "Woofz",   color: "#2f66ff", order: ["Motion Designer", "Graphic Designer", "Innovation Manager"] },
    { id: "p_stylio",  name: "Stylio",  color: "#8b5cf6", order: ["Motion Designer", "Graphic Designer", "Innovation Manager"] },
    { id: "p_intellio",name: "Intellio",color: "#14b8a6", order: ["Motion Designer", "Graphic Designer", "Innovation Manager"] },
  ];
  const D = (name, email, access, position, productId) =>
    ({ id: uid("d"), name, email, access, position, productId, order: _id });
  const designers = [
    D("Sasha Sierikov", "aleksandr.sierikov@nove8.com", "admin",    "Motion Designer",    "p_woofz"),
    D("Galina Miros",   "galina.miros@nove8.com",       "designer", "Motion Designer",    "p_woofz"),
    D("Marta Kovalenko","marta.kovalenko@nove8.com",    "designer", "Graphic Designer",   "p_woofz"),
    D("Ivan Petrov",    "ivan.petrov@nove8.com",        "designer", "Graphic Designer",   "p_woofz"),
    D("Oleg Sokolov",   "oleg.sokolov@nove8.com",       "manager",  "Motion Designer",    "p_stylio"),
    D("Nina Orlova",    "nina.orlova@nove8.com",        "designer", "Graphic Designer",   "p_stylio"),
    D("Lena Vasileva",  "lena.vasileva@nove8.com",      "designer", "Innovation Manager", "p_stylio"),
    D("Dima Frolov",    "dima.frolov@nove8.com",        "manager",  "Motion Designer",    "p_intellio"),
    D("Katya Belova",   "katya.belova@nove8.com",       "designer", "Graphic Designer",   "p_intellio"),
    D("Roman Zaytsev",  "roman.zaytsev@nove8.com",      "designer", "Innovation Manager", null),
    D("Yulia Mints",    "yulia.mints@nove8.com",        "designer", "Motion Designer",    null),
  ];
  const M = (name, email) => ({ id: uid("m"), name, email, access: "manager", order: _id++ });
  const managers = [
    { id: uid("m"), name: "Anton Savchenko", email: "anton.savchenko@nove8.com", access: "admin", order: _id++ },
    { id: uid("m"), name: "Alex", email: "alex@nove8.com", access: "admin", order: _id++ },
    M("Andrei Volkov",  "andrei.volkov@nove8.com"),
    M("Daria Kuznetsova", "daria.kuznetsova@nove8.com"),
    M("Pavel Smirnov",  "pavel.smirnov@nove8.com"),
  ];
  return { products, designers, managers };
}

function load() {
  try {
    const raw = localStorage.getItem(STORE_KEY);
    if (raw) {
      const s = JSON.parse(raw);
      if (s && s.products && s.designers) {
        // dedupe ids defensively (older saves may collide)
        const seen = new Set();
        s.designers = s.designers.map((d) => {
          let id = d.id;
          while (seen.has(id)) id = uid("d");
          seen.add(id);
          return id === d.id ? d : { ...d, id };
        });
        if (!Array.isArray(s.managers)) s.managers = seed().managers;
        // ensure these emails are always present as admins (even on existing saves)
        [["Anton Savchenko", "anton.savchenko@nove8.com"], ["Alex", "alex@nove8.com"]].forEach(([nm, em]) => {
          if (![...(s.designers || []), ...(s.managers || [])]
                .some((p) => (p.email || "").trim().toLowerCase() === em)) {
            s.managers.push({ id: uid("m"), name: nm, email: em, access: "admin", order: _id++ });
          }
        });
        // one-time bootstrap: make sure at least one Admin exists
        const everyone = [...(s.designers || []), ...(s.managers || [])];
        if (everyone.length && !everyone.some((p) => p.access === "admin")) {
          const norm = (e) => (e || "").trim().toLowerCase();
          let target = s.designers.find((d) => norm(d.email) === "aleksandr.sierikov@nove8.com")
            || s.managers.find((m) => norm(m.email) === "aleksandr.sierikov@nove8.com")
            || s.designers.find((d) => d.access === "manager")
            || s.managers[0]
            || s.designers[0];
          if (target) target.access = "admin";
        }
        return s;
      }
    }
  } catch (e) {}
  return seed();
}

/* ============================ Small UI bits ============================ */
const initials = (n) => n.split(" ").filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("");

function Avatar({ name, position }) {
  const s = POS_STYLE[position] || {};
  return <div className="avatar" style={{ background: s.bg, color: s.fg }}>{initials(name)}</div>;
}

function Tag({ position }) {
  const s = POS_STYLE[position] || {};
  return <span className="tag" style={{ background: s.bg, color: s.fg }}>{position}</span>;
}

const ChevR = (p) => <svg className="chev" width="14" height="14" viewBox="0 0 14 14" {...p}><path d="M5 3l4 4-4 4" stroke="currentColor" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>;
const Up = () => <svg width="11" height="11" viewBox="0 0 12 12"><path d="M3 7.5L6 4.5l3 3" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>;
const Down = () => <svg width="11" height="11" viewBox="0 0 12 12"><path d="M3 4.5L6 7.5l3-3" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>;
const Plus = () => <svg width="14" height="14" viewBox="0 0 14 14"><path d="M7 2.5v9M2.5 7h9" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>;
const Trash = () => <svg width="14" height="14" viewBox="0 0 16 16"><path d="M3 4.5h10M6.5 4.5V3.2c0-.4.3-.7.7-.7h1.6c.4 0 .7.3.7.7v1.3M4.3 4.5l.5 7.8c0 .5.4.9.9.9h4.6c.5 0 .9-.4.9-.9l.5-7.8" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>;
const Key = () => <svg width="11" height="11" viewBox="0 0 12 12"><path d="M7.5 1.5a3 3 0 1 0 1.1 5.2L10 8.1l-1 1 1 1 1-1 .5-.5-3.4-3.4A3 3 0 0 0 7.5 1.5zm-.2 1.6a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8z" fill="currentColor"/></svg>;

/* Linked calculator names — maps this account to the designer name(s) used in
   the calculator, so their historical statistics attach to them. Free typing
   (commas), commits on blur. Resyncs if the roster updates from the server. */
function HandlesField({ value, fallback, onCommit }) {
  const initial = (value && value.length ? value : fallback).join(", ");
  const [txt, setTxt] = useState(initial);
  useEffect(() => { setTxt((value && value.length ? value : fallback).join(", ")); }, [JSON.stringify(value), JSON.stringify(fallback)]);
  return (
    <input className="tinput" type="text" value={txt}
      placeholder={fallback[0] || ""}
      onChange={(e) => setTxt(e.target.value)}
      onBlur={() => onCommit(txt.split(",").map((s) => s.trim()).filter(Boolean))} />
  );
}

/* ============================ Designer row ============================ */
function DesignerRow({ d, open, onToggle, onPatch, onRemove, onMove, first, last, onDragStart, onDragEnd, dragging }) {
  return (
    <div className={"drow" + (open ? " open" : "") + (dragging ? " dragging" : "")}>
      <div
        className="drow-head"
        draggable
        onDragStart={(e) => { e.dataTransfer.effectAllowed = "move"; onDragStart(); }}
        onDragEnd={onDragEnd}
        onClick={onToggle}
      >
        <Avatar name={d.name} position={d.position} />
        <div className="dmeta">
          <div className="dname">{d.name}</div>
          {!open && <div className="dsub">{d.email}</div>}
        </div>
        {!open && d.access === "manager" && <span className="mgr-badge" title="Manager access"><Key/>mgr</span>}
        <div className="row-reorder" onClick={(e) => e.stopPropagation()}>
          <button className="mini" disabled={first} onClick={() => onMove(-1)} title="Move up"><Up/></button>
          <button className="mini" disabled={last} onClick={() => onMove(1)} title="Move down"><Down/></button>
        </div>
        <ChevR />
      </div>

      {open && (
        <div className="drow-body" onClick={(e) => e.stopPropagation()}>
          <div className="field">
            <label>Email</label>
            <input className="tinput" type="email" value={d.email}
              placeholder="name@nove8.com"
              onChange={(e) => onPatch({ email: e.target.value })} />
          </div>
          <div className="two">
            <div className="field">
              <label>Position</label>
              <select className="tselect" value={d.position} onChange={(e) => onPatch({ position: e.target.value })}>
                {POSITIONS.map((p) => <option key={p} value={p}>{p}</option>)}
              </select>
            </div>
            <div className="field">
              <label>Access for this email</label>
              <div className="seg">
                <button className={d.access === "admin" ? "on adm" : ""} onClick={() => onPatch({ access: "admin" })}>Admin</button>
                <button className={d.access === "manager" ? "on mgr" : ""} onClick={() => onPatch({ access: "manager" })}>Manager</button>
                <button className={d.access === "designer" ? "on" : ""} onClick={() => onPatch({ access: "designer" })}>Designer</button>
              </div>
            </div>
          </div>
          <div className="field">
            <label>Linked calculator names <span style={{ color: "var(--muted)", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>· past stats are matched by these · comma-separated</span></label>
            <HandlesField value={d.handles} fallback={[d.name]} onCommit={(handles) => onPatch({ handles })} />
          </div>
          <div className="drow-foot">
            <button className="text-btn" onClick={onRemove}><Trash/> Remove from team</button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ============================ Add-designer inline form (Unassigned only) ============================ */
function AddDesigner({ onAdd }) {
  const [adding, setAdding] = useState(false);
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [position, setPosition] = useState(POSITIONS[0]);
  const [access, setAccess] = useState("designer");
  const ref = useRef(null);
  useEffect(() => { if (adding && ref.current) ref.current.focus(); }, [adding]);

  if (!adding) return <button className="add-row" onClick={() => setAdding(true)}><Plus/> Add</button>;

  const reset = () => { setName(""); setEmail(""); setPosition(POSITIONS[0]); setAccess("designer"); setAdding(false); };
  const submit = () => {
    if (!name.trim() || !email.trim()) return;
    onAdd({ name: name.trim(), email: email.trim(), position, access });
    reset();
  };
  return (
    <div className="add-form">
      <div className="field">
        <label>Name</label>
        <input ref={ref} className="tinput" placeholder="Designer name" value={name}
          onChange={(e) => setName(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Escape") reset(); }} />
      </div>
      <div className="field">
        <label>Email <span style={{ color: "var(--muted)", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>· access is granted to this address</span></label>
        <input className="tinput" type="email" placeholder="name@nove8.com" value={email}
          onChange={(e) => setEmail(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") reset(); }} />
      </div>
      <div className="two">
        <div className="field">
          <label>Position</label>
          <select className="tselect" value={position} onChange={(e) => setPosition(e.target.value)}>
            {POSITIONS.map((p) => <option key={p} value={p}>{p}</option>)}
          </select>
        </div>
        <div className="field">
          <label>Access</label>
          <div className="seg">
            <button className={access === "admin" ? "on adm" : ""} onClick={() => setAccess("admin")}>Admin</button>
            <button className={access === "manager" ? "on mgr" : ""} onClick={() => setAccess("manager")}>Manager</button>
            <button className={access === "designer" ? "on" : ""} onClick={() => setAccess("designer")}>Designer</button>
          </div>
        </div>
      </div>
      <div className="btns">
        <button className="btn primary" disabled={!name.trim() || !email.trim()} onClick={submit}>Add</button>
        <button className="btn ghost" onClick={reset}>Cancel</button>
      </div>
    </div>
  );
}

/* ============================ Product card ============================ */
function ProductCard({ product, designers, expanded, dragId, dropPid, actions }) {
  const [editName, setEditName] = useState(false);
  const [nameVal, setNameVal] = useState(product.name);
  const nameRef = useRef(null);
  useEffect(() => { if (editName && nameRef.current) { nameRef.current.focus(); nameRef.current.select(); } }, [editName]);

  const members = designers.filter((d) => d.productId === product.id);
  const order = product.order && product.order.length ? product.order : POSITIONS;
  const groups = order
    .map((pos) => ({ pos, items: members.filter((m) => m.position === pos).sort((a, b) => a.order - b.order) }))
    .filter((g) => g.items.length > 0);

  const commitName = () => { const v = nameVal.trim(); if (v) actions.renameProduct(product.id, v); else setNameVal(product.name); setEditName(false); };

  return (
    <div
      className={"pcard" + (dropPid === product.id ? " drop" : "")}
      onDragOver={(e) => { if (dragId) { e.preventDefault(); actions.setDrop(product.id); } }}
      onDragLeave={(e) => { if (e.currentTarget === e.target) actions.setDrop(null); }}
      onDrop={(e) => { e.preventDefault(); actions.assign(dragId, product.id); }}
    >
      <div className="pcard-head">
        <span className="pcard-stripe" style={{ background: product.color }}></span>
        <div className="pcard-title">
          {editName
            ? <input ref={nameRef} value={nameVal} onChange={(e) => setNameVal(e.target.value)}
                onBlur={commitName}
                onKeyDown={(e) => { if (e.key === "Enter") commitName(); if (e.key === "Escape") { setNameVal(product.name); setEditName(false); } }} />
            : <span onClick={() => setEditName(true)} title="Rename" style={{ cursor: "text" }}>{product.name}</span>}
        </div>
        <span className="pcount">{members.length}</span>
        <button className="icon-btn danger" title="Delete product" onClick={() => actions.removeProduct(product.id)}><Trash/></button>
      </div>

      <div className={"pcard-body" + (members.length === 0 ? " empty" : "")}>
        {groups.map((g, gi) => {
          const s = POS_STYLE[g.pos];
          return (
            <div className="group" key={g.pos}>
              <div className="group-head">
                <span className="group-label"><span className="gdot" style={{ background: s.dot }}></span>{g.pos} · {g.items.length}</span>
                <span className="group-line"></span>
                <span className="group-move">
                  <button className="mini" disabled={gi === 0} onClick={() => actions.moveGroup(product.id, g.pos, -1)} title="Move group up"><Up/></button>
                  <button className="mini" disabled={gi === groups.length - 1} onClick={() => actions.moveGroup(product.id, g.pos, 1)} title="Move group down"><Down/></button>
                </span>
              </div>
              {g.items.map((d, i) => (
                <DesignerRow key={d.id} d={d}
                  open={expanded.has(d.id)}
                  onToggle={() => actions.toggle(d.id)}
                  onPatch={(patch) => actions.patch(d.id, patch)}
                  onRemove={() => actions.unassign(d.id)}
                  onMove={(dir) => actions.moveInGroup(d.id, dir)}
                  first={i === 0} last={i === g.items.length - 1}
                  dragging={dragId === d.id}
                  onDragStart={() => actions.setDrag(d.id)}
                  onDragEnd={() => actions.endDrag()} />
              ))}
            </div>
          );
        })}
        {members.length === 0 && <div className="empty-hint">No designers yet — drag someone here from Unassigned</div>}
      </div>
    </div>
  );
}

/* ============================ Unassigned card ============================ */
function UnassignedCard({ designers, expanded, dragId, dropPid, actions }) {
  const members = designers.filter((d) => d.productId === null).sort((a, b) => a.order - b.order);
  return (
    <div
      className={"pcard ghost" + (dropPid === "__none__" ? " drop" : "")}
      onDragOver={(e) => { if (dragId) { e.preventDefault(); actions.setDrop("__none__"); } }}
      onDragLeave={(e) => { if (e.currentTarget === e.target) actions.setDrop(null); }}
      onDrop={(e) => { e.preventDefault(); actions.assign(dragId, null); }}
    >
      <div className="pcard-head">
        <span className="pcard-stripe" style={{ background: "#c2c8d0" }}></span>
        <div className="pcard-title">Unassigned</div>
        <span className="pcount">{members.length}</span>
      </div>
      <div className={"pcard-body" + (members.length === 0 ? " empty" : "")}>
        {members.map((d, i) => (
          <DesignerRow key={d.id} d={d}
            open={expanded.has(d.id)}
            onToggle={() => actions.toggle(d.id)}
            onPatch={(patch) => actions.patch(d.id, patch)}
            onRemove={() => actions.deleteDesigner(d.id)}
            onMove={(dir) => actions.moveInGroup(d.id, dir)}
            first={i === 0} last={i === members.length - 1}
            dragging={dragId === d.id}
            onDragStart={() => actions.setDrag(d.id)}
            onDragEnd={() => actions.endDrag()} />
        ))}
        {members.length === 0 && <div className="empty-hint">Everyone is assigned to a product 🎉</div>}
        <AddDesigner onAdd={(payload) => actions.addDesigner(payload)} />
      </div>
    </div>
  );
}

/* ============================ Add product card ============================ */
function AddProduct({ onAdd }) {
  const [adding, setAdding] = useState(false);
  const [name, setName] = useState("");
  const ref = useRef(null);
  useEffect(() => { if (adding && ref.current) ref.current.focus(); }, [adding]);
  if (!adding) return <button className="addp" onClick={() => setAdding(true)}><span><Plus/><br/>Add product</span></button>;
  const submit = () => { if (!name.trim()) return; onAdd(name.trim()); setName(""); setAdding(false); };
  return (
    <div className="addp-form">
      <input ref={ref} className="tinput" placeholder="Product name" value={name}
        onChange={(e) => setName(e.target.value)}
        onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") setAdding(false); }} />
      <div className="btns" style={{ display: "flex", gap: 8 }}>
        <button className="btn primary" disabled={!name.trim()} onClick={submit}>Create</button>
        <button className="btn ghost" onClick={() => setAdding(false)}>Cancel</button>
      </div>
    </div>
  );
}

/* ============================ Managers card ============================ */
function ManagerRow({ m, open, onToggle, onPatch, onRemove }) {
  return (
    <div className={"drow" + (open ? " open" : "")}>
      <div className="drow-head" onClick={onToggle}>
        <div className="avatar" style={{ background: "var(--blue-soft)", color: "var(--blue)" }}>{initials(m.name)}</div>
        <div className="dmeta">
          <div className="dname">{m.name}</div>
          {!open && <div className="dsub">{m.email}</div>}
        </div>
        {!open && <span className="mgr-badge" title="Manager access"><Key/>mgr</span>}
        <ChevR />
      </div>
      {open && (
        <div className="drow-body" onClick={(e) => e.stopPropagation()}>
          <div className="field">
            <label>Email</label>
            <input className="tinput" type="email" value={m.email} placeholder="name@nove8.com"
              onChange={(e) => onPatch({ email: e.target.value })} />
          </div>
          <div className="field">
            <label>Access for this email</label>
            <div className="seg">
              <button className={m.access === "admin" ? "on adm" : ""} onClick={() => onPatch({ access: "admin" })}>Admin</button>
              <button className={m.access === "manager" ? "on mgr" : ""} onClick={() => onPatch({ access: "manager" })}>Manager</button>
              <button className={m.access === "designer" ? "on" : ""} onClick={() => onPatch({ access: "designer" })}>Designer</button>
            </div>
          </div>
          <div className="drow-foot">
            <button className="text-btn" onClick={onRemove}><Trash/> Remove manager</button>
          </div>
        </div>
      )}
    </div>
  );
}

function AddManager({ onAdd }) {
  const [adding, setAdding] = useState(false);
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [access, setAccess] = useState("manager");
  const ref = useRef(null);
  useEffect(() => { if (adding && ref.current) ref.current.focus(); }, [adding]);
  if (!adding) return <button className="add-row" onClick={() => setAdding(true)}><Plus/> Add manager</button>;
  const reset = () => { setName(""); setEmail(""); setAccess("manager"); setAdding(false); };
  const submit = () => { if (!name.trim() || !email.trim()) return; onAdd({ name: name.trim(), email: email.trim(), access }); reset(); };
  return (
    <div className="add-form">
      <div className="field">
        <label>Name</label>
        <input ref={ref} className="tinput" placeholder="Manager name" value={name}
          onChange={(e) => setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") reset(); }} />
      </div>
      <div className="field">
        <label>Email <span style={{ color: "var(--muted)", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>· access is granted to this address</span></label>
        <input className="tinput" type="email" placeholder="name@nove8.com" value={email}
          onChange={(e) => setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") reset(); }} />
      </div>
      <div className="field">
        <label>Access</label>
        <div className="seg">
          <button className={access === "admin" ? "on adm" : ""} onClick={() => setAccess("admin")}>Admin</button>
          <button className={access === "manager" ? "on mgr" : ""} onClick={() => setAccess("manager")}>Manager</button>
          <button className={access === "designer" ? "on" : ""} onClick={() => setAccess("designer")}>Designer</button>
        </div>
      </div>
      <div className="btns">
        <button className="btn primary" disabled={!name.trim() || !email.trim()} onClick={submit}>Add</button>
        <button className="btn ghost" onClick={reset}>Cancel</button>
      </div>
    </div>
  );
}

function ManagersCard({ managers, expanded, actions }) {
  return (
    <div className="pcard mcard">
      <div className="pcard-head">
        <span className="pcard-stripe" style={{ background: "var(--blue)" }}></span>
        <div className="pcard-title">Managers</div>
        <span className="pcount">{managers.length}</span>
      </div>
      <div className={"pcard-body" + (managers.length === 0 ? " empty" : "")}>
        {managers.map((m) => (
          <ManagerRow key={m.id} m={m}
            open={expanded.has(m.id)}
            onToggle={() => actions.toggleM(m.id)}
            onPatch={(patch) => actions.patchM(m.id, patch)}
            onRemove={() => actions.removeM(m.id)} />
        ))}
        {managers.length === 0 && <div className="empty-hint">No managers yet</div>}
        <AddManager onAdd={actions.addManager} />
      </div>
    </div>
  );
}

/* ============================ Statistics ============================ */
function Stats({ products, designers, onReset }) {
  const total = designers.length;
  const assigned = designers.filter((d) => d.productId !== null).length;
  const unassigned = total - assigned;
  const managers = designers.filter((d) => d.access === "manager").length;

  const byPos = POSITIONS.map((p) => ({ pos: p, n: designers.filter((d) => d.position === p).length }));
  const maxPos = Math.max(1, ...byPos.map((x) => x.n));

  const byProduct = products.map((pr) => {
    const mem = designers.filter((d) => d.productId === pr.id);
    return { pr, n: mem.length, parts: POSITIONS.map((p) => mem.filter((d) => d.position === p).length) };
  });
  const maxProd = Math.max(1, ...byProduct.map((x) => x.n));

  return (
    <div className="stats">
      <div className="stats-card">
        <h2>Team overview</h2>
        <div className="sub">Live across all products</div>

        <div className="bignum">
          <span className="n">{total}</span>
          <span className="lbl">designers<br/>in the team</span>
        </div>
        <div className="chips">
          <span className="chip">{assigned} assigned</span>
          <span className={"chip" + (unassigned ? " warn" : "")}>{unassigned} unassigned</span>
          <span className="chip">{managers} manager{managers === 1 ? "" : "s"}</span>
        </div>

        <div className="sect">
          <div className="sect-h">By position</div>
          {byPos.map((x) => {
            const s = POS_STYLE[x.pos];
            return (
              <div className="stat-row" key={x.pos}>
                <span className="gdot" style={{ background: s.dot }}></span>
                <div>
                  <div className="nm" style={{ marginBottom: 5 }}>{x.pos}</div>
                  <div className="bar"><span style={{ width: (x.n / maxPos * 100) + "%", background: s.dot }}></span></div>
                </div>
                <span className="ct">{x.n}</span>
              </div>
            );
          })}
        </div>

        <div className="sect">
          <div className="sect-h">By product</div>
          {byProduct.map(({ pr, n, parts }) => (
            <div className="prod-row" key={pr.id}>
              <span className="pcard-stripe" style={{ background: pr.color }}></span>
              <div style={{ minWidth: 0 }}>
                <div className="nm" style={{ marginBottom: 5 }}><b>{pr.name}</b></div>
                <div className="seg-bar">
                  {parts.map((c, i) => c > 0 && (
                    <span key={i} style={{ width: (c / maxProd * 100) + "%", background: POS_STYLE[POSITIONS[i]].dot }}></span>
                  ))}
                </div>
              </div>
              <span className="ct">{n}</span>
            </div>
          ))}
          {byProduct.length === 0 && <div className="empty-hint">No products yet</div>}
          <div className="legend">
            {POSITIONS.map((p) => (
              <span key={p}><span className="gdot" style={{ background: POS_STYLE[p].dot }}></span>{POS_STYLE[p].short}</span>
            ))}
          </div>
        </div>

        <div className="stats-foot">
          <span style={{ fontSize: 12, color: "var(--muted)", fontWeight: 600 }}>{products.length} product{products.length === 1 ? "" : "s"}</span>
          <button className="reset" onClick={onReset}>Reset demo data</button>
        </div>
      </div>
    </div>
  );
}

/* ============================ App ============================ */
const TABS = ["Story Points", "People", "Designers Tables", "Catalog", "Statistics"];

/* Embeds the real Story Points app (story_points_system.html) and hides its own sidebar,
   so our prototype's nav drives it. Same code = identical calculator + items +
   statistics logic. `view` selects which internal tab is shown. */
function EmbeddedApp({ view }) {
  const ref = useRef(null);
  const loaded = useRef(false);

  const applyView = () => {
    try {
      const doc = ref.current && ref.current.contentDocument;
      if (!doc) return;
      const target = view === "statistics" ? "statistics" : "calculator";
      const btn = doc.querySelector('.sidebar-tab[data-tab="' + target + '"]');
      if (btn) btn.click();
    } catch (e) {}
  };

  const onLoad = () => {
    try {
      const doc = ref.current && ref.current.contentDocument;
      if (!doc) return;
      if (!doc.getElementById("__sp_embed_css")) {
        const s = doc.createElement("style");
        s.id = "__sp_embed_css";
        s.textContent = ".sidebar{display:none!important} .app-layout{display:block!important} .app-main{width:100%!important;min-height:100vh!important} .app-footer{display:none!important} .update-banner{display:none!important}";
        doc.head.appendChild(s);
      }
      loaded.current = true;
      applyView();
    } catch (e) {}
  };

  useEffect(() => { if (loaded.current) applyView(); }, [view]);

  return <iframe ref={ref} className="sp-frame" src="story_points_system.html" onLoad={onLoad} title="Story Points"></iframe>;
}

function App({ auth }) {
  const init = useMemo(load, []);
  const [products, setProducts] = useState(init.products);
  const [designers, setDesigners] = useState(init.designers);
  const [managers, setManagers] = useState(init.managers || []);
  const [expanded, setExpanded] = useState(() => new Set());
  const [dragId, setDragId] = useState(null);
  const [dropPid, setDropPid] = useState(null);
  const [tab, setTab] = useState("People");
  const [loaded, setLoaded] = useState(false);
  // Live-sync bookkeeping: appliedSigRef = signature of the last roster we
  // fetched or wrote; lastEditRef = when this admin last edited (so a background
  // poll can't stomp an in-flight change).
  const appliedSigRef = useRef("");
  const rosterSaveTimer = useRef(null);
  const lastEditRef = useRef(0);

  const access = auth ? auth.access : "manager";
  const isAdmin = access === "admin";
  // Only admins can switch view. Managers always see manager view; designers their own.
  const [adminRole, setAdminRole] = useState(() => {
    try { return localStorage.getItem("nove8_role") || "manager"; } catch (e) { return "manager"; }
  });
  const role = isAdmin ? adminRole : (access === "designer" ? "designer" : "manager");

  const [meId, setMeId] = useState(() => {
    if (auth && auth.person && auth.person.kind === "designer") return auth.person.id;
    try { return localStorage.getItem("nove8_me") || ""; } catch (e) { return ""; }
  });

  const DESIGNER_TABS = ["Story Points", "My Table"];
  const visibleTabs = role === "designer" ? DESIGNER_TABS : TABS;
  const effectiveMe = (designers.find((d) => d.id === meId) || designers[0] || null);

  // A plain designer is always locked to their own table.
  useEffect(() => {
    if (!isAdmin && access === "designer" && auth && auth.person && auth.person.kind === "designer") {
      setMeId(auth.person.id);
    }
  }, [isAdmin, access]);

  useEffect(() => {
    try { localStorage.setItem("nove8_role", adminRole); } catch (e) {}
  }, [adminRole]);
  useEffect(() => {
    if (role === "designer" && !DESIGNER_TABS.includes(tab)) setTab("Story Points");
    if (role !== "designer" && !TABS.includes(tab)) setTab("People");
  }, [role]);
  useEffect(() => { try { localStorage.setItem("nove8_me", meId); } catch (e) {} }, [meId]);

  // Bind the embedded calculator to the signed-in designer: their past stats
  // attach to them and new calcs are attributed to their account. Admins and
  // managers are left unbound (free choice + all stats), exactly like before.
  useEffect(() => {
    try {
      const isRealDesigner = access === "designer" && auth && auth.person && auth.person.kind === "designer";
      const handles = isRealDesigner
        ? ((auth.person.handles && auth.person.handles.length) ? auth.person.handles : [auth.person.name])
        : null;
      if (handles && handles.length) localStorage.setItem("nove8_calc_identity", JSON.stringify({ handles, lock: true }));
      else localStorage.removeItem("nove8_calc_identity");
      // expose the shell role so the embedded calculator can gate its Edit button
      localStorage.setItem("nove8_shell_role", access || "");
    } catch (e) {}
  }, [access, auth && auth.email, auth && auth.person && JSON.stringify(auth.person.handles)]);

  // Hydrate the shared catalog (task types / types / funnels) from the server
  // into the localStorage keys every consumer reads (tables.jsx, catalog.jsx,
  // nove8-ext.js in the calculator iframe), then keep it fresh with a quiet
  // poll. Skipped briefly after a local catalog edit so a stale poll can't
  // stomp an in-flight change.
  useEffect(() => {
    let alive = true;
    const applyCatalog = (cat) => {
      if (!cat) return;
      try {
        const editTs = Number(localStorage.getItem("nove8_catalog_edit_ts") || 0);
        if (Date.now() - editTs < 45000) return;
        let changed = false;
        const put = (key, arr) => {
          if (!Array.isArray(arr)) return;
          const next = JSON.stringify(arr);
          if (localStorage.getItem(key) !== next) { localStorage.setItem(key, next); changed = true; }
        };
        put("nove8_tasktypes_v1", cat.taskTypes && cat.taskTypes.length ? cat.taskTypes : null);
        put("nove8_types_v1", cat.types && cat.types.length ? cat.types : null);
        put("nove8_funnels_v1", cat.funnels);
        if (changed) window.dispatchEvent(new Event("nove8-catalog-changed"));
      } catch (e) {}
    };
    const readLS = (k) => {
      try { const r = localStorage.getItem(k); if (r) { const a = JSON.parse(r); if (Array.isArray(a)) return a; } } catch (e) {}
      return null;
    };
    const fetchCat = (initial) => {
      fetch("/api/catalog", { cache: "no-store" })
        .then((r) => (r.ok ? r.json() : null))
        .then((j) => {
          if (!alive || !j) return;
          if (j.state) { applyCatalog(j.state); return; }
          // Server catalog is empty: seed it from this admin's local lists so
          // edits made before the catalog became server-backed reach everyone.
          if (initial && access === "admin") {
            const doc = {
              taskTypes: readLS("nove8_tasktypes_v1") || [],
              types: readLS("nove8_types_v1") || [],
              funnels: readLS("nove8_funnels_v1") || [],
            };
            if (doc.taskTypes.length || doc.types.length || doc.funnels.length) {
              fetch("/api/catalog", {
                method: "PUT",
                headers: { "content-type": "application/json" },
                body: JSON.stringify(doc),
              }).catch(() => {});
            }
          }
        })
        .catch(() => {});
    };
    fetchCat(true);
    const id = setInterval(() => fetchCat(false), 30000);
    return () => { alive = false; clearInterval(id); };
  }, []);

  // Pull the shared roster from the server on mount. The server wins over the
  // local cache, so a demoted person's stale device can't keep admin access.
  useEffect(() => {
    let alive = true;
    fetchServerState().then((srv) => {
      if (!alive) return;
      if (srv) {
        appliedSigRef.current = JSON.stringify({ products: srv.products, designers: srv.designers, managers: srv.managers });
        setProducts(srv.products || []);
        setDesigners(srv.designers || []);
        setManagers(srv.managers || []);
      }
      setLoaded(true);
    });
    return () => { alive = false; };
  }, []);

  useEffect(() => {
    const state = { products, designers, managers };
    const sig = JSON.stringify(state);
    try { localStorage.setItem(STORE_KEY, sig); } catch (e) {}
    if (!loaded) return;
    // A change matching the last applied signature came from the initial load
    // or a background poll — don't treat it as a local edit or echo it back.
    if (sig === appliedSigRef.current) return;
    appliedSigRef.current = sig;
    lastEditRef.current = Date.now();
    // Only admins publish to the shared store (write-through, debounced —
    // colour dragging and drag-and-drop fire state changes in bursts).
    if (isAdmin) {
      if (rosterSaveTimer.current) clearTimeout(rosterSaveTimer.current);
      rosterSaveTimer.current = setTimeout(() => saveServerState(state), 600);
    }
  }, [products, designers, managers, loaded, isAdmin]);

  // People is the source of truth for the CURRENT team: reconcile the
  // calculator's designer registry (/api/designers: handle + product) after
  // roster edits, so product filters and the calc designer list follow People.
  // History in task_evaluations is untouched — removed people stay in stats.
  const registryTimer = useRef(null);
  useEffect(() => {
    if (!loaded || !isAdmin || !designers.length) return;
    if (registryTimer.current) clearTimeout(registryTimer.current);
    registryTimer.current = setTimeout(async () => {
      try {
        const r = await fetch("/api/designers", { cache: "no-store" });
        if (!r.ok) return;
        const j = await r.json();
        const existing = new Map((Array.isArray(j.designers) ? j.designers : []).map((d) => [d.handle, d.product || null]));
        const prodName = (pid) => { const p = products.find((x) => x.id === pid); return p ? p.name : null; };
        const desired = new Map();
        designers.forEach((d) => {
          const handle = String((d.handles && d.handles[0]) || d.name || "").trim();
          if (handle) desired.set(handle, d.productId ? prodName(d.productId) : null);
        });
        const hdr = { "content-type": "application/json" };
        for (const [handle, product] of desired) {
          if (!existing.has(handle)) {
            await fetch("/api/designers", { method: "POST", headers: hdr, body: JSON.stringify({ handle }) });
            if (product) await fetch("/api/designers", { method: "PATCH", headers: hdr, body: JSON.stringify({ handle, product }) });
          } else if (existing.get(handle) !== product) {
            await fetch("/api/designers", { method: "PATCH", headers: hdr, body: JSON.stringify({ handle, product }) });
          }
        }
        for (const [handle] of existing) {
          if (!desired.has(handle)) {
            await fetch("/api/designers?handle=" + encodeURIComponent(handle), { method: "DELETE" });
          }
        }
      } catch (e) {}
    }, 1500);
    return () => { if (registryTimer.current) clearTimeout(registryTimer.current); };
  }, [designers, products, loaded, isAdmin]);

  // Quiet background sync: keep long-open tabs current without a page reload.
  // The server roster is applied in place; React diffs it, so there is no flicker.
  useEffect(() => {
    const POLL_MS = 30000;
    const id = setInterval(async () => {
      const srv = await fetchServerState();
      if (!srv) return;
      const sig = JSON.stringify({ products: srv.products, designers: srv.designers, managers: srv.managers });
      if (sig === appliedSigRef.current) return;                          // nothing new
      if (isAdmin && Date.now() - lastEditRef.current < POLL_MS) return;  // don't stomp an active editor
      appliedSigRef.current = sig;
      setProducts(srv.products || []);
      setDesigners(srv.designers || []);
      setManagers(srv.managers || []);
    }, POLL_MS);
    return () => clearInterval(id);
  }, [isAdmin]);

  const patchDesigner = (id, patch) => setDesigners((ds) => ds.map((d) => d.id === id ? { ...d, ...patch } : d));

  const actions = {
    toggle: (id) => setExpanded((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }),
    patch: patchDesigner,
    setDrag: (id) => setDragId(id),
    endDrag: () => { setDragId(null); setDropPid(null); },
    setDrop: (pid) => setDropPid(pid),

    assign: (id, pid) => {
      if (!id) return;
      setDesigners((ds) => {
        const maxOrder = Math.max(0, ...ds.filter((d) => d.productId === pid).map((d) => d.order));
        return ds.map((d) => d.id === id ? { ...d, productId: pid, order: maxOrder + 1 } : d);
      });
      setDragId(null); setDropPid(null);
    },

    moveInGroup: (id, dir) => setDesigners((ds) => {
      const me = ds.find((d) => d.id === id);
      if (!me) return ds;
      const group = ds.filter((d) => d.productId === me.productId && d.position === me.position).sort((a, b) => a.order - b.order);
      const idx = group.findIndex((d) => d.id === id);
      const swap = group[idx + dir];
      if (!swap) return ds;
      return ds.map((d) => d.id === me.id ? { ...d, order: swap.order } : d.id === swap.id ? { ...d, order: me.order } : d);
    }),

    moveGroup: (pid, pos, dir) => setProducts((ps) => ps.map((p) => {
      if (p.id !== pid) return p;
      const order = (p.order && p.order.length ? [...p.order] : [...POSITIONS]);
      const i = order.indexOf(pos);
      const j = i + dir;
      if (i < 0 || j < 0 || j >= order.length) return p;
      [order[i], order[j]] = [order[j], order[i]];
      return { ...p, order };
    })),

    addDesigner: ({ name, email, position, access }) => setDesigners((ds) => {
      const maxOrder = Math.max(0, ...ds.map((d) => d.order));
      return [...ds, { id: uid("d"), name, email, access, position, productId: null, order: maxOrder + 1 }];
    }),

    unassign: (id) => patchDesigner(id, { productId: null }),
    deleteDesigner: (id) => setDesigners((ds) => ds.filter((d) => d.id !== id)),

    addProduct: (name) => setProducts((ps) => {
      const color = PRODUCT_COLORS[ps.length % PRODUCT_COLORS.length];
      return [...ps, { id: uid("p"), name, color, order: [...POSITIONS] }];
    }),
    renameProduct: (id, name) => setProducts((ps) => ps.map((p) => p.id === id ? { ...p, name } : p)),
    removeProduct: (id) => {
      setDesigners((ds) => ds.map((d) => d.productId === id ? { ...d, productId: null } : d));
      setProducts((ps) => ps.filter((p) => p.id !== id));
    },

    toggleM: (id) => setExpanded((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }),
    patchM: (id, patch) => setManagers((ms) => ms.map((m) => m.id === id ? { ...m, ...patch } : m)),
    removeM: (id) => setManagers((ms) => ms.filter((m) => m.id !== id)),
    addManager: ({ name, email, access }) => setManagers((ms) => [...ms, { id: uid("m"), name, email, access, order: Date.now() }]),
  };

  const reset = () => {
    if (!confirm("Reset to demo data? Your changes will be lost.")) return;
    const s = seed();
    setProducts(s.products); setDesigners(s.designers); setManagers(s.managers); setExpanded(new Set());
  };

  // Pull the existing designers from the calculator's DB list (/api/designers,
  // handle + product) into People as accounts. Adds missing products, links the
  // person's handle so their stats attach, skips names already present. Emails
  // stay blank — the admin fills them so the person can sign in.
  const importFromCalculator = async () => {
    // Collect designer names from two places: the registry (/api/designers,
    // carries product) and anyone who already has calculations
    // (/api/task-evaluations distinct designer). Merge, registry wins on product.
    const byKey = new Map(); // lowercased name -> { name, product }
    try {
      const r = await fetch("/api/designers", { cache: "no-store" });
      if (r.ok) {
        const j = await r.json();
        (Array.isArray(j.designers) ? j.designers : []).forEach((d) => {
          const name = (d && typeof d.handle === "string") ? d.handle.trim() : "";
          if (name) byKey.set(name.toLowerCase(), { name, product: (d.product || "").trim() });
        });
      }
    } catch (e) {}
    try {
      const r = await fetch("/api/task-evaluations", { cache: "no-store" });
      if (r.ok) {
        const j = await r.json();
        (Array.isArray(j.events) ? j.events : []).forEach((ev) => {
          const name = String((ev && ev.designer) || "").trim();
          if (name && !byKey.has(name.toLowerCase())) byKey.set(name.toLowerCase(), { name, product: "" });
        });
      }
    } catch (e) {}
    const totalFetched = byKey.size;
    const list = [...byKey.values()].map((x) => ({ handle: x.name, product: x.product }));
    const haveName = new Set(designers.map((d) => (d.name || "").trim().toLowerCase()));
    const prodByName = new Map(products.map((p) => [(p.name || "").trim().toLowerCase(), p]));
    const newProducts = [...products];
    const newDesigners = [...designers];
    let added = 0, prodAdded = 0, ord = Date.now();
    list.forEach((row) => {
      const handle = (row && typeof row.handle === "string") ? row.handle.trim() : "";
      if (!handle) return;
      let pid = null;
      const pname = (row.product || "").trim();
      if (pname) {
        let p = prodByName.get(pname.toLowerCase());
        if (!p) {
          p = { id: uid("p"), name: pname, color: PRODUCT_COLORS[newProducts.length % PRODUCT_COLORS.length], order: [...POSITIONS] };
          prodByName.set(pname.toLowerCase(), p); newProducts.push(p); prodAdded++;
        }
        pid = p.id;
      }
      if (haveName.has(handle.toLowerCase())) return;
      haveName.add(handle.toLowerCase());
      newDesigners.push({ id: uid("d"), name: handle, email: "", access: "designer", position: POSITIONS[0], productId: pid, handles: [handle], order: ord++ });
      added++;
    });
    setProducts(newProducts);
    setDesigners(newDesigners);
    return { added, prodAdded, totalFetched };
  };
  const doImport = async () => {
    const res = await importFromCalculator();
    try {
      alert(
        `Получено имён из базы: ${res.totalFetched}.\n` +
        `Добавлено новых: ${res.added}` + (res.prodAdded ? `, продуктов: ${res.prodAdded}` : "") + ".\n" +
        (res.totalFetched === 0
          ? "\nБаза вернула пусто — возможно, не загрузилась свежая версия (проверь v1.55 внизу сайдбара и обнови Cmd+Shift+R), либо запрос к API не прошёл."
          : res.added === 0
            ? "\nВсе эти имена уже есть в People."
            : "\nУ импортированных пустой email — добавь почты в карточках, иначе они не смогут войти.")
      );
    } catch (e) {}
  };

  return (
    <div className="frame">
      <div className="app">
        <aside className="sidebar">
          {isAdmin && (
            <div className="role-switch" role="group" aria-label="View as">
              <span className="role-label">View as</span>
              <div className="role-seg">
                <button className={role === "manager" ? "on" : ""} onClick={() => setAdminRole("manager")}>Manager</button>
                <button className={role === "designer" ? "on" : ""} onClick={() => setAdminRole("designer")}>Designer</button>
              </div>
            </div>
          )}
          {visibleTabs.map((t) => (
            <div key={t} className={"tab" + (t === tab ? " active" : "")} onClick={() => setTab(t)}>{t}</div>
          ))}
          {isAdmin && role === "designer" && (
            <div className="role-me">
              <span className="role-label">Viewing as</span>
              <select className="role-me-sel" value={effectiveMe ? effectiveMe.id : ""} onChange={(e) => setMeId(e.target.value)}>
                {designers.map((d) => <option key={d.id} value={d.id}>{d.name}</option>)}
              </select>
            </div>
          )}
          {auth && (
            <div className="me-box">
              <div className="me-who">
                <span className="me-av">{(auth.name || auth.email).split(" ").filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("")}</span>
                <div className="me-meta">
                  <div className="me-name">
                    {auth.name || auth.email}
                    {access === "admin" && <span className="me-tag admin">admin</span>}
                    {access === "manager" && <span className="me-tag mgr">mgr</span>}
                  </div>
                  <div className="me-email">{auth.email}</div>
                </div>
              </div>
              <button className="logout-btn" onClick={auth.signOut}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg>
                Log out
              </button>
            </div>
          )}
          <div className="app-ver" style={{ textAlign: "center", paddingTop: "12px", fontSize: "11px", fontWeight: 700, letterSpacing: ".03em", color: "var(--muted)" }}>v{APP_VERSION}</div>
        </aside>

        <div className="view">
          {tab === "People" && (
            <div className="dview">
              <main className="content">
                <div className="topbar">
                  <div>
                    <h1>People</h1>
                    <p>Managers &amp; designers · drag designers between products, expand a card to set email &amp; access</p>
                  </div>
                  {isAdmin && (
                    <button onClick={doImport} title="Pull existing designers from the calculator database into People"
                      style={{ marginLeft: "auto", alignSelf: "center", border: "1px solid var(--line)", background: "#fff", borderRadius: "10px", padding: "10px 14px", fontWeight: 700, fontSize: "13px", color: "var(--ink-2)", cursor: "pointer", whiteSpace: "nowrap" }}>
                      ⬇ Импорт дизайнеров из базы
                    </button>
                  )}
                </div>
                <div className="board-scroll">
                  <div className="board" onDragEnd={actions.endDrag}>
                    <ManagersCard managers={managers} expanded={expanded} actions={actions} />
                    {products.map((p) => (
                      <ProductCard key={p.id} product={p} designers={designers}
                        expanded={expanded} dragId={dragId} dropPid={dropPid} actions={actions} />
                    ))}
                    <UnassignedCard designers={designers} expanded={expanded} dragId={dragId} dropPid={dropPid} actions={actions} />
                    <AddProduct onAdd={actions.addProduct} />
                  </div>
                </div>
              </main>
              <Stats products={products} designers={designers} onReset={reset} />
            </div>
          )}

          {tab === "Designers Tables" && (
            <DesignerTables products={products} designers={designers} />
          )}

          {tab === "My Table" && (
            <DesignerTables products={products} designers={designers} soloId={effectiveMe ? effectiveMe.id : null} readOnly={true} />
          )}

          {tab === "Story Points" && <EmbeddedApp view="calculator" />}

          {tab === "Statistics" && <EmbeddedApp view="statistics" />}

          {tab === "Catalog" && <CatalogTab products={products} onProductColor={isAdmin ? (id, color) => setProducts((ps) => ps.map((p) => p.id === id ? { ...p, color } : p)) : null} />}
        </div>
      </div>
    </div>
  );
}

const DesignerTables = window.DesignerTables;
const CatalogTab = window.CatalogTab;
const StatsTab = window.StatsTab;
const AuthGate = window.AuthGate;

// Make sure People data exists AND has at least one Admin before the auth gate
// reads it — persist synchronously so the gate sees the final state on first load.
try {
  if (!localStorage.getItem(STORE_KEY)) {
    localStorage.setItem(STORE_KEY, JSON.stringify(seed()));
  } else {
    const s = load(); // load() applies the admin bootstrap in-memory
    localStorage.setItem(STORE_KEY, JSON.stringify(s));
  }
} catch (e) {}

ReactDOM.createRoot(document.getElementById("root")).render(
  <AuthGate>{(auth) => <App auth={auth} />}</AuthGate>
);
