// Admin · Production floor
// ---------------------------------------------------------------------------
// Five screens, all built on the production/staff/guest API that already lives
// on the server:
//   • Production plan   — the run-time engine + machine assignment + tasks/rush
//   • Production calendar — week grid of machine run-hours
//   • Shop floor        — live start/stop board, one row per machine
//   • Floor team        — staff accounts, roles & custom permissions
//   • Tracking links    — mint / revoke secure guest links for an order
//
// The scheduling MATH lives here on the client: the server stores whatever plan
// we compute (POST /api/production/plan) so the calendar + floor stay fast.
// Numbers below are US-standard Industrial-Engineering style standard minutes
// (SMV) at each machine's rated efficiency; everything is editable per machine,
// and the shop-wide allowances sit in PROD_STANDARDS so Joy can tune them.
// ---------------------------------------------------------------------------

// ===========================================================================
// 1 · THE ENGINE
// ===========================================================================

// Shop-wide standard allowances. "min" = standard minutes; "sec" = seconds.
const PROD_STANDARDS = {
  embroidery: {
    hoopMin: 0.75,          // hoop a garment on
    unhoopMin: 0.40,        // hoop off + quick inspect
    setupBaseMin: 8,        // load design, align, sew-out test
    setupPerColorMin: 1.5,  // thread / change each cone at setup
    trimAllowancePct: 8,    // jump-stitch & trim overhead on raw stitch time
  },
  screenprint: {
    loadMin: 0.20,          // load/unload a shirt on the press
    setupBaseMin: 15,       // mount + register the press
    setupPerColorMin: 12,   // burn, tape & register each screen
    flashSecPerColor: 2.5,  // flash/cure dwell added per colour per impression
  },
  dtf: {
    loadMin: 0.30,          // place transfer + press cycle handling
    setupBaseMin: 6,        // gang-sheet + bring press to temp
    setupPerColorMin: 0,    // full-colour transfer — no per-colour screens
  },
};

// A neutral reference machine, used when estimating before assignment.
const REF_MACHINE = {
  embroidery:  { kind: "embroidery",  heads: 1, spm: 750, effPct: 80, colorChangeSec: 12, hoursPerDay: 8 },
  screenprint: { kind: "screenprint", iph: 220, effPct: 75, hoursPerDay: 8 },
  dtf:         { kind: "dtf",         iph: 90,  effPct: 80, hoursPerDay: 8 },
};

function refMachineFor(service, machines) {
  const m = (machines || []).find(x => x.active && x.kind === service);
  return m || REF_MACHINE[service] || REF_MACHINE.embroidery;
}

// Estimate one task on one machine. Returns standard minutes split into the
// three buckets the server stores, plus a human breakdown for the UI.
function estimateTask(task, machine) {
  const svc = task.service || "embroidery";
  const qty = Math.max(1, Number(task.qty) || 1);
  const colors = Math.max(0, Number(task.colors) || 0);
  const m = machine || REF_MACHINE[svc] || REF_MACHINE.embroidery;
  const eff = Math.min(1, Math.max(0.1, (Number(m.effPct) || 80) / 100));

  let runMin = 0, laborMin = 0, setupMin = 0, lines = [];

  if (svc === "screenprint") {
    const S = PROD_STANDARDS.screenprint;
    const effIph = Math.max(1, (Number(m.iph) || 220) * eff);
    const perImpMin = 60 / effIph;
    const flashMin = (colors > 1 ? colors : 0) * (S.flashSecPerColor / 60);
    runMin = qty * (perImpMin + flashMin);
    laborMin = qty * S.loadMin;
    setupMin = S.setupBaseMin + colors * S.setupPerColorMin;
    lines = [
      `${qty} prints @ ${effIph.toFixed(0)} imp/hr (${m.effPct || 75}% eff)`,
      colors > 1 ? `${colors}-colour flash between stations` : `single colour`,
      `${colors} screen${colors === 1 ? "" : "s"} to burn & register`,
    ];
  } else if (svc === "dtf") {
    const S = PROD_STANDARDS.dtf;
    const effIph = Math.max(1, (Number(m.iph) || 90) * eff);
    runMin = qty * (60 / effIph);
    laborMin = qty * S.loadMin;
    setupMin = S.setupBaseMin;
    lines = [
      `${qty} transfers @ ${effIph.toFixed(0)}/hr (${m.effPct || 80}% eff)`,
      `full-colour — no per-colour setup`,
    ];
  } else {
    const S = PROD_STANDARDS.embroidery;
    const stitches = Math.max(0, Number(task.stitchCount) || 0);
    const heads = Math.max(1, Number(m.heads) || 1);
    const effSpm = Math.max(1, (Number(m.spm) || 750) * eff);
    const cycles = Math.ceil(qty / heads);
    const stitchMinPerCycle = (stitches / effSpm) * (1 + S.trimAllowancePct / 100);
    const ccMinPerCycle = Math.max(0, colors - 1) * ((Number(m.colorChangeSec) || 12) / 60);
    runMin = cycles * (stitchMinPerCycle + ccMinPerCycle);
    laborMin = qty * (S.hoopMin + S.unhoopMin);
    setupMin = S.setupBaseMin + colors * S.setupPerColorMin;
    lines = [
      `${stitches.toLocaleString()} st @ ${effSpm.toFixed(0)} spm (${m.effPct || 80}% eff)`,
      `${cycles} cycle${cycles === 1 ? "" : "s"} on ${heads} head${heads === 1 ? "" : "s"} for ${qty}`,
      colors > 1 ? `${colors - 1} colour change${colors - 1 === 1 ? "" : "s"}/cycle` : `single colour`,
    ];
  }

  runMin = Math.round(runMin * 100) / 100;
  laborMin = Math.round(laborMin * 100) / 100;
  setupMin = Math.round(setupMin * 100) / 100;
  return { runMin, laborMin, setupMin, totalMin: runMin + laborMin + setupMin, lines };
}

// Greedy nearest-neighbour sequencing to minimise cone (thread) changes between
// consecutive embroidery jobs on the same machine. Returns the reordered tasks
// plus how many changes the order costs vs. the naive order — so we can show
// the saving. Cost between two jobs ≈ colours in A not present in B (cones we
// have to pull and re-thread).
function sequenceThreads(tasks) {
  const emb = tasks.filter(t => (t.service || "embroidery") === "embroidery");
  const rest = tasks.filter(t => (t.service || "embroidery") !== "embroidery");
  const setOf = (t) => new Set((Array.isArray(t.threadColors) ? t.threadColors : []).map(c => String(c).toLowerCase().trim()).filter(Boolean));
  // Cost of running B right after A ≈ cones B needs that A didn't already have up.
  const changeCost = (a, b) => { const A = setOf(a), B = setOf(b); let n = 0; B.forEach(c => { if (!A.has(c)) n++; }); return n; };
  const seqCost = (arr) => { let n = 0; for (let i = 1; i < arr.length; i++) n += changeCost(arr[i - 1], arr[i]); return n; };

  if (emb.length <= 2) {
    return { ordered: [...emb, ...rest], changes: seqCost(emb), naive: seqCost(emb), saved: 0 };
  }
  const naive = seqCost(emb);
  // Start from the job with the most colours (most cones already up), then
  // always go to the remaining job that needs the fewest fresh cones.
  const pool = emb.slice();
  pool.sort((a, b) => setOf(b).size - setOf(a).size);
  const ordered = [pool.shift()];
  while (pool.length) {
    let bestI = 0, bestC = Infinity;
    for (let i = 0; i < pool.length; i++) {
      const c = changeCost(ordered[ordered.length - 1], pool[i]);
      if (c < bestC) { bestC = c; bestI = i; }
    }
    ordered.push(pool.splice(bestI, 1)[0]);
  }
  const changes = seqCost(ordered);
  // Never ship a worse order than we were given.
  if (changes > naive) return { ordered: [...emb, ...rest], changes: naive, naive, saved: 0 };
  return { ordered: [...ordered, ...rest], changes, naive, saved: Math.max(0, naive - changes) };
}

// Date helpers for day-packing the calendar.
const DAY_MS = 24 * 60 * 60 * 1000;
const ymd = (d) => { const x = new Date(d); return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}-${String(x.getDate()).padStart(2, "0")}`; };
const isWeekend = (d) => { const g = new Date(d).getDay(); return g === 0 || g === 6; };
function nextWorkday(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); while (isWeekend(x)) x.setTime(x.getTime() + DAY_MS); return x; }

// The whole plan: assign every task to a machine (load-balanced), sequence each
// machine's queue (rush first, then thread-optimised for embroidery / deadline
// for the rest), pack into working days against each machine's hours, and
// estimate the minutes. Returns assignments ready to POST plus a summary.
function generatePlan(tasks, machines) {
  const live = (machines || []).filter(m => m.active);
  const open = (tasks || []).filter(t => t.status !== "done");
  const warnings = [];
  const byKind = { embroidery: [], screenprint: [], dtf: [] };
  for (const t of open) (byKind[t.service] || byKind.embroidery).push(t);

  const assignments = [];
  const loadByMachine = {};        // machineId -> standard minutes booked
  live.forEach(m => { loadByMachine[m.id] = 0; });
  const queueByMachine = {};       // machineId -> [tasks]
  live.forEach(m => { queueByMachine[m.id] = []; });

  for (const kind of ["embroidery", "screenprint", "dtf"]) {
    const pool = byKind[kind];
    if (!pool.length) continue;
    const eligible = live.filter(m => m.kind === kind);
    if (!eligible.length) {
      warnings.push(`No active ${kind} machine — ${pool.length} ${kind} job${pool.length === 1 ? "" : "s"} couldn't be scheduled.`);
      pool.forEach(t => assignments.push(planRow(t, null, 0, null)));
      continue;
    }
    // Priority order: rush first, then earliest deadline, then longest job
    // first so big jobs get placed while machines are still free.
    const sized = pool.map(t => ({ t, est: estimateTask(t, refMachineFor(kind, machines)) }));
    sized.sort((a, b) => {
      if (!!b.t.rush !== !!a.t.rush) return b.t.rush ? 1 : -1;
      const da = a.t.deadline || "9999", db = b.t.deadline || "9999";
      if (da !== db) return da < db ? -1 : 1;
      return b.est.totalMin - a.est.totalMin;
    });
    for (const { t } of sized) {
      // Pick the machine that finishes this job soonest — its current load plus
      // how long the job takes *on that specific machine*. This keeps big jobs
      // off slow machines when a faster head is free.
      let best = eligible[0], bestFinish = Infinity, bestEst = null;
      for (const m of eligible) {
        const est = estimateTask(t, m);
        const finish = loadByMachine[m.id] + est.totalMin;
        if (finish < bestFinish) { bestFinish = finish; best = m; bestEst = est; }
      }
      loadByMachine[best.id] += bestEst.totalMin;
      queueByMachine[best.id].push(t);
    }
  }

  let totalRun = 0, totalLabor = 0, totalSetup = 0, threadSaved = 0;
  for (const m of live) {
    let q = queueByMachine[m.id];
    if (!q.length) continue;
    // Sequence: rush block first (keep their deadline order), then the rest
    // thread-optimised (embroidery) so cone changes between jobs are minimal.
    const rush = q.filter(t => t.rush).sort((a, b) => (a.deadline || "9999") < (b.deadline || "9999") ? -1 : 1);
    const norm = q.filter(t => !t.rush);
    const seqd = sequenceThreads(norm);
    threadSaved += seqd.saved;
    const finalOrder = [...rush, ...seqd.ordered];

    // Day-pack against this machine's available run hours.
    let day = nextWorkday(Date.now());
    let dayMinsLeft = m.hoursPerDay * 60;
    let seq = 0;
    for (const t of finalOrder) {
      const est = estimateTask(t, m);
      // Setup + run consume machine-day capacity; if a job won't fit and the day
      // already has work, roll to the next working day.
      const need = est.runMin + est.setupMin;
      if (need > dayMinsLeft && dayMinsLeft < m.hoursPerDay * 60) {
        day = nextWorkday(new Date(day.getTime() + DAY_MS));
        dayMinsLeft = m.hoursPerDay * 60;
      }
      dayMinsLeft -= need;
      if (dayMinsLeft < 0) {            // a single huge job spans days; advance the clock
        const over = -dayMinsLeft;
        let extraDays = Math.ceil(over / (m.hoursPerDay * 60));
        let nd = day;
        while (extraDays-- > 0) nd = nextWorkday(new Date(nd.getTime() + DAY_MS));
        day = nd; dayMinsLeft = m.hoursPerDay * 60;
      }
      assignments.push(planRow(t, m.id, seq++, ymd(day), est));
      totalRun += est.runMin; totalLabor += est.laborMin; totalSetup += est.setupMin;
    }
  }

  return {
    assignments,
    summary: {
      tasks: open.length,
      scheduled: assignments.filter(a => a.machineId).length,
      unscheduled: assignments.filter(a => !a.machineId).length,
      runHours: totalRun / 60, laborHours: totalLabor / 60, setupHours: totalSetup / 60,
      totalHours: (totalRun + totalLabor + totalSetup) / 60,
      threadChangesSaved: threadSaved,
    },
    warnings,
    loadByMachine,
  };
}

function planRow(t, machineId, seq, scheduledDay, est) {
  const e = est || estimateTask(t, null);
  return {
    id: t.id, machineId: machineId || null, seq, scheduledDay: scheduledDay || null,
    estRunMin: e.runMin, estLaborMin: e.laborMin, estSetupMin: e.setupMin,
  };
}

// Pretty-print standard minutes.
function fmtMins(min) {
  const m = Math.max(0, Math.round(Number(min) || 0));
  if (m < 60) return `${m}m`;
  const h = Math.floor(m / 60), r = m % 60;
  return r ? `${h}h ${r}m` : `${h}h`;
}
function fmtHours(h) { return `${(Math.round((Number(h) || 0) * 10) / 10).toFixed(1)} h`; }

// ===========================================================================
// 2 · SHARED LITTLE UI BITS
// ===========================================================================

const SERVICE_LABEL = { embroidery: "Embroidery", screenprint: "Screen print", dtf: "DTF" };
const OPERATION_LABEL = {
  queued: "Queued", hooping: "Hooping", stitching: "Stitching", printing: "Printing",
  curing: "Curing", trimming: "Trimming", qc: "QC", done: "Done", running: "Running", paused: "Paused",
};

function Pill({ children, tone }) {
  const tones = {
    sage: ["#eef2e3", "var(--sage-deep)"], rose: ["#f6e6ea", "var(--rose-deep)"],
    gold: ["#f6edd9", "var(--gold-deep)"], ink: ["#ece4d6", "var(--ink-2)"],
  };
  const [bg, fg] = tones[tone] || tones.ink;
  return <span style={{ background: bg, color: fg, borderRadius: 999, padding: "2px 10px", fontSize: 12, fontWeight: 600, whiteSpace: "nowrap" }}>{children}</span>;
}

function ProdEmpty({ title, hint }) {
  return (
    <div className="stitched" style={{ padding: 28, textAlign: "center", color: "var(--ink-3)" }}>
      <div style={{ fontWeight: 600, color: "var(--ink-2)", marginBottom: 4 }}>{title}</div>
      <div style={{ fontSize: 14 }}>{hint}</div>
    </div>
  );
}

// ===========================================================================
// 3 · MACHINES MANAGER  (used inside the Plan screen)
// ===========================================================================
function MachinesManager({ machines, onChange }) {
  const app = useApp();
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState(null);
  const [busy, setBusy] = useState(false);

  const blank = () => ({ name: "", kind: "embroidery", heads: 1, spm: 750, iph: 220, effPct: 80, hoursPerDay: 8, colorChangeSec: 12, active: true, notes: "" });
  const startAdd = () => { setDraft(blank()); setOpen(true); };
  const startEdit = (m) => { setDraft({ ...m }); setOpen(true); };

  const save = async () => {
    setBusy(true);
    try {
      if (draft.id) await api.updateMachine(draft.id, draft);
      else await api.addMachine(draft);
      setOpen(false); setDraft(null);
      await onChange();
      app.toast?.("Machine saved.");
    } catch (e) { app.toast?.(e.message || "Couldn't save the machine."); }
    finally { setBusy(false); }
  };
  const remove = async (m) => {
    if (!window.confirm(`Remove ${m.name}? Its jobs will go back to the unassigned pool.`)) return;
    try { await api.deleteMachine(m.id); await onChange(); app.toast?.("Machine removed."); }
    catch (e) { app.toast?.(e.message || "Couldn't remove the machine."); }
  };

  return (
    <div className="stitched stitched-sage" style={{ padding: 18, marginBottom: 18 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
        <div>
          <div className="smallcaps" style={{ color: "var(--sage-deep)" }}>Shop floor</div>
          <h3 style={{ margin: "2px 0 0" }}>Machines</h3>
        </div>
        <button className="btn btn-small" onClick={startAdd}>+ Add machine</button>
      </div>

      {!machines.length && <div style={{ color: "var(--ink-3)", fontSize: 14 }}>No machines yet — add your embroidery heads and presses so the planner has somewhere to schedule.</div>}

      {machines.map(m => (
        <div key={m.id} style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 0", borderTop: "1px solid var(--line)" }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 600 }}>{m.name} {!m.active && <Pill tone="ink">paused</Pill>}</div>
            <div style={{ fontSize: 13, color: "var(--ink-3)" }}>
              {SERVICE_LABEL[m.kind]} · {m.kind === "embroidery" ? `${m.heads} head${m.heads === 1 ? "" : "s"} · ${m.spm} spm` : `${m.iph} imp/hr`} · {m.effPct}% eff · {m.hoursPerDay}h/day
            </div>
          </div>
          <button className="btn btn-ghost btn-small" onClick={() => startEdit(m)}>Edit</button>
          <button className="btn btn-ghost btn-small" onClick={() => remove(m)}>Remove</button>
        </div>
      ))}

      {open && draft && (
        <div style={{ marginTop: 14, paddingTop: 14, borderTop: "2px dashed var(--line)" }}>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div className="field"><label>Name</label><input value={draft.name} onChange={e => setDraft({ ...draft, name: e.target.value })} placeholder="Tajima 6-head" /></div>
            <div className="field"><label>Type</label>
              <select value={draft.kind} onChange={e => setDraft({ ...draft, kind: e.target.value })}>
                <option value="embroidery">Embroidery</option>
                <option value="screenprint">Screen print</option>
                <option value="dtf">DTF</option>
              </select>
            </div>
            {draft.kind === "embroidery" ? (
              <>
                <div className="field"><label>Heads</label><input type="number" min="1" value={draft.heads} onChange={e => setDraft({ ...draft, heads: +e.target.value })} /></div>
                <div className="field"><label>Rated stitches/min</label><input type="number" min="60" value={draft.spm} onChange={e => setDraft({ ...draft, spm: +e.target.value })} /></div>
                <div className="field"><label>Colour change (sec)</label><input type="number" min="0" value={draft.colorChangeSec} onChange={e => setDraft({ ...draft, colorChangeSec: +e.target.value })} /></div>
              </>
            ) : (
              <div className="field"><label>Impressions/hour</label><input type="number" min="1" value={draft.iph} onChange={e => setDraft({ ...draft, iph: +e.target.value })} /></div>
            )}
            <div className="field"><label>Efficiency %</label><input type="number" min="10" max="100" value={draft.effPct} onChange={e => setDraft({ ...draft, effPct: +e.target.value })} /></div>
            <div className="field"><label>Run hours / day</label><input type="number" min="0.5" step="0.5" value={draft.hoursPerDay} onChange={e => setDraft({ ...draft, hoursPerDay: +e.target.value })} /></div>
          </div>
          <label style={{ display: "flex", gap: 8, alignItems: "center", margin: "6px 0 12px", cursor: "pointer" }}>
            <input type="checkbox" checked={draft.active} onChange={e => setDraft({ ...draft, active: e.target.checked })} />
            <span>Available for scheduling</span>
          </label>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" disabled={busy || !draft.name} onClick={save}>{busy ? "Saving…" : "Save machine"}</button>
            <button className="btn btn-ghost btn-small" onClick={() => { setOpen(false); setDraft(null); }}>Cancel</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ===========================================================================
// 4 · PRODUCTION PLAN  (engine + tasks + rush)
// ===========================================================================
function ProductionPlanTab() {
  const app = useApp();
  const [machines, setMachines] = useState([]);
  const [tasks, setTasks] = useState([]);
  const [loading, setLoading] = useState(true);
  const [plan, setPlan] = useState(null);     // result of generatePlan, not yet saved
  const [busy, setBusy] = useState("");
  const [addOpen, setAddOpen] = useState(false);
  const [newTask, setNewTask] = useState({ title: "", service: "embroidery", qty: 1, stitchCount: 0, colors: 1, deadline: "", rush: false });

  const load = async () => {
    setLoading(true);
    try {
      const [m, t] = await Promise.all([api.listMachines(), api.listTasks()]);
      setMachines(m); setTasks(t);
    } catch (e) { app.toast?.(e.message || "Couldn't load the floor."); }
    finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const sync = async () => {
    setBusy("sync");
    try { const r = await api.syncTasks(); setTasks(r.tasks || []); app.toast?.(r.created ? `Pulled in ${r.created} new order${r.created === 1 ? "" : "s"}.` : "Already up to date — no new orders."); }
    catch (e) { app.toast?.(e.message || "Couldn't sync orders."); }
    finally { setBusy(""); }
  };

  const toggleRush = async (t) => {
    try { const u = await api.updateTask(t.id, { rush: !t.rush }); setTasks(tasks.map(x => x.id === t.id ? u : x)); setPlan(null); }
    catch (e) { app.toast?.(e.message || "Couldn't update."); }
  };
  const editField = async (t, patch) => {
    try { const u = await api.updateTask(t.id, patch); setTasks(tasks.map(x => x.id === t.id ? u : x)); setPlan(null); }
    catch (e) { app.toast?.(e.message || "Couldn't update."); }
  };
  const removeTask = async (t) => {
    if (!window.confirm(`Remove "${t.title || t.ref}" from the queue?`)) return;
    try { await api.deleteTask(t.id); setTasks(tasks.filter(x => x.id !== t.id)); setPlan(null); }
    catch (e) { app.toast?.(e.message || "Couldn't remove."); }
  };
  const addManual = async () => {
    setBusy("add");
    try { const u = await api.addTask(newTask); setTasks([...tasks, u]); setAddOpen(false); setNewTask({ title: "", service: "embroidery", qty: 1, stitchCount: 0, colors: 1, deadline: "", rush: false }); setPlan(null); }
    catch (e) { app.toast?.(e.message || "Couldn't add the task."); }
    finally { setBusy(""); }
  };

  const compute = () => {
    const p = generatePlan(tasks.filter(t => t.status !== "done"), machines);
    setPlan(p);
    if (p.warnings.length) app.toast?.(p.warnings[0]);
  };
  const savePlan = async () => {
    if (!plan) return;
    setBusy("save");
    try {
      await api.savePlan(plan.assignments);
      await load();
      setPlan(null);
      app.toast?.("Production plan saved — it's on the calendar and floor now.");
    } catch (e) { app.toast?.(e.message || "Couldn't save the plan."); }
    finally { setBusy(""); }
  };

  const machineName = (id) => (machines.find(m => m.id === id) || {}).name || "—";
  const open = tasks.filter(t => t.status !== "done");
  const rushCount = open.filter(t => t.rush).length;

  // Live (unsaved) estimate per task for the queue table, using the assigned
  // machine if there is one, otherwise a reference machine of its kind.
  const liveEst = (t) => estimateTask(t, machines.find(m => m.id === t.machineId) || refMachineFor(t.service, machines));

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 14, flexWrap: "wrap", gap: 10 }}>
        <div>
          <h2 style={{ margin: 0 }}>Production plan</h2>
          <div style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 4 }}>
            Pull in open orders, flag the rush jobs, then build a balanced plan across every machine.
          </div>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <button className="btn btn-ghost btn-small" disabled={busy === "sync"} onClick={sync}>{busy === "sync" ? "Syncing…" : "Pull in orders"}</button>
          <button className="btn btn-ghost btn-small" onClick={() => setAddOpen(o => !o)}>+ Manual job</button>
          <button className="btn btn-small" disabled={!machines.some(m => m.active) || !open.length} onClick={compute}>Generate plan</button>
        </div>
      </div>

      <MachinesManager machines={machines} onChange={load} />

      {addOpen && (
        <div className="stitched" style={{ padding: 16, marginBottom: 16 }}>
          <h4 style={{ marginTop: 0 }}>Manual job</h4>
          <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr", gap: 10 }}>
            <div className="field"><label>Title</label><input value={newTask.title} onChange={e => setNewTask({ ...newTask, title: e.target.value })} placeholder="Staff polos" /></div>
            <div className="field"><label>Type</label>
              <select value={newTask.service} onChange={e => setNewTask({ ...newTask, service: e.target.value })}>
                <option value="embroidery">Embroidery</option><option value="screenprint">Screen print</option><option value="dtf">DTF</option>
              </select>
            </div>
            <div className="field"><label>Qty</label><input type="number" min="1" value={newTask.qty} onChange={e => setNewTask({ ...newTask, qty: +e.target.value })} /></div>
            {newTask.service === "embroidery" && <div className="field"><label>Stitches</label><input type="number" min="0" value={newTask.stitchCount} onChange={e => setNewTask({ ...newTask, stitchCount: +e.target.value })} /></div>}
            <div className="field"><label>Colours</label><input type="number" min="0" value={newTask.colors} onChange={e => setNewTask({ ...newTask, colors: +e.target.value })} /></div>
          </div>
          <div style={{ display: "flex", gap: 12, alignItems: "center", marginTop: 6 }}>
            <div className="field" style={{ margin: 0 }}><label>Deadline</label><input type="date" value={newTask.deadline} onChange={e => setNewTask({ ...newTask, deadline: e.target.value })} /></div>
            <label style={{ display: "flex", gap: 6, alignItems: "center", cursor: "pointer", marginTop: 18 }}><input type="checkbox" checked={newTask.rush} onChange={e => setNewTask({ ...newTask, rush: e.target.checked })} /><span>Rush</span></label>
            <div style={{ flex: 1 }} />
            <button className="btn btn-small" disabled={busy === "add" || !newTask.title} onClick={addManual} style={{ marginTop: 14 }}>{busy === "add" ? "Adding…" : "Add job"}</button>
          </div>
        </div>
      )}

      {plan && (
        <div className="stitched stitched-ink" style={{ padding: 18, marginBottom: 18 }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10, flexWrap: "wrap", gap: 8 }}>
            <h3 style={{ margin: 0 }}>Proposed plan</h3>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn btn-ghost btn-small" onClick={() => setPlan(null)}>Discard</button>
              <button className="btn btn-small" disabled={busy === "save"} onClick={savePlan}>{busy === "save" ? "Saving…" : "Save plan"}</button>
            </div>
          </div>
          <div style={{ display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12 }}>
            <Stat label="Total" value={fmtHours(plan.summary.totalHours)} />
            <Stat label="Machine run" value={fmtHours(plan.summary.runHours)} />
            <Stat label="Handling" value={fmtHours(plan.summary.laborHours)} />
            <Stat label="Setup" value={fmtHours(plan.summary.setupHours)} />
            <Stat label="Scheduled" value={`${plan.summary.scheduled}/${plan.summary.tasks}`} />
            <Stat label="Thread changes saved" value={plan.summary.threadChangesSaved} tone="sage" />
          </div>
          {plan.warnings.map((w, i) => <div key={i} style={{ color: "var(--rose-deep)", fontSize: 13, marginBottom: 4 }}>⚠ {w}</div>)}
          <div style={{ overflowX: "auto" }}>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
              <thead><tr style={{ textAlign: "left", color: "var(--ink-3)" }}>
                <th style={thS}>#</th><th style={thS}>Job</th><th style={thS}>Machine</th><th style={thS}>Day</th><th style={thS}>Run</th><th style={thS}>Handling</th><th style={thS}>Setup</th>
              </tr></thead>
              <tbody>
                {plan.assignments.slice().sort((a, b) => (a.machineId || "z").localeCompare(b.machineId || "z") || a.seq - b.seq).map((a, i) => {
                  const t = tasks.find(x => x.id === a.id) || {};
                  return (
                    <tr key={a.id} style={{ borderTop: "1px solid var(--line)" }}>
                      <td style={tdS}>{a.machineId ? a.seq + 1 : "—"}</td>
                      <td style={tdS}>{t.rush ? <Pill tone="rose">RUSH</Pill> : null} {t.title || t.ref} <span style={{ color: "var(--ink-3)" }}>· {SERVICE_LABEL[t.service]}</span></td>
                      <td style={tdS}>{a.machineId ? machineName(a.machineId) : <span style={{ color: "var(--rose-deep)" }}>unscheduled</span>}</td>
                      <td style={tdS}>{a.scheduledDay || "—"}</td>
                      <td style={tdS}>{fmtMins(a.estRunMin)}</td>
                      <td style={tdS}>{fmtMins(a.estLaborMin)}</td>
                      <td style={tdS}>{fmtMins(a.estSetupMin)}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
      )}

      <div className="stitched" style={{ padding: 18 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
          <h3 style={{ margin: 0 }}>Work queue {rushCount > 0 && <Pill tone="rose">{rushCount} rush</Pill>}</h3>
          <span style={{ color: "var(--ink-3)", fontSize: 13 }}>{open.length} open job{open.length === 1 ? "" : "s"}</span>
        </div>
        {loading ? <div style={{ color: "var(--ink-3)" }}>Loading…</div>
          : !open.length ? <div style={{ color: "var(--ink-3)", fontSize: 14 }}>Nothing queued. Use “Pull in orders” to bring approved custom orders onto the floor.</div>
          : (
            <div style={{ overflowX: "auto" }}>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
                <thead><tr style={{ textAlign: "left", color: "var(--ink-3)" }}>
                  <th style={thS}>Rush</th><th style={thS}>Job</th><th style={thS}>Type</th><th style={thS}>Qty</th><th style={thS}>Detail</th><th style={thS}>Deadline</th><th style={thS}>Est.</th><th style={thS}>Machine</th><th style={thS}></th>
                </tr></thead>
                <tbody>
                  {open.slice().sort((a, b) => (b.rush - a.rush) || (a.deadline || "9999").localeCompare(b.deadline || "9999")).map(t => {
                    const e = liveEst(t);
                    return (
                      <tr key={t.id} style={{ borderTop: "1px solid var(--line)", background: t.rush ? "rgba(147,78,92,0.05)" : "transparent" }}>
                        <td style={tdS}><label style={{ cursor: "pointer" }}><input type="checkbox" checked={!!t.rush} onChange={() => toggleRush(t)} /></label></td>
                        <td style={tdS}><div style={{ fontWeight: 600 }}>{t.title || t.ref || "Job"}</div>{t.ref && <div style={{ color: "var(--ink-3)" }}>{t.ref}</div>}</td>
                        <td style={tdS}>{SERVICE_LABEL[t.service]}</td>
                        <td style={tdS}>{t.qty}</td>
                        <td style={tdS}>
                          {t.service === "embroidery" && (
                            <input type="number" min="0" value={t.stitchCount || 0} title="Stitch count"
                              onChange={e => editField(t, { stitchCount: +e.target.value })}
                              style={{ width: 76, fontSize: 12, padding: "2px 4px" }} />
                          )}
                          <input type="number" min="0" value={t.colors || 0} title="Colours"
                            onChange={e => editField(t, { colors: +e.target.value })}
                            style={{ width: 46, fontSize: 12, padding: "2px 4px", marginLeft: t.service === "embroidery" ? 4 : 0 }} />
                          <span style={{ color: "var(--ink-3)", fontSize: 11, marginLeft: 4 }}>{t.service === "embroidery" ? "st · c" : "colours"}</span>
                        </td>
                        <td style={tdS}><input type="date" value={t.deadline || ""} onChange={e => editField(t, { deadline: e.target.value })} style={{ fontSize: 12, padding: "2px 4px" }} /></td>
                        <td style={tdS} title={e.lines.join("\n")}>{fmtMins(e.totalMin)}</td>
                        <td style={tdS}>{t.machineId ? machineName(t.machineId) : <span style={{ color: "var(--ink-3)" }}>—</span>}</td>
                        <td style={tdS}><button className="btn btn-ghost btn-small" onClick={() => removeTask(t)}>✕</button></td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
      </div>
    </div>
  );
}

function Stat({ label, value, tone }) {
  return (
    <div>
      <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11 }}>{label}</div>
      <div style={{ fontSize: 20, fontWeight: 600, color: tone === "sage" ? "var(--sage-deep)" : "var(--ink)" }}>{value}</div>
    </div>
  );
}
const thS = { padding: "6px 8px", fontWeight: 600, fontSize: 12 };
const tdS = { padding: "6px 8px", verticalAlign: "middle" };

// ===========================================================================
// 5 · PRODUCTION CALENDAR  (week grid of machine run-hours)
// ===========================================================================
function ProductionCalendarTab() {
  const app = useApp();
  const [machines, setMachines] = useState([]);
  const [tasks, setTasks] = useState([]);
  const [loading, setLoading] = useState(true);
  const [weekStart, setWeekStart] = useState(() => mondayOf(new Date()));

  useEffect(() => {
    (async () => {
      setLoading(true);
      try { const [m, t] = await Promise.all([api.listMachines(), api.listTasks()]); setMachines(m.filter(x => x.active)); setTasks(t); }
      catch (e) { app.toast?.(e.message || "Couldn't load the calendar."); }
      finally { setLoading(false); }
    })();
  }, []);

  const days = [];
  for (let i = 0; i < 7; i++) { const d = new Date(weekStart); d.setDate(d.getDate() + i); days.push(d); }
  const dayKeys = days.map(ymd);

  // hours[machineId][ymd] = run+setup hours scheduled that day
  const hours = {};
  let weekTotal = 0;
  machines.forEach(m => { hours[m.id] = {}; dayKeys.forEach(k => hours[m.id][k] = 0); });
  for (const t of tasks) {
    if (t.status === "done" || !t.machineId || !t.scheduledDay) continue;
    if (!hours[t.machineId] || hours[t.machineId][t.scheduledDay] === undefined) continue;
    const hrs = (Number(t.estRunMin || 0) + Number(t.estSetupMin || 0)) / 60;
    hours[t.machineId][t.scheduledDay] += hrs;
    weekTotal += hrs;
  }
  const dayTotals = dayKeys.map(k => machines.reduce((s, m) => s + (hours[m.id][k] || 0), 0));
  const today = ymd(new Date());

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 14, flexWrap: "wrap", gap: 10 }}>
        <div>
          <h2 style={{ margin: 0 }}>Production calendar</h2>
          <div style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 4 }}>Scheduled machine hours for the week — run time plus setup, per machine per day.</div>
        </div>
        <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
          <button className="btn btn-ghost btn-small" onClick={() => setWeekStart(addDaysD(weekStart, -7))}>← Prev</button>
          <button className="btn btn-ghost btn-small" onClick={() => setWeekStart(mondayOf(new Date()))}>This week</button>
          <button className="btn btn-ghost btn-small" onClick={() => setWeekStart(addDaysD(weekStart, 7))}>Next →</button>
        </div>
      </div>

      <div className="stitched" style={{ padding: 18 }}>
        <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12 }}>
          <div className="smallcaps" style={{ color: "var(--sage-deep)" }}>Week of {weekStart.toLocaleDateString(undefined, { month: "long", day: "numeric" })}</div>
          <div style={{ fontWeight: 600 }}>{fmtHours(weekTotal)} total</div>
        </div>

        {loading ? <div style={{ color: "var(--ink-3)" }}>Loading…</div>
          : !machines.length ? <ProdEmpty title="No active machines" hint="Add machines on the Production plan screen first." />
          : (
            <div style={{ overflowX: "auto" }}>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13, minWidth: 720 }}>
                <thead>
                  <tr>
                    <th style={{ ...thS, position: "sticky", left: 0 }}>Machine</th>
                    {days.map((d, i) => (
                      <th key={i} style={{ ...thS, textAlign: "center", color: dayKeys[i] === today ? "var(--rose-deep)" : "var(--ink-3)" }}>
                        {d.toLocaleDateString(undefined, { weekday: "short" })}<br /><span style={{ fontWeight: 400 }}>{d.getDate()}</span>
                      </th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {machines.map(m => (
                    <tr key={m.id} style={{ borderTop: "1px solid var(--line)" }}>
                      <td style={{ ...tdS, fontWeight: 600 }}>{m.name}<div style={{ fontSize: 11, color: "var(--ink-3)", fontWeight: 400 }}>{m.hoursPerDay}h/day</div></td>
                      {dayKeys.map((k, i) => {
                        const h = hours[m.id][k] || 0;
                        const cap = m.hoursPerDay || 8;
                        const pct = Math.min(100, Math.round((h / cap) * 100));
                        const over = h > cap + 0.01;
                        const wknd = isWeekend(days[i]);
                        return (
                          <td key={k} style={{ ...tdS, textAlign: "center", background: wknd ? "rgba(58,42,38,0.03)" : "transparent" }}>
                            {h > 0 ? (
                              <div>
                                <div style={{ fontWeight: 600, color: over ? "var(--rose-deep)" : "var(--ink)" }}>{fmtHours(h)}</div>
                                <div style={{ height: 5, borderRadius: 3, background: "var(--line)", marginTop: 3, overflow: "hidden" }}>
                                  <div style={{ width: `${pct}%`, height: "100%", background: over ? "var(--rose)" : "var(--sage)" }} />
                                </div>
                              </div>
                            ) : <span style={{ color: "var(--ink-3)" }}>·</span>}
                          </td>
                        );
                      })}
                    </tr>
                  ))}
                  <tr style={{ borderTop: "2px solid var(--line)" }}>
                    <td style={{ ...tdS, fontWeight: 600 }}>All machines</td>
                    {dayTotals.map((h, i) => (
                      <td key={i} style={{ ...tdS, textAlign: "center", fontWeight: 600, color: h > 0 ? "var(--sage-deep)" : "var(--ink-3)" }}>{h > 0 ? fmtHours(h) : "·"}</td>
                    ))}
                  </tr>
                </tbody>
              </table>
            </div>
          )}
        <div style={{ color: "var(--ink-3)", fontSize: 12, marginTop: 10 }}>
          Bars show each day against that machine's available hours. Hours come from the saved plan — regenerate it on the Production plan screen after queue changes.
        </div>
      </div>
    </div>
  );
}
function mondayOf(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); const g = (x.getDay() + 6) % 7; x.setDate(x.getDate() - g); return x; }
function addDaysD(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }

// ===========================================================================
// 6 · SHOP FLOOR  (real-time start/stop board)
// ===========================================================================
const FLOOR_OPERATIONS = ["hooping", "stitching", "printing", "curing", "trimming", "qc"];

function ShopFloorTab() {
  const app = useApp();
  const [board, setBoard] = useState(null);
  const [loading, setLoading] = useState(true);
  const [tick, setTick] = useState(0);

  const load = async () => {
    try { const r = await api.getFloor(); setBoard(r.board || []); }
    catch (e) { app.toast?.(e.message || "Couldn't load the floor."); }
    finally { setLoading(false); }
  };
  useEffect(() => {
    load();
    const poll = setInterval(load, 10000);          // live refresh every 10s
    const clock = setInterval(() => setTick(t => t + 1), 1000); // running timers
    return () => { clearInterval(poll); clearInterval(clock); };
  }, []);

  const punch = async (taskId, body) => {
    try { await api.punchTask(taskId, body); await load(); }
    catch (e) { app.toast?.(e.message || "Couldn't record that."); }
  };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 14, flexWrap: "wrap", gap: 10 }}>
        <div>
          <h2 style={{ margin: 0 }}>Shop floor</h2>
          <div style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 4 }}>Live board — what every machine is on right now. Start and stop capture the real minutes.</div>
        </div>
        <Pill tone="sage">● live</Pill>
      </div>

      {loading ? <div style={{ color: "var(--ink-3)" }}>Loading…</div>
        : !board || !board.length ? <ProdEmpty title="No active machines on the floor" hint="Add machines and save a plan to populate the board." />
        : (
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))", gap: 16 }}>
            {board.map(b => <FloorCard key={b.machine.id} b={b} onPunch={punch} now={Date.now()} />)}
          </div>
        )}
    </div>
  );
}

function FloorCard({ b, onPunch }) {
  const running = b.running;
  const [op, setOp] = useState((running && running.openPunch && running.openPunch.operation) || (b.machine.kind === "embroidery" ? "stitching" : "printing"));
  const elapsed = running && running.openPunch ? Math.max(0, Math.floor((Date.now() - running.openPunch.startedAt) / 1000)) : 0;
  const mm = String(Math.floor(elapsed / 60)).padStart(2, "0"), ss = String(elapsed % 60).padStart(2, "0");

  return (
    <div className={"stitched" + (running ? " stitched-sage" : "")} style={{ padding: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
        <div style={{ fontWeight: 700, fontSize: 16 }}>{b.machine.name}</div>
        <Pill tone={running ? "sage" : "ink"}>{running ? "running" : "idle"}</Pill>
      </div>
      <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: 10 }}>
        {SERVICE_LABEL[b.machine.kind]} · {b.queueCount} in queue · {fmtMins(b.actualMinToday)} logged today
      </div>

      {running ? (
        <div style={{ borderTop: "1px solid var(--line)", paddingTop: 10 }}>
          <div style={{ fontWeight: 600 }}>{running.title || running.ref} {running.rush && <Pill tone="rose">RUSH</Pill>}</div>
          <div style={{ fontSize: 13, color: "var(--ink-3)", margin: "2px 0 8px" }}>
            {OPERATION_LABEL[running.openPunch.operation] || running.openPunch.operation} · {running.openPunch.staffName}
          </div>
          <div className="mono" style={{ fontSize: 26, fontWeight: 700, color: "var(--sage-deep)", marginBottom: 10 }}>{mm}:{ss}</div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-ghost btn-small" onClick={() => onPunch(running.id, { action: "stop" })}>Pause</button>
            <button className="btn btn-small" onClick={() => onPunch(running.id, { action: "stop", done: true })}>Finish job</button>
          </div>
        </div>
      ) : b.upNext.length ? (
        <div style={{ borderTop: "1px solid var(--line)", paddingTop: 10 }}>
          <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginBottom: 6 }}>Up next</div>
          {b.upNext.map((t, i) => (
            <div key={t.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 0", borderTop: i ? "1px solid var(--line)" : "none" }}>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: i === 0 ? 600 : 400 }}>{t.title || t.ref} {t.rush && <Pill tone="rose">RUSH</Pill>}</div>
                <div style={{ fontSize: 12, color: "var(--ink-3)" }}>{t.qty} pc · est {fmtMins(t.estRunMin)}</div>
              </div>
              {i === 0 && (
                <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
                  <select value={op} onChange={e => setOp(e.target.value)} style={{ fontSize: 12, padding: "3px 4px" }}>
                    {FLOOR_OPERATIONS.map(o => <option key={o} value={o}>{OPERATION_LABEL[o]}</option>)}
                  </select>
                  <button className="btn btn-small" onClick={() => onPunch(t.id, { action: "start", operation: op })}>Start</button>
                </div>
              )}
            </div>
          ))}
        </div>
      ) : <div style={{ borderTop: "1px solid var(--line)", paddingTop: 10, color: "var(--ink-3)", fontSize: 14 }}>Queue empty — nothing assigned here.</div>}
    </div>
  );
}

// ===========================================================================
// 7 · FLOOR TEAM  (staff accounts + roles + custom permissions)
// ===========================================================================
const CAP_LABELS = {
  canPlan: "Build & save plans", canEditTasks: "Edit jobs & rush", canManageMachines: "Manage machines",
  canManageStaff: "Manage team", canViewFloor: "See the floor board", canPunch: "Start / stop jobs", canViewReports: "See reports & calendar",
};

function FloorTeamTab() {
  const app = useApp();
  const [staff, setStaff] = useState([]);
  const [caps, setCaps] = useState({ caps: [], rolePresets: {} });
  const [loading, setLoading] = useState(true);
  const [draft, setDraft] = useState(null);
  const [busy, setBusy] = useState(false);

  const load = async () => {
    setLoading(true);
    try { const [s, c] = await Promise.all([api.listStaff(), api.staffCaps()]); setStaff(s); setCaps(c); }
    catch (e) { app.toast?.(e.message || "Couldn't load the team."); }
    finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const blank = () => ({ name: "", username: "", pin: "", role: "operator", permissions: {}, active: true, payType: "hourly", hourlyRate: "", annualSalary: "" });
  const startAdd = () => setDraft(blank());
  const startEdit = (s) => setDraft({ id: s.id, name: s.name, username: s.username, role: s.role, permissions: s.permissions || {}, active: s.active, pin: "", payType: s.payType || "hourly", hourlyRate: s.payType === "salary" ? "" : (s.hourlyRate || ""), annualSalary: s.payType === "salary" ? (s.annualSalary || "") : "" });

  const presetFor = (role) => caps.rolePresets[role] || {};
  // Effective view in the editor: explicit override wins, else role preset.
  const effective = (d, cap) => (d.permissions[cap] !== undefined ? !!d.permissions[cap] : !!presetFor(d.role)[cap]);
  const toggleCap = (cap) => {
    const cur = effective(draft, cap);
    setDraft({ ...draft, permissions: { ...draft.permissions, [cap]: cur ? 0 : 1 } });
  };

  const save = async () => {
    setBusy(true);
    try {
      const body = { name: draft.name, role: draft.role, permissions: draft.permissions, active: draft.active,
        payType: draft.payType || "hourly", hourlyRate: draft.hourlyRate, annualSalary: draft.annualSalary };
      if (draft.pin) body.pin = draft.pin;
      if (draft.id) { await api.updateStaff(draft.id, body); }
      else { body.username = draft.username; await api.addStaff(body); }
      setDraft(null); await load(); app.toast?.("Team member saved.");
    } catch (e) { app.toast?.(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };
  const remove = async (s) => {
    if (!window.confirm(`Remove ${s.name} from the floor team?`)) return;
    try { await api.deleteStaff(s.id); await load(); app.toast?.("Removed."); }
    catch (e) { app.toast?.(e.message || "Couldn't remove."); }
  };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 14, flexWrap: "wrap", gap: 10 }}>
        <div>
          <h2 style={{ margin: 0 }}>Floor team</h2>
          <div style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 4 }}>Each person signs in with their own PIN so start/stop times are attributable. Roles set sensible defaults; tick boxes to fine-tune.</div>
          <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 4 }}>Operators clock in at <span className="mono" style={{ color: "var(--sage-deep)" }}>{(typeof location !== "undefined" ? location.origin : "")}/#/floor</span></div>
        </div>
        <button className="btn btn-small" onClick={startAdd}>+ Add person</button>
      </div>

      {loading ? <div style={{ color: "var(--ink-3)" }}>Loading…</div>
        : !staff.length && !draft ? <ProdEmpty title="No floor team yet" hint="Add your operators so the floor board shows who's running each machine." />
        : (
          <div className="stitched" style={{ padding: 0, overflow: "hidden" }}>
            {staff.map(s => (
              <div key={s.id} style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", borderBottom: "1px solid var(--line)" }}>
                <div style={{ flex: 1 }}>
                  <div style={{ fontWeight: 600 }}>{s.name} {!s.active && <Pill tone="ink">inactive</Pill>}</div>
                  <div style={{ fontSize: 13, color: "var(--ink-3)" }}>@{s.username} · {s.role} · last in {s.lastLoginAt ? new Date(s.lastLoginAt).toLocaleDateString() : "never"}</div>
                  {s.effectiveHourly > 0 && (
                    <div style={{ fontSize: 13, color: "var(--ink-2)", marginTop: 2, display: "inline-flex", alignItems: "center", gap: 6 }}>
                      <span>{s.payType === "salary" ? `Salaried · $${Number(s.annualSalary).toLocaleString()}/yr` : "Hourly"} — <b>${Number(s.effectiveHourly).toFixed(2)}/hr</b></span>
                      <button type="button" className="mono" title="Copy hourly cost for the job-cost calculator"
                        onClick={() => { const v = Number(s.effectiveHourly).toFixed(2); try { navigator.clipboard.writeText(v); app.toast?.(`Copied $${v}/hr`); } catch (e) { app.toast?.(`$${v}/hr`); } }}
                        style={{ border: "1px solid var(--line)", background: "var(--card)", borderRadius: 6, cursor: "pointer", fontSize: 11, color: "var(--sage-deep)", padding: "1px 6px" }}>copy</button>
                    </div>
                  )}
                  <div style={{ marginTop: 4, display: "flex", gap: 4, flexWrap: "wrap" }}>
                    {(caps.caps || []).filter(c => s.effective && s.effective[c]).map(c => <Pill key={c} tone="sage">{CAP_LABELS[c] || c}</Pill>)}
                  </div>
                </div>
                <button className="btn btn-ghost btn-small" onClick={() => startEdit(s)}>Edit</button>
                <button className="btn btn-ghost btn-small" onClick={() => remove(s)}>Remove</button>
              </div>
            ))}
          </div>
        )}

      {draft && (
        <div className="stitched stitched-ink" style={{ padding: 18, marginTop: 16 }}>
          <h3 style={{ marginTop: 0 }}>{draft.id ? "Edit team member" : "New team member"}</h3>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
            <div className="field"><label>Name</label><input value={draft.name} onChange={e => setDraft({ ...draft, name: e.target.value })} /></div>
            <div className="field"><label>Username</label><input value={draft.username} disabled={!!draft.id} onChange={e => setDraft({ ...draft, username: e.target.value })} placeholder="firstname" /></div>
            <div className="field"><label>{draft.id ? "Reset PIN (optional)" : "PIN (min 4)"}</label><input type="password" value={draft.pin} onChange={e => setDraft({ ...draft, pin: e.target.value })} placeholder="••••" /></div>
          </div>
          <div className="field" style={{ maxWidth: 240 }}><label>Role</label>
            <select value={draft.role} onChange={e => setDraft({ ...draft, role: e.target.value, permissions: {} })}>
              {Object.keys(caps.rolePresets).map(r => <option key={r} value={r}>{r[0].toUpperCase() + r.slice(1)}</option>)}
            </select>
          </div>

          {/* [HB] Wage — hourly or salaried. The effective hourly cost is shown
              with a copy button so Joy can paste it into the job-cost calculator. */}
          {(() => {
            const nz = (v) => { const n = parseFloat(v); return isFinite(n) ? n : 0; };
            const effHourly = draft.payType === "salary"
              ? (nz(draft.annualSalary) > 0 ? Math.round((nz(draft.annualSalary) / 2080) * 100) / 100 : 0)
              : Math.round(nz(draft.hourlyRate) * 100) / 100;
            const copyHourly = () => {
              const val = effHourly.toFixed(2);
              try { navigator.clipboard.writeText(val); app.toast?.(`Copied $${val}/hr`); }
              catch (e) { app.toast?.(`Hourly cost: $${val}`); }
            };
            return (
              <div className="stitched" style={{ background: "var(--paper-2)", padding: 12, margin: "10px 0" }}>
                <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 8 }}>Pay</div>
                <div className="field-row">
                  <div className="field" style={{ maxWidth: 180 }}><label>Type</label>
                    <select value={draft.payType || "hourly"} onChange={e => setDraft({ ...draft, payType: e.target.value })}>
                      <option value="hourly">Hourly</option>
                      <option value="salary">Salaried</option>
                    </select>
                  </div>
                  {draft.payType === "salary" ? (
                    <div className="field" style={{ maxWidth: 200 }}><label>Annual salary ($)</label>
                      <input inputMode="decimal" value={draft.annualSalary} onChange={e => setDraft({ ...draft, annualSalary: e.target.value })} placeholder="e.g. 45000" />
                    </div>
                  ) : (
                    <div className="field" style={{ maxWidth: 200 }}><label>Hourly rate ($/hr)</label>
                      <input inputMode="decimal" value={draft.hourlyRate} onChange={e => setDraft({ ...draft, hourlyRate: e.target.value })} placeholder="e.g. 18.50" />
                    </div>
                  )}
                </div>
                <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginTop: 4 }}>
                  <span style={{ fontSize: 14, color: "var(--ink-2)" }}>Labour cost: <b>${effHourly.toFixed(2)}/hr</b>{draft.payType === "salary" && <span style={{ color: "var(--ink-3)" }}> (salary ÷ 2,080 h)</span>}</span>
                  <button type="button" className="btn btn-ghost btn-small" onClick={copyHourly} disabled={!effHourly}>Copy for calculator</button>
                </div>
                <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 4 }}>This hourly cost is what the timecard uses, and what you paste into the job-cost calculator. Only you (admin) can see it.</div>
              </div>
            );
          })()}

          <div className="smallcaps" style={{ color: "var(--ink-3)", margin: "10px 0 6px" }}>Permissions</div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 6 }}>
            {(caps.caps || []).map(cap => (
              <label key={cap} style={{ display: "flex", gap: 8, alignItems: "center", cursor: "pointer", fontSize: 14 }}>
                <input type="checkbox" checked={effective(draft, cap)} onChange={() => toggleCap(cap)} />
                <span>{CAP_LABELS[cap] || cap}</span>
              </label>
            ))}
          </div>
          <label style={{ display: "flex", gap: 8, alignItems: "center", margin: "12px 0", cursor: "pointer" }}>
            <input type="checkbox" checked={draft.active} onChange={e => setDraft({ ...draft, active: e.target.checked })} /><span>Active (can sign in)</span>
          </label>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" disabled={busy || !draft.name || (!draft.id && (!draft.username || draft.pin.length < 4))} onClick={save}>{busy ? "Saving…" : "Save"}</button>
            <button className="btn btn-ghost btn-small" onClick={() => setDraft(null)}>Cancel</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ===========================================================================
// 8 · TRACKING LINKS  (mint / revoke secure guest links)
// ===========================================================================
function TrackingLinksTab() {
  const app = useApp();
  const [q, setQ] = useState("");
  const [selected, setSelected] = useState(null);  // an upload (order)
  const [links, setLinks] = useState([]);
  const [issued, setIssued] = useState(null);       // last minted { url, expiresAt }
  const [busy, setBusy] = useState(false);

  const orders = app.uploads || [];
  const matches = q.trim() ? orders.filter(u => {
    const s = q.toLowerCase();
    return (u.ref || "").toLowerCase().includes(s) || (u.email || "").toLowerCase().includes(s) || (u.name || "").toLowerCase().includes(s);
  }).slice(0, 8) : [];

  const pick = async (u) => {
    setSelected(u); setQ(""); setIssued(null);
    try { setLinks(await api.guestLinksForOrder(u.id)); } catch (e) { setLinks([]); }
  };
  const issue = async () => {
    if (!selected) return;
    setBusy(true);
    try {
      const r = await api.guestIssue({ uploadId: selected.id });
      setIssued({ url: r.url, expiresAt: r.expiresAt });
      setLinks(await api.guestLinksForOrder(selected.id));
      app.toast?.("Tracking link created.");
    } catch (e) { app.toast?.(e.message || "Couldn't create the link."); }
    finally { setBusy(false); }
  };
  const revoke = async (id) => {
    if (!window.confirm("Revoke this link? It stops working immediately.")) return;
    try { await api.guestRevoke(id); setLinks(await api.guestLinksForOrder(selected.id)); app.toast?.("Link revoked."); }
    catch (e) { app.toast?.(e.message || "Couldn't revoke."); }
  };
  const copy = async (url) => { try { await navigator.clipboard.writeText(url); app.toast?.("Link copied."); } catch { app.toast?.("Copy failed — select and copy manually."); } };

  const expiryNote = (l) => {
    const days = Math.max(0, Math.ceil((l.expiresAt - Date.now()) / DAY_MS));
    if (l.revoked) return "revoked";
    if (l.expiresAt < Date.now()) return "expired";
    return l.deliveredAt ? `${days} day${days === 1 ? "" : "s"} left (post-delivery)` : `active while in progress`;
  };

  return (
    <div>
      <h2 style={{ margin: 0 }}>Tracking links</h2>
      <div style={{ color: "var(--ink-3)", fontSize: 14, margin: "4px 0 16px" }}>
        Give a customer without an account a secure link to follow their order and keep their receipts. Links stay live the whole time the order's in progress, then for 30 days after it's finished — no need to message you.
      </div>

      <div className="stitched stitched-sage" style={{ padding: 18, marginBottom: 16 }}>
        <div className="field" style={{ marginBottom: selected ? 12 : 0 }}>
          <label>Find an order</label>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search by ref, name or email" />
        </div>
        {matches.length > 0 && (
          <div style={{ border: "1px solid var(--line)", borderRadius: 8, overflow: "hidden", marginBottom: 12 }}>
            {matches.map(u => (
              <div key={u.id} onClick={() => pick(u)} style={{ padding: "8px 12px", cursor: "pointer", borderTop: "1px solid var(--line)" }}>
                <strong>{u.ref}</strong> · {u.name} <span style={{ color: "var(--ink-3)" }}>· {u.email}</span>
              </div>
            ))}
          </div>
        )}
        {selected && (
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
            <div>
              <div style={{ fontWeight: 600 }}>{selected.ref} · {selected.name}</div>
              <div style={{ fontSize: 13, color: "var(--ink-3)" }}>{selected.email} · status: {selected.status}</div>
            </div>
            <button className="btn btn-small" disabled={busy} onClick={issue}>{busy ? "Creating…" : "Create link"}</button>
          </div>
        )}
      </div>

      {issued && (
        <div className="stitched stitched-ink" style={{ padding: 16, marginBottom: 16 }}>
          <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 6 }}>New link — copy it now</div>
          <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
            <input readOnly value={issued.url} onClick={e => e.target.select()} style={{ flex: 1, minWidth: 240, fontSize: 13 }} className="mono" />
            <button className="btn btn-small" onClick={() => copy(issued.url)}>Copy</button>
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 6 }}>The full link is shown only once — paste it to the customer now.</div>
        </div>
      )}

      {selected && (
        <div className="stitched" style={{ padding: 18 }}>
          <h3 style={{ marginTop: 0 }}>Links for {selected.ref}</h3>
          {!links.length ? <div style={{ color: "var(--ink-3)", fontSize: 14 }}>No links yet for this order.</div>
            : links.map(l => (
              <div key={l.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderTop: "1px solid var(--line)" }}>
                <div>
                  <div style={{ fontSize: 13 }}>Created {new Date(l.createdAt).toLocaleDateString()} · {l.lastUsedAt ? `last opened ${new Date(l.lastUsedAt).toLocaleDateString()}` : "not opened yet"}</div>
                  <div style={{ fontSize: 12, color: l.revoked || l.expiresAt < Date.now() ? "var(--rose-deep)" : "var(--sage-deep)" }}>{expiryNote(l)}</div>
                </div>
                {!l.revoked && l.expiresAt > Date.now() && <button className="btn btn-ghost btn-small" onClick={() => revoke(l.id)}>Revoke</button>}
              </div>
            ))}
        </div>
      )}
    </div>
  );
}

// ===========================================================================
// 9 · MAINTENANCE  (schedule reference + per-machine status + completion log)
// ===========================================================================

// The schedule itself is standard upkeep guidance, shown as a reference. The
// server only records WHEN each cadence was last done, to flag what's due.
// [HB] Per-machine-type checklists: an embroidery head, a DTF printer and a
// screen-print press need very different upkeep. `maintFor(kind)` picks the
// right one; MAINTENANCE_SCHEDULE stays as an alias for shared cadence labels.
const MAINTENANCE_EMBROIDERY = {
  daily: {
    label: "Daily", when: "Before your first project of the day",
    tasks: [
      ["Inspect & brush", "Remove the needle plate and bobbin case. Use a small brush or vacuum to clear lint and dust from the feed dogs and rotary hook."],
      ["Oil the hook", "Apply exactly one drop of sewing-machine oil to the hook raceway."],
      ["Check needles", "Run a finger or a business card over the needle tip to feel for burrs. Replace any needle that's bent or dull."],
    ],
  },
  weekly: {
    label: "Weekly", when: "Once a week",
    tasks: [
      ["Deep clean", "Remove the needle plate and thoroughly clean the tension disks and bobbin cavity."],
      ["Lubricate moving parts", "Check your machine's manual for guide rails or needle bars that need weekly grease or oil."],
      ["Tighten screws", "Stitching vibration loosens screws over time. Gently check and tighten the screws on the thread guides and presser foot."],
    ],
  },
  monthly: {
    label: "Monthly", when: "Once a month",
    tasks: [
      ["Deep maintenance", "Remove the needle plate and completely dust the area underneath it. Clean the tension disks with a soft brush or floss to lift invisible lint buildup."],
      ["Software backup", "Back up your custom embroidery designs and machine settings to a flash drive or the cloud."],
    ],
  },
  yearly: {
    label: "Yearly", when: "Every 12 months — or every 6 months for heavy commercial use",
    tasks: [
      ["Professional servicing", "Have the machine professionally tuned, cleaned and re-timed by a certified local technician."],
    ],
  },
};
// [HB] DTF printers use thick, pigment-heavy inks that clog easily. Daily
// cleaning is what prevents permanent printhead damage — skipping it is costly.
const MAINTENANCE_DTF = {
  daily: {
    label: "Daily", when: "Before every print session — skipping this is what kills printheads",
    tasks: [
      ["Check the environment", "Keep the room between 70–80°F with 40–60% humidity. Use a digital hygrometer to monitor it."],
      ["Shake the white ink", "Remove and gently shake the white ink cartridges or bags for about 30 seconds, so the heavy pigment doesn't settle."],
      ["Test the nozzles", "Run a nozzle check pattern from your software. Clean the printhead if any lines are missing or broken."],
      ["Clean the wiper & caps", "Wipe the rubber wiper blade and the capping station edges with a foam swab and DTF cleaning solution. A tight seal is what stops the head drying out."],
    ],
  },
  weekly: {
    label: "Weekly", when: "Once a week",
    tasks: [
      ["Check the waste ink", "Empty the waste ink bottle if it's more than half full."],
      ["Deep clean the printhead edges", "With a flashlight and a swab of cleaning solution, gently clean around the printhead edges. Never scrub the delicate nozzle holes underneath directly."],
      ["Inspect mechanical parts", "Check the film feed rollers and pressure rollers for adhesive dust, and wipe them with a clean cloth."],
    ],
  },
  monthly: {
    label: "Monthly", when: "Once a month",
    tasks: [
      ["Check dampers & tubes", "Inspect the small ink dampers above the printhead. Replace any that are clogged or filled with dark, hardened ink."],
      ["Clean the encoder strip", "Find the thin clear plastic strip running behind the print carriage and wipe it very gently with a dry, lint-free cloth."],
      ["Lubricate the gears", "Apply a tiny drop of industrial printer grease to the carriage rail and gears to prevent wear and noise."],
    ],
  },
  yearly: {
    label: "Yearly", when: "Every 12 months",
    tasks: [
      ["Professional servicing", "Have the printer serviced by a technician — printhead alignment, ink line flush, and a full mechanical check."],
    ],
  },
};

// [HB] Screen-print press upkeep.
const MAINTENANCE_SCREENPRINT = {
  daily: {
    label: "Daily", when: "At the end of each print session",
    tasks: [
      ["Clean screens & squeegees", "Wash ink from screens and squeegee blades before it can dry and block the mesh."],
      ["Wipe the platens", "Clean adhesive and ink residue from the platens so shirts sit flat and true."],
    ],
  },
  weekly: {
    label: "Weekly", when: "Once a week",
    tasks: [
      ["Check registration", "Verify the print heads still register cleanly; adjust the micro-registration if colours are drifting."],
      ["Inspect squeegee blades", "Look for nicks and rounded edges — a worn blade lays ink unevenly."],
    ],
  },
  monthly: {
    label: "Monthly", when: "Once a month",
    tasks: [
      ["Degrease & reclaim screens", "Reclaim and degrease screens so emulsion holds properly on the next burn."],
      ["Lubricate the press", "Grease the pivot points and rotating bearings on the press arms."],
    ],
  },
  yearly: {
    label: "Yearly", when: "Every 12 months",
    tasks: [
      ["Professional servicing", "Have the press levelled and serviced, and the flash/dryer elements checked."],
    ],
  },
};

const MAINTENANCE_BY_KIND = {
  embroidery: MAINTENANCE_EMBROIDERY,
  dtf: MAINTENANCE_DTF,
  screenprint: MAINTENANCE_SCREENPRINT,
};
// The checklist for a given machine. Unknown kinds fall back to embroidery.
function maintFor(kind) {
  return MAINTENANCE_BY_KIND[String(kind || "").toLowerCase()] || MAINTENANCE_EMBROIDERY;
}
// Cadence labels (Daily/Weekly/…) are shared across every machine type, so
// anything that only needs the label can keep using this alias.
const MAINTENANCE_SCHEDULE = MAINTENANCE_EMBROIDERY;
const MAINT_ORDER = ["daily", "weekly", "monthly", "yearly"];
const MAINT_TONE = { ok: "sage", due: "gold", overdue: "rose" };
const MAINT_STATUS_LABEL = { ok: "Up to date", due: "Due soon", overdue: "Overdue" };

function todayYmd() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; }

function MaintenanceTab() {
  const app = useApp();
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);
  const [showSchedule, setShowSchedule] = useState(false);
  const [schedKind, setSchedKind] = useState("embroidery");   // which checklist the reference shows
  const [log, setLog] = useState(null);        // { machine, cadence }
  const [history, setHistory] = useState(null); // machineId currently expanded
  const [histItems, setHistItems] = useState([]);

  const load = async () => {
    setLoading(true);
    try { setRows(await api.listMaintenance()); }
    catch (e) { app.toast?.(e.message || "Couldn't load maintenance."); }
    finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const openHistory = async (m) => {
    if (history === m.id) { setHistory(null); return; }
    setHistory(m.id); setHistItems([]);
    try { setHistItems(await api.maintenanceHistory(m.id)); } catch (e) { setHistItems([]); }
  };

  // [HB] Correct a machine's type in place. The checklist a machine shows is
  // driven by its `kind`, so a DTF printer left on the default "embroidery" type
  // shows needle-and-hook tasks. Flipping it here switches the checklist (and its
  // production standards) to match.
  const setKind = async (m, kind) => {
    if (!kind || kind === m.kind) return;
    setRows(rs => rs.map(r => r.id === m.id ? { ...r, kind } : r)); // optimistic
    try {
      await api.updateMachine(m.id, { kind });
      app.toast?.(`${m.name} set to ${SERVICE_LABEL[kind] || kind} — its checklist now matches.`);
    } catch (e) {
      app.toast?.(e.message || "Couldn't change the machine type.");
      load(); // revert to server truth
    }
  };

  const dueTotal = rows.reduce((n, m) => n + (m.overdue || m.due ? 1 : 0), 0);

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 14, flexWrap: "wrap", gap: 10 }}>
        <div>
          <h2 style={{ margin: 0 }}>Machine maintenance</h2>
          <div style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 4 }}>
            Keep every machine on schedule. Log each upkeep with the date it was done — the dashboard flags what's due or overdue.
          </div>
        </div>
        <button className="btn btn-ghost btn-small" onClick={() => setShowSchedule(s => !s)}>{showSchedule ? "Hide schedule" : "View full schedule"}</button>
      </div>

      {showSchedule && (
        <>
        <div style={{ display: "flex", gap: 6, marginBottom: 10, flexWrap: "wrap", alignItems: "center" }}>
          <span style={{ fontSize: 12, color: "var(--ink-3)" }}>Checklist for:</span>
          {[["embroidery", "Embroidery"], ["dtf", "DTF printer"], ["screenprint", "Screen print"]].map(([k, lbl]) => (
            <button key={k} className={classNames("btn", "btn-small", schedKind !== k && "btn-ghost")} onClick={() => setSchedKind(k)}>{lbl}</button>
          ))}
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 14, marginBottom: 18 }}>
          {MAINT_ORDER.map(c => (
            <div key={c} className="stitched" style={{ padding: 16 }}>
              <div className="smallcaps" style={{ color: "var(--sage-deep)" }}>{maintFor(schedKind)[c].label}</div>
              <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: 10 }}>{maintFor(schedKind)[c].when}</div>
              {maintFor(schedKind)[c].tasks.map(([t, d], i) => (
                <div key={i} style={{ marginBottom: 8 }}>
                  <div style={{ fontWeight: 600, fontSize: 14 }}>{t}</div>
                  <div style={{ fontSize: 13, color: "var(--ink-2)" }}>{d}</div>
                </div>
              ))}
            </div>
          ))}
        </div>
        </>
      )}

      {loading ? <div style={{ color: "var(--ink-3)" }}>Loading…</div>
        : !rows.length ? <ProdEmpty title="No machines yet" hint="Add machines on the Production plan screen, then track their upkeep here." />
        : (
          <>
            <div style={{ marginBottom: 12, color: dueTotal ? "var(--rose-deep)" : "var(--sage-deep)", fontWeight: 600 }}>
              {dueTotal ? `${dueTotal} machine${dueTotal === 1 ? "" : "s"} need attention` : "All machines up to date"}
            </div>
            <div style={{ display: "grid", gap: 14 }}>
              {rows.map(m => (
                <div key={m.id} className="stitched" style={{ padding: 16 }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12, flexWrap: "wrap", gap: 8 }}>
                    <div style={{ fontWeight: 700, fontSize: 16 }}>{m.name} {!m.active && <Pill tone="ink">paused</Pill>}</div>
                    <button className="btn btn-ghost btn-small" onClick={() => openHistory(m)}>{history === m.id ? "Hide log" : "View log"}</button>
                  </div>
                  {/* [HB] Machine type drives the checklist. Editable here so a DTF
                      printer added on the default type can be corrected in place. */}
                  <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: 12, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                    <span>Type:</span>
                    <select value={["embroidery", "screenprint", "dtf"].includes(m.kind) ? m.kind : "embroidery"}
                      onChange={e => setKind(m, e.target.value)}
                      style={{ fontSize: 12, padding: "2px 6px", borderRadius: 6 }}>
                      <option value="embroidery">Embroidery</option>
                      <option value="dtf">DTF printer</option>
                      <option value="screenprint">Screen print</option>
                    </select>
                  </div>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 10 }}>
                    {MAINT_ORDER.map(c => {
                      const cad = m.cadences[c] || { status: "overdue", lastDoneAt: null };
                      return (
                        <div key={c} style={{ border: "1px solid var(--line)", borderRadius: 10, padding: 12 }}>
                          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
                            <span style={{ fontWeight: 600 }}>{MAINTENANCE_SCHEDULE[c].label}</span>
                            <Pill tone={MAINT_TONE[cad.status]}>{MAINT_STATUS_LABEL[cad.status]}</Pill>
                          </div>
                          <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: 8 }}>
                            {cad.lastDoneAt ? `Last done ${new Date(cad.lastDoneAt).toLocaleDateString()}` : "Never logged"}
                          </div>
                          <button className="btn btn-small" style={{ width: "100%" }} onClick={() => setLog({ machine: m, cadence: c })}>Log {MAINTENANCE_SCHEDULE[c].label.toLowerCase()}</button>
                        </div>
                      );
                    })}
                  </div>

                  {history === m.id && (
                    <div style={{ marginTop: 12, borderTop: "1px dashed var(--line)", paddingTop: 10 }}>
                      <div className="smallcaps" style={{ color: "var(--ink-3)", marginBottom: 6 }}>Recent maintenance</div>
                      {!histItems.length ? <div style={{ color: "var(--ink-3)", fontSize: 13 }}>Nothing logged yet.</div>
                        : histItems.map(h => (
                          <div key={h.id} style={{ fontSize: 13, padding: "4px 0", borderTop: "1px solid var(--line)" }}>
                            <strong>{MAINTENANCE_SCHEDULE[h.cadence] ? MAINTENANCE_SCHEDULE[h.cadence].label : h.cadence}</strong> · {new Date(h.doneAt).toLocaleDateString()} · {h.doneBy}
                            {h.note && <span style={{ color: "var(--ink-3)" }}> — {h.note}</span>}
                          </div>
                        ))}
                    </div>
                  )}
                </div>
              ))}
            </div>
          </>
        )}

      {log && <MaintLogModal log={log} onClose={() => setLog(null)} onSaved={async () => { setLog(null); await load(); if (history) { try { setHistItems(await api.maintenanceHistory(history)); } catch (e) {} } }} />}
    </div>
  );
}

function MaintLogModal({ log, onClose, onSaved }) {
  const app = useApp();
  const { machine, cadence } = log;
  const sched = maintFor(machine.kind)[cadence];
  const [date, setDate] = useState(todayYmd());
  const [note, setNote] = useState("");
  const [checked, setChecked] = useState({});
  const [busy, setBusy] = useState(false);

  const save = async () => {
    setBusy(true);
    try {
      await api.logMaintenance({ machineId: machine.id, cadence, doneAt: date, note });
      app.toast?.(`${sched.label} maintenance logged for ${machine.name}.`);
      onSaved();
    } catch (e) { app.toast?.(e.message || "Couldn't log that."); }
    finally { setBusy(false); }
  };

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(58,42,38,0.45)", display: "flex", alignItems: "flex-start", justifyContent: "center", padding: 20, overflow: "auto", zIndex: 1000 }}>
      <div onClick={e => e.stopPropagation()} className="stitched" style={{ background: "var(--card)", maxWidth: 560, width: "100%", padding: 24, marginTop: 30 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
          <h3 style={{ margin: 0 }}>{sched.label} maintenance</h3>
          <button className="btn btn-ghost btn-small" onClick={onClose}>Close</button>
        </div>
        <div style={{ color: "var(--ink-3)", fontSize: 13, marginBottom: 14 }}>{machine.name} · {sched.when}</div>

        <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 6 }}>Checklist</div>
        <div style={{ marginBottom: 16 }}>
          {sched.tasks.map(([t, d], i) => (
            <label key={i} style={{ display: "flex", gap: 10, alignItems: "flex-start", padding: "6px 0", cursor: "pointer" }}>
              <input type="checkbox" checked={!!checked[i]} onChange={e => setChecked({ ...checked, [i]: e.target.checked })} style={{ marginTop: 3 }} />
              <span><span style={{ fontWeight: 600 }}>{t}.</span> <span style={{ color: "var(--ink-2)" }}>{d}</span></span>
            </label>
          ))}
        </div>

        <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
          <div className="field" style={{ margin: 0 }}><label>Date completed</label><input type="date" value={date} max={todayYmd()} onChange={e => setDate(e.target.value)} /></div>
          <div className="field" style={{ margin: 0, flex: 1, minWidth: 180 }}><label>Note (optional)</label><input value={note} onChange={e => setNote(e.target.value)} placeholder="Replaced needle, oiled hook…" /></div>
        </div>
        <div style={{ display: "flex", gap: 8, marginTop: 16 }}>
          <button className="btn" disabled={busy} onClick={save}>{busy ? "Saving…" : "Mark done"}</button>
          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  PROD_STANDARDS, estimateTask, sequenceThreads, generatePlan, fmtMins, fmtHours,
  ProductionPlanTab, ProductionCalendarTab, ShopFloorTab, FloorTeamTab, TrackingLinksTab,
  MaintenanceTab,
});

// [HB] Ported from white-label: admin shift corrections + weekly schedule + time-off approvals.
function ShiftOverrides({ onChanged }) {
  const app = useApp();
  const [shifts, setShifts] = useState(null);
  const [staff, setStaff] = useState([]);
  const [edit, setEdit] = useState(null);   // shift being edited, or "new"
  const [brk, setBrk] = useState(null);     // break being added/edited
  const [busy, setBusy] = useState(false);

  const load = async () => {
    try { setShifts(await api.listShifts()); } catch (e) { setShifts([]); }
  };
  useEffect(() => {
    load();
    api.listStaff().then(s => setStaff((s || []).filter(x => x.active !== false))).catch(() => {});
  }, []);

  // <input type="datetime-local"> works in local time; convert both ways.
  const toLocal = (ms) => {
    if (!ms) return "";
    const d = new Date(ms);
    const pad = (n) => String(n).padStart(2, "0");
    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
  };
  const fromLocal = (s) => (s ? new Date(s).getTime() : null);

  const startNew = () => setEdit({ id: null, staffId: staff[0] ? staff[0].id : "", startedAt: toLocal(Date.now() - 3600000), endedAt: toLocal(Date.now()), note: "" });
  const startEdit = (s) => setEdit({ id: s.id, staffId: s.staffId, startedAt: toLocal(s.startedAt), endedAt: toLocal(s.endedAt), note: s.note || "" });

  const save = async () => {
    setBusy(true);
    try {
      const body = {
        staffId: edit.staffId,
        startedAt: fromLocal(edit.startedAt),
        endedAt: fromLocal(edit.endedAt),
        note: edit.note,
      };
      if (!body.startedAt) { app.toast?.("Set a start time."); setBusy(false); return; }
      if (edit.id) await api.updateShift(edit.id, body);
      else await api.addShift(body);
      setEdit(null);
      await load();
      if (onChanged) await onChanged();
      app.toast?.("Clock updated.");
    } catch (e) { app.toast?.(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };

  const remove = async (s) => {
    if (!window.confirm(`Delete ${s.name}'s shift? This removes those hours.`)) return;
    try { await api.deleteShift(s.id); await load(); if (onChanged) await onChanged(); }
    catch (e) { app.toast?.(e.message || "Couldn't delete."); }
  };

  // ---- break corrections ----
  const startBreak = (s) => setBrk({
    id: null, shiftId: s.id, kind: "break",
    startedAt: toLocal(Date.now()), endedAt: "",
  });
  const editBreak = (s, b) => setBrk({
    id: b.id, shiftId: s.id, kind: b.kind,
    startedAt: toLocal(b.startedAt), endedAt: toLocal(b.endedAt),
  });
  const saveBreak = async () => {
    setBusy(true);
    try {
      const body = { kind: brk.kind, startedAt: fromLocal(brk.startedAt), endedAt: fromLocal(brk.endedAt) };
      if (!body.startedAt) { app.toast?.("Set a start time."); setBusy(false); return; }
      if (brk.id) await api.updateBreak(brk.id, body);
      else await api.addBreak(brk.shiftId, body);
      setBrk(null);
      await load();
      if (onChanged) await onChanged();
      app.toast?.("Break updated.");
    } catch (e) { app.toast?.(e.message || "Couldn't save the break."); }
    finally { setBusy(false); }
  };
  const removeBreak = async (b) => {
    if (!window.confirm(`Delete this ${b.kind}?`)) return;
    try { await api.deleteBreak(b.id); await load(); if (onChanged) await onChanged(); }
    catch (e) { app.toast?.(e.message || "Couldn't delete."); }
  };

  const hhmm = (ms) => ms ? new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) : "—";
  const dur = (s) => fmtMins(Math.max(0, Math.round(((s.endedAt || Date.now()) - s.startedAt) / 60000)) - (s.breakMinutes || 0));

  return (
    <div className="stitched" style={{ marginTop: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 8, marginBottom: 8 }}>
        <div>
          <h3 style={{ margin: 0 }}>Clock corrections</h3>
          <div style={{ color: "var(--ink-3)", fontSize: 13 }}>Fix a shift someone forgot to start or stop. Corrections are marked “manual”.</div>
        </div>
        <button className="btn btn-small" onClick={startNew} disabled={!staff.length}>+ Add shift</button>
      </div>

      {edit && (
        <div className="stitched stitched-ink" style={{ padding: 14, marginBottom: 12 }}>
          <div className="field-row">
            <div className="field">
              <label>Person</label>
              <select value={edit.staffId} disabled={!!edit.id} onChange={e => setEdit({ ...edit, staffId: e.target.value })}>
                {staff.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
              </select>
            </div>
            <div className="field">
              <label>Clocked in</label>
              <input type="datetime-local" value={edit.startedAt} onChange={e => setEdit({ ...edit, startedAt: e.target.value })} />
            </div>
            <div className="field">
              <label>Clocked out <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>(blank = still in)</span></label>
              <input type="datetime-local" value={edit.endedAt} onChange={e => setEdit({ ...edit, endedAt: e.target.value })} />
            </div>
          </div>
          <div className="field"><label>Reason (optional)</label><input value={edit.note} onChange={e => setEdit({ ...edit, note: e.target.value })} placeholder="Forgot to clock out" /></div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
            <button className="btn btn-ghost btn-small" onClick={() => setEdit(null)}>Cancel</button>
          </div>
        </div>
      )}

      {shifts === null ? <div style={{ color: "var(--ink-3)", fontSize: 14 }}>Loading…</div>
        : shifts.length === 0 ? <div style={{ color: "var(--ink-3)", fontSize: 14 }}>No shifts today.</div>
        : shifts.map(s => (
          <div key={s.id} style={{ borderTop: "1px solid var(--line)", padding: "10px 0" }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
              <div>
                <div style={{ fontSize: 15 }}>
                  {s.name}{" "}
                  {!s.endedAt && <Pill tone="sage">still in</Pill>}
                  {s.source === "manual" && <Pill tone="ink">manual</Pill>}
                </div>
                <div style={{ color: "var(--ink-3)", fontSize: 12 }}>
                  {dur(s)} paid{s.breakMinutes > 0 ? ` · ${fmtMins(s.breakMinutes)} break` : ""}
                  {s.note ? ` · ${s.note}` : ""}
                </div>
              </div>
              <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                <button className="btn btn-ghost btn-small" onClick={() => startEdit(s)}>Edit times</button>
                <button className="btn btn-ghost btn-small" onClick={() => startBreak(s)}>+ Break</button>
                <button className="btn btn-ghost btn-small" onClick={() => remove(s)} style={{ color: "var(--rose-deep)" }}>Delete</button>
              </div>
            </div>

            {/* The day as it actually happened — every clock event with its time. */}
            <div style={{ marginTop: 6, marginLeft: 2, fontSize: 13 }}>
              <div style={{ display: "flex", gap: 6, alignItems: "center", color: "var(--sage-deep)" }}>
                <span style={{ width: 68, color: "var(--ink-3)", fontSize: 11 }}>CLOCK IN</span>
                <span className="mono">{hhmm(s.startedAt)}</span>
              </div>
              {(s.breaks || []).map(b => (
                <div key={b.id} style={{ display: "flex", gap: 6, alignItems: "center", color: "var(--gold-deep)", marginTop: 2 }}>
                  <span style={{ width: 68, color: "var(--ink-3)", fontSize: 11, textTransform: "uppercase" }}>{b.kind}</span>
                  <span className="mono">{hhmm(b.startedAt)} → {b.endedAt ? hhmm(b.endedAt) : "still out"}</span>
                  <button className="btn btn-ghost btn-small" style={{ padding: "0 6px", fontSize: 11 }} onClick={() => editBreak(s, b)}>edit</button>
                  <button className="btn btn-ghost btn-small" style={{ padding: "0 6px", fontSize: 11, color: "var(--rose-deep)" }} onClick={() => removeBreak(b)}>×</button>
                </div>
              ))}
              <div style={{ display: "flex", gap: 6, alignItems: "center", color: s.endedAt ? "var(--ink-2)" : "var(--ink-3)", marginTop: 2 }}>
                <span style={{ width: 68, color: "var(--ink-3)", fontSize: 11 }}>CLOCK OUT</span>
                <span className="mono">{s.endedAt ? hhmm(s.endedAt) : "—"}</span>
              </div>
            </div>

            {/* Break editor for this shift */}
            {brk && brk.shiftId === s.id && (
              <div className="stitched stitched-ink" style={{ padding: 12, marginTop: 8 }}>
                <div className="field-row">
                  <div className="field">
                    <label>Type</label>
                    <select value={brk.kind} onChange={e => setBrk({ ...brk, kind: e.target.value })}>
                      <option value="break">Break</option>
                      <option value="lunch">Lunch</option>
                    </select>
                  </div>
                  <div className="field">
                    <label>Started</label>
                    <input type="datetime-local" value={brk.startedAt} onChange={e => setBrk({ ...brk, startedAt: e.target.value })} />
                  </div>
                  <div className="field">
                    <label>Ended <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>(blank = still out)</span></label>
                    <input type="datetime-local" value={brk.endedAt} onChange={e => setBrk({ ...brk, endedAt: e.target.value })} />
                  </div>
                </div>
                <div style={{ display: "flex", gap: 8 }}>
                  <button className="btn btn-small" onClick={saveBreak} disabled={busy}>{busy ? "Saving…" : "Save break"}</button>
                  <button className="btn btn-ghost btn-small" onClick={() => setBrk(null)}>Cancel</button>
                </div>
              </div>
            )}
          </div>
        ))}
    </div>
  );
}

// ---------------------------------------------------------------------------
// [HB-WL] Weekly schedule (admin) — who's rostered when, plus the time-off and
// overtime approval queue. The schedule is the PLAN; the timecard is what
// actually happened. Keeping them separate is what lets you see the gap.
// ---------------------------------------------------------------------------
// [HB-WL] The work week doesn't start on Monday everywhere — a lot of shops run
// Sunday-to-Saturday. The start day is a setting; everything below derives from it.
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const DAY_INDEX = { sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6 };

function weekStartIndex(settings) {
  const raw = String((settings && settings.week_start_day) || "monday").trim().toLowerCase();
  if (DAY_INDEX[raw] !== undefined) return DAY_INDEX[raw];
  const n = Number(raw);
  return Number.isInteger(n) && n >= 0 && n <= 6 ? n : 1;   // default Monday
}
// Day labels rotated so the week reads in the shop's own order.
function weekDayNames(startIdx) {
  return Array.from({ length: 7 }, (_, i) => DAY_NAMES[(startIdx + i) % 7]);
}

function schedWeekStart(d, startIdx) {
  const x = new Date(d);
  const back = (x.getDay() - startIdx + 7) % 7;
  x.setDate(x.getDate() - back);
  x.setHours(12, 0, 0, 0);
  return x;
}
// Kept for anything still calling the old name.
function schedMonday(d) { return schedWeekStart(d, 1); }
function schedYmd(d) {
  const x = new Date(d);
  const p = (n) => String(n).padStart(2, "0");
  return `${x.getFullYear()}-${p(x.getMonth() + 1)}-${p(x.getDate())}`;
}
function schedAddDays(ymdStr, n) {
  const d = new Date(`${ymdStr}T12:00:00`);
  d.setDate(d.getDate() + n);
  return schedYmd(d);
}

function ScheduleTab() {
  const app = useApp();
  const [settings, setSettings] = useState({});
  const startIdx = weekStartIndex(settings);
  const DAYS = weekDayNames(startIdx);
  const [monday, setMonday] = useState(() => schedYmd(schedWeekStart(new Date(), 1)));
  const [data, setData] = useState(null);       // { shifts, timeOff, pending }
  const [staff, setStaff] = useState([]);
  const [machines, setMachines] = useState([]);
  const [requests, setRequests] = useState([]);
  const [cell, setCell] = useState(null);       // { staffId, date, ... } being edited
  const [busy, setBusy] = useState(false);

  // Once the shop's week-start setting loads, snap the view to that week.
  useEffect(() => {
    api.listSettings()
      .then(cfg => {
        setSettings(cfg || {});
        const idx = weekStartIndex(cfg || {});
        setMonday(schedYmd(schedWeekStart(new Date(), idx)));
      })
      .catch(() => {});
  }, []);

  const weekEnd = schedAddDays(monday, 6);

  const load = async () => {
    try {
      const [sched, reqs] = await Promise.all([
        api.getSchedule(monday, weekEnd),
        api.listRequests().catch(() => []),
      ]);
      setData(sched);
      setRequests(reqs || []);
    } catch (e) { app.toast?.(e.message || "Couldn't load the schedule."); setData({ shifts: [], timeOff: [] }); }
  };
  useEffect(() => { load(); }, [monday]);
  useEffect(() => {
    api.listStaff().then(s => setStaff((s || []).filter(x => x.active !== false))).catch(() => {});
    api.listMachines().then(setMachines).catch(() => {});
  }, []);

  const shiftsFor = (staffId, date) =>
    ((data && data.shifts) || []).filter(s => s.staffId === staffId && s.date === date);
  const offFor = (staffId, date) =>
    ((data && data.timeOff) || []).find(o => o.staffId === staffId && date >= o.startDate && date <= o.endDate);
  // A request that's still awaiting a decision — show it so nobody schedules over it.
  const pendingFor = (staffId, date) =>
    ((data && data.pending) || []).find(o => o.staffId === staffId && o.kind !== "overtime" && date >= o.startDate && date <= o.endDate);

  const openCell = (staffId, date) => {
    const existing = shiftsFor(staffId, date)[0];
    const pend = pendingFor(staffId, date);
    if (pend && !existing) {
      const who = (staff.find(s => s.id === staffId) || {}).name || "This person";
      if (!window.confirm(`${who} has asked for ${String(pend.kind).replace("_", " ")} on ${date} and it hasn't been decided yet${pend.reason ? ` (“${pend.reason}”)` : ""}.\n\nSchedule them anyway?`)) return;
    }
    setCell(existing
      ? { id: existing.id, staffId, date, startTime: existing.startTime, endTime: existing.endTime, machineId: existing.machineId || "", note: existing.note || "" }
      : { id: null, staffId, date, startTime: "09:00", endTime: "17:00", machineId: "", note: "" });
  };

  const saveCell = async () => {
    setBusy(true);
    try {
      const body = { staffId: cell.staffId, date: cell.date, startTime: cell.startTime, endTime: cell.endTime, machineId: cell.machineId || null, note: cell.note };
      if (cell.id) await api.updateSchedule(cell.id, body);
      else await api.addSchedule(body);
      setCell(null);
      await load();
    } catch (e) { app.toast?.(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };
  const removeCell = async () => {
    if (!cell.id) { setCell(null); return; }
    setBusy(true);
    try { await api.deleteSchedule(cell.id); setCell(null); await load(); }
    catch (e) { app.toast?.(e.message || "Couldn't remove."); }
    finally { setBusy(false); }
  };

  const copyLastWeek = async () => {
    const prev = schedAddDays(monday, -7);
    if (!window.confirm(`Copy the week of ${prev} into this week? People with approved time off are skipped.`)) return;
    setBusy(true);
    try {
      const r = await api.copyWeek({ fromMonday: prev, toMonday: monday });
      app.toast?.(`${r.created} shift${r.created === 1 ? "" : "s"} copied${r.skipped ? `, ${r.skipped} skipped` : ""}.`);
      await load();
    } catch (e) { app.toast?.(e.message || "Couldn't copy."); }
    finally { setBusy(false); }
  };

  const decide = async (r, status) => {
    const note = status === "denied" ? (window.prompt("Reason for denying (optional):") || "") : "";
    try {
      const res = await api.decideRequest(r.id, { status, note });
      if (status === "approved" && res && res.conflicts && res.conflicts.length) {
        const days = res.conflicts.map(c => `${c.date} (${c.startTime}–${c.endTime})`).join(", ");
        app.toast?.(`Approved — but ${r.name} is still rostered on ${days}. Clear those shifts.`);
      } else {
        app.toast?.(status === "approved" ? "Approved." : "Denied.");
      }
      await load();
    } catch (e) { app.toast?.(e.message || "Couldn't update."); }
  };

  const pending = requests.filter(r => r.status === "pending");
  const decided = requests.filter(r => r.status !== "pending").slice(0, 12);
  const weekHours = (staffId) =>
    DAYS.reduce((sum, _, i) => sum + shiftsFor(staffId, schedAddDays(monday, i)).reduce((s, sh) => s + sh.hours, 0), 0);

  const KIND_LABEL = { time_off: "Time off", overtime: "Overtime", other: "Other" };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 10 }}>
        <h2 style={{ margin: 0 }}>Schedule</h2>
        <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
          <button className="btn btn-ghost btn-small" onClick={() => setMonday(schedAddDays(monday, -7))}>← Prev</button>
          <span style={{ color: "var(--ink-2)", fontSize: 14 }}>{monday} → {weekEnd}</span>
          <button className="btn btn-ghost btn-small" onClick={() => setMonday(schedAddDays(monday, 7))}>Next →</button>
          <button className="btn btn-ghost btn-small" onClick={() => setMonday(schedYmd(schedWeekStart(new Date(), startIdx)))}>This week</button>
          <button className="btn btn-ghost btn-small" onClick={copyLastWeek} disabled={busy}>Copy last week</button>
        </div>
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 4 }}>
        Click any cell to roster someone. This is the plan — the Timecard shows what actually happened.
      </p>

      {/* Pending approvals */}
      {pending.length > 0 && (
        <div className="stitched stitched-gold" style={{ marginBottom: 14 }}>
          <h3 style={{ marginTop: 0 }}>Awaiting your decision <span style={{ color: "var(--ink-3)", fontSize: 15 }}>({pending.length})</span></h3>
          {pending.map(r => (
            <div key={r.id} style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap", alignItems: "center", borderTop: "1px solid var(--line)", padding: "8px 0" }}>
              <div>
                <div style={{ fontSize: 15 }}>
                  {r.name} · <strong>{KIND_LABEL[r.kind] || r.kind}</strong>
                  {r.hours ? ` · ${r.hours}h` : ""}
                </div>
                <div style={{ color: "var(--ink-3)", fontSize: 13 }}>
                  {r.startDate}{r.endDate !== r.startDate ? ` → ${r.endDate}` : ""}
                  {r.reason ? ` · ${r.reason}` : ""}
                </div>
              </div>
              <div style={{ display: "flex", gap: 6 }}>
                <button className="btn btn-small" onClick={() => decide(r, "approved")}>Approve</button>
                <button className="btn btn-ghost btn-small" onClick={() => decide(r, "denied")} style={{ color: "var(--rose-deep)" }}>Deny</button>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Availability at a glance — who's off before you start rostering. */}
      {data && staff.length > 0 && (
        <div className="stitched" style={{ marginBottom: 12, padding: 10 }}>
          <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginBottom: 6 }}>Availability this week</div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 6 }}>
            {DAYS.map((d, i) => {
              const date = schedAddDays(monday, i);
              const isToday = date === schedYmd(new Date());
              const offToday = staff.filter(p => offFor(p.id, date));
              const pendToday = staff.filter(p => !offFor(p.id, date) && pendingFor(p.id, date));
              const working = staff.filter(p => !offFor(p.id, date) && shiftsFor(p.id, date).length > 0);
              const free = staff.length - offToday.length - working.length;
              return (
                <div key={d} style={{
                  borderRadius: 6, padding: 8, fontSize: 11, lineHeight: 1.45,
                  border: isToday ? "1px solid var(--sage)" : "1px solid var(--line)",
                  background: offToday.length ? "rgba(var(--rose-rgb, 184,106,120), .06)" : "transparent",
                }}>
                  <div style={{ fontWeight: 700, color: isToday ? "var(--sage-deep)" : "var(--ink-2)", marginBottom: 3 }}>
                    {d} <span style={{ fontWeight: 400, color: "var(--ink-3)" }}>{date.slice(5)}</span>
                  </div>
                  <div style={{ color: "var(--sage-deep)" }}>{working.length} working</div>
                  {free > 0 && <div style={{ color: "var(--ink-3)" }}>{free} unscheduled</div>}
                  {offToday.length > 0 && (
                    <div style={{ color: "var(--rose-deep)", marginTop: 3 }}>
                      Off: {offToday.map(p => p.name.split(" ")[0]).join(", ")}
                    </div>
                  )}
                  {pendToday.length > 0 && (
                    <div style={{ color: "var(--gold-deep)", marginTop: 3 }}>
                      Pending: {pendToday.map(p => p.name.split(" ")[0]).join(", ")}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
          <div style={{ display: "flex", gap: 12, marginTop: 8, fontSize: 11, color: "var(--ink-3)", flexWrap: "wrap" }}>
            <span><span style={{ display: "inline-block", width: 10, height: 10, background: "var(--sage)", borderRadius: 2, marginRight: 4 }} />Scheduled</span>
            <span><span style={{ display: "inline-block", width: 10, height: 10, background: "var(--line)", borderRadius: 2, marginRight: 4 }} />Approved time off — can't be scheduled</span>
            <span><span style={{ display: "inline-block", width: 10, height: 10, background: "var(--gold)", borderRadius: 2, marginRight: 4 }} />Request pending a decision</span>
          </div>
        </div>
      )}

      {/* Week grid */}
      {!data ? <div style={{ color: "var(--ink-3)" }}>Loading…</div> : staff.length === 0 ? (
        <div className="stitched" style={{ padding: 24, textAlign: "center", color: "var(--ink-2)" }}>
          Add people under Floor team first, then you can schedule them.
        </div>
      ) : (
        <div className="stitched" style={{ overflowX: "auto", padding: 10 }}>
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13, minWidth: 720 }}>
            <thead>
              <tr>
                <th style={{ textAlign: "left", padding: "6px 8px", color: "var(--ink-3)", fontSize: 12 }}>Person</th>
                {DAYS.map((d, i) => {
                  const date = schedAddDays(monday, i);
                  const isToday = date === schedYmd(new Date());
                  return (
                    <th key={d} style={{ padding: "6px 4px", color: isToday ? "var(--sage-deep)" : "var(--ink-3)", fontSize: 12, fontWeight: isToday ? 700 : 400 }}>
                      {d}<br /><span style={{ fontWeight: 400 }}>{date.slice(5)}</span>
                    </th>
                  );
                })}
                <th style={{ padding: "6px 8px", color: "var(--ink-3)", fontSize: 12, textAlign: "right" }}>Hrs</th>
              </tr>
            </thead>
            <tbody>
              {staff.map(p => (
                <tr key={p.id} style={{ borderTop: "1px solid var(--line)" }}>
                  <td style={{ padding: "6px 8px", whiteSpace: "nowrap" }}>{p.name}</td>
                  {DAYS.map((_, i) => {
                    const date = schedAddDays(monday, i);
                    const sh = shiftsFor(p.id, date)[0];
                    const off = offFor(p.id, date);
                    const pend = !off && pendingFor(p.id, date);
                    const title = off ? `Approved ${String(off.kind).replace("_", " ")}${off.reason ? " — " + off.reason : ""}`
                      : pend ? `Pending ${String(pend.kind).replace("_", " ")} request${pend.reason ? " — " + pend.reason : ""} — decide it before scheduling`
                      : "";
                    return (
                      <td key={i} onClick={() => !off && openCell(p.id, date)} title={title}
                          style={{ padding: 3, textAlign: "center", cursor: off ? "not-allowed" : "pointer" }}>
                        {off ? (
                          <div style={{ background: "var(--line)", color: "var(--ink-3)", borderRadius: 5, padding: "5px 2px", fontSize: 11 }}>
                            {off.kind === "time_off" ? "Off" : "—"}
                          </div>
                        ) : sh ? (
                          <div style={{
                            background: "var(--sage)", color: "#fff", borderRadius: 5, padding: "5px 2px",
                            fontSize: 11, lineHeight: 1.3,
                            boxShadow: pend ? "inset 0 0 0 2px var(--gold-deep)" : "none",
                          }}>
                            {sh.startTime}–{sh.endTime}
                            {sh.machineName ? <div style={{ fontSize: 10, opacity: .85 }}>{sh.machineName}</div> : null}
                            {pend ? <div style={{ fontSize: 9, opacity: .95 }}>⚠ off requested</div> : null}
                          </div>
                        ) : pend ? (
                          <div style={{ background: "var(--gold)", color: "#fff", borderRadius: 5, padding: "5px 2px", fontSize: 10, lineHeight: 1.3 }}>
                            Pending
                          </div>
                        ) : (
                          <div style={{ color: "var(--line)", fontSize: 16 }}>+</div>
                        )}
                      </td>
                    );
                  })}
                  <td style={{ padding: "6px 8px", textAlign: "right", color: "var(--ink-2)" }}>{weekHours(p.id) || ""}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {/* Cell editor */}
      {cell && (
        <div className="stitched stitched-ink" style={{ padding: 14, marginTop: 12 }}>
          <h3 style={{ marginTop: 0 }}>
            {(staff.find(s => s.id === cell.staffId) || {}).name} · {cell.date}
          </h3>
          <div className="field-row">
            <div className="field"><label>Start</label><input type="time" value={cell.startTime} onChange={e => setCell({ ...cell, startTime: e.target.value })} /></div>
            <div className="field"><label>End</label><input type="time" value={cell.endTime} onChange={e => setCell({ ...cell, endTime: e.target.value })} /></div>
            <div className="field">
              <label>Machine (optional)</label>
              <select value={cell.machineId} onChange={e => setCell({ ...cell, machineId: e.target.value })}>
                <option value="">Any / unassigned</option>
                {machines.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
              </select>
            </div>
          </div>
          <div className="field"><label>Note (optional)</label><input value={cell.note} onChange={e => setCell({ ...cell, note: e.target.value })} placeholder="Early finish, training…" /></div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" onClick={saveCell} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
            {cell.id && <button className="btn btn-ghost btn-small" onClick={removeCell} style={{ color: "var(--rose-deep)" }}>Remove</button>}
            <button className="btn btn-ghost btn-small" onClick={() => setCell(null)}>Cancel</button>
          </div>
        </div>
      )}

      {/* Recently decided */}
      {decided.length > 0 && (
        <div className="stitched" style={{ marginTop: 14 }}>
          <h3 style={{ marginTop: 0 }}>Recent decisions</h3>
          {decided.map(r => (
            <div key={r.id} style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap", borderTop: "1px solid var(--line)", padding: "6px 0", fontSize: 13 }}>
              <div>
                {r.name} · {KIND_LABEL[r.kind] || r.kind} · {r.startDate}{r.endDate !== r.startDate ? ` → ${r.endDate}` : ""}
                {r.decisionNote ? <span style={{ color: "var(--ink-3)" }}> · {r.decisionNote}</span> : null}
              </div>
              <div style={{ color: r.status === "approved" ? "var(--sage-deep)" : "var(--rose-deep)" }}>{r.status}</div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// [HB-WL] Pay periods.
//
// Bookkeeping is date-based: hours worked on 30 June are a June expense, hours
// on 1 July are a July expense, even when one paycheck covers both. That's the
// right basis for a monthly P&L, and it means a pay period straddling two
// months never distorts the books.
//
// What you still need is the payroll figure itself: "for the period ending
// 11 July, what do I owe each person?" That's what this computes.
// ---------------------------------------------------------------------------

// [HB] Admin time clock — who's on the clock right now with today's hours, plus
// the shift-correction tools (fix a forgotten clock-in/out, add a missed shift,
// edit or remove breaks).
function TimeClockTab() {
  const [card, setCard] = useState(null);
  const [, setTick] = useState(0);

  const load = async () => { try { setCard(await api.timecard()); } catch (e) { setCard({ people: [] }); } };
  useEffect(() => {
    load();
    const poll = setInterval(load, 30000);
    const clock = setInterval(() => setTick(t => t + 1), 1000);
    return () => { clearInterval(poll); clearInterval(clock); };
  }, []);

  const hhmm = (m) => (m >= 60 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${m || 0}m`);
  const mins = (since) => Math.max(0, Math.round((Date.now() - since) / 60000));
  const people = (card && card.people) || [];
  const onNow = people.filter(p => p.onClock);

  return (
    <div>
      <h2 style={{ marginBottom: 4 }}>Time clock</h2>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>
        Floor staff clock in, take breaks and clock out from their own floor screen. Their hours land here,
        and you can correct anything that was mis-clocked.
      </p>

      <div className="stitched" style={{ padding: 14, marginBottom: 16 }}>
        <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 8 }}>On the clock now ({onNow.length})</div>
        {onNow.length === 0
          ? <div style={{ color: "var(--ink-3)", fontSize: 14 }}>Nobody is clocked in right now.</div>
          : (
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 10 }}>
              {onNow.map(p => (
                <div key={p.staffId} className="stitched stitched-sage" style={{ padding: 10 }}>
                  <div style={{ fontWeight: 700 }}>{p.name}</div>
                  <div style={{ fontSize: 13, color: "var(--sage-deep)" }}>In {hhmm(mins(p.since))} · {hhmm(p.paidMinutes)} paid today</div>
                  {p.breakMinutes > 0 && <div style={{ fontSize: 12, color: "var(--ink-3)" }}>{hhmm(p.breakMinutes)} on breaks</div>}
                </div>
              ))}
            </div>
          )}
        {people.length > onNow.length && (
          <div style={{ marginTop: 10, fontSize: 13, color: "var(--ink-2)" }}>
            {people.filter(p => !p.onClock).map(p => `${p.name} — ${hhmm(p.paidMinutes)}`).join(" · ")} clocked earlier today.
          </div>
        )}
      </div>

      <ShiftOverrides onChanged={load} />
    </div>
  );
}

Object.assign(window, { ScheduleTab, ShiftOverrides, TimeClockTab });
