/* ==================================================================
   Operations → Sheet Builder
   ------------------------------------------------------------------
   The office's hand-made checklists, built over the live roster:
     1. pick classes (and boarders / day scholars),
     2. pick roster fields — Roll, Name, Father's name, …,
     3. add hand-entered columns — "Collected marksheet" as a tick box,
        "Remarks" as text, …,
   then fill it in on screen and export CSV / Excel / PDF.

   A sheet is saved once (Save sheet); after that every change saves
   itself — config with a short debounce, each typed cell on its own
   (PATCH …/cells), so two people ticking the same list never overwrite
   each other. Roster fields are never stored: they are read live, so
   an old sheet picks up a name corrected in the Ledger.

   Backend: backend/src/routes/admin_sheets.ts. Every top-level name
   here is prefixed Sb/SB_ — the cockpit's JSX files share one global
   scope, and a clashing `const` kills the whole later file.
   ================================================================== */

const SB_COLUMN_TYPES = [
  { value: "check",  label: "Tick box" },
  { value: "text",   label: "Text" },
  { value: "number", label: "Number" },
  { value: "date",   label: "Date" },
];

// The columns the office keeps asking for, one tap each.
const SB_PRESETS = [
  { label: "Collected marksheet",      type: "check" },
  { label: "Applied for scholarship",  type: "check" },
  { label: "Documents submitted",      type: "check" },
  { label: "Fees cleared",             type: "check" },
  { label: "Signature",                type: "text" },
  { label: "Remarks",                  type: "text" },
];

const SB_DEFAULT_FIELDS = ["sl", "roll_no", "name", "class_label"];

// Classes in school order, not alphabetical ("IX" must not sort before "V").
const SB_CLASS_ORDER = ["NURSERY", "INFANT I", "INFANT II", "I", "II", "III", "IV", "V",
  "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
const sbClassRank = (level) => {
  const i = SB_CLASS_ORDER.indexOf(String(level || "").trim().toUpperCase());
  return i === -1 ? 999 : i;
};
const sbRollKey = (roll) => {
  const m = String(roll || "").match(/(\d+)\s*$/);
  return m ? Number(m[1]) : Number.MAX_SAFE_INTEGER;
};

const sbNewId = () => "c" + Math.random().toString(36).slice(2, 9);

const sbBlankSheet = () => ({
  id: null,
  title: "",
  class_section_ids: [],
  student_type: "",
  fields: [...SB_DEFAULT_FIELDS],
  custom_columns: [{ id: sbNewId(), label: "Collected marksheet", type: "check" }],
  cell_values: {},
});

const sbCustomWidth = (c) =>
  c.type === "check" ? Math.min(16, Math.max(8, Math.ceil(c.label.length * 0.75)))
    : c.type === "number" ? 10
    : c.type === "date" ? 12
    : 22;

// Same quoting rule as CX.exportCsv, plus a byte-order mark: without it Excel
// opens a UTF-8 file as Latin-1 and every Bengali name turns to mojibake.
const sbDownloadCsv = (filename, rows) => {
  const csv = "﻿" + rows
    .map((r) => r.map((v) => `"${String(v ?? "").replace(/"/g, '""')}"`).join(","))
    .join("\r\n");
  const a = document.createElement("a");
  a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
  a.download = filename;
  a.click();
  setTimeout(() => URL.revokeObjectURL(a.href), 10000);
};

const sbSlug = (s) =>
  (String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "sheet")
  + "-" + new Date().toISOString().slice(0, 10);

const SB_CSS = `
.sb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.sb-tile { text-align: left; cursor: pointer; padding: 18px 20px; display: flex; flex-direction: column; gap: 6px;
  border: 1px solid transparent; transition: border-color .12s; font: inherit; color: inherit; }
.sb-tile:hover { border-color: var(--line-strong); }
.sb-tile-title { font-size: 15px; font-weight: 600; color: var(--ink-0); overflow-wrap: anywhere; }
.sb-tile-meta { font-size: 12px; color: var(--ink-3); }
.sb-setup { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; }
.sb-step-label { font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 10px; }
.sb-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.sb-chip { border: 1px solid var(--line-strong); background: #fff; color: var(--ink-1); border-radius: 999px;
  padding: 5px 11px; font-size: 12px; cursor: pointer; font: inherit; font-size: 12px; line-height: 1.3; }
.sb-chip:hover { background: var(--bg-2); }
.sb-chip.on { background: var(--ink-btn); color: #fff; border-color: transparent; }
.sb-chip.add { border-style: dashed; color: var(--ink-2); }
.sb-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.sb-item { display: flex; align-items: center; gap: 6px; background: var(--bg-2); border-radius: 10px; padding: 5px 6px 5px 10px; }
.sb-item-label { flex: 1; min-width: 0; font-size: 12.5px; color: var(--ink-0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sb-item .input, .sb-item .select { padding: 5px 8px; font-size: 12.5px; border-radius: 8px; }
.sb-item .select { width: auto; flex: none; }
.sb-ibtn { border: 0; background: transparent; color: var(--ink-3); cursor: pointer; width: 26px; height: 26px;
  border-radius: 7px; font-size: 13px; line-height: 1; flex: none; }
.sb-ibtn:hover:not(:disabled) { background: var(--bg-3); color: var(--ink-0); }
.sb-ibtn:disabled { opacity: .3; cursor: default; }
.sb-toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 14px; }
.sb-table-wrap { overflow: auto; max-height: 70vh; border: 1px solid var(--line); border-radius: 12px; }
.sb-table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 12.5px; }
.sb-table th { position: sticky; top: 0; z-index: 1; background: var(--bg-2); text-align: left; font-weight: 600;
  color: var(--ink-1); padding: 9px 10px; border-bottom: 1px solid var(--line); white-space: nowrap; }
.sb-table th.sb-own { background: var(--accent-bg); min-width: 120px; }
.sb-table td { padding: 5px 10px; border-bottom: 1px solid var(--line); color: var(--ink-0); vertical-align: middle; }
.sb-table tr:last-child td { border-bottom: 0; }
.sb-table td.sb-num { text-align: right; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.sb-table td.sb-tick, .sb-table th.sb-tick { text-align: center; }
.sb-table td.sb-own { background: rgba(59,123,246,.03); }
.sb-table input[type=checkbox] { width: 17px; height: 17px; cursor: pointer; accent-color: var(--accent); }
.sb-cell { border: 1px solid transparent; background: transparent; border-radius: 6px; padding: 4px 6px;
  font: inherit; color: inherit; width: 100%; min-width: 120px; }
.sb-cell:hover { border-color: var(--line-strong); background: #fff; }
.sb-cell:focus { outline: none; border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px var(--accent-bg); }
.sb-count { font-weight: 500; color: var(--ink-3); margin-left: 4px; }
.sb-status { font-size: 12px; color: var(--ink-3); white-space: nowrap; }
.sb-err { margin-bottom: 14px; padding: 10px 14px; border-radius: 10px; background: var(--red-bg); color: var(--red); font-size: 13px; }
@media (max-width: 1100px) { .sb-setup { grid-template-columns: 1fr 1fr; } }
@media (max-width: 760px) {
  .sb-setup { grid-template-columns: 1fr; }
  .sb-toolbar > * { flex: 1 1 100%; }
  .sb-table-wrap { max-height: none; }
}
`;

/* ---------- Saved sheets list ---------- */

const SbList = ({ setScreen, onOpen, onNew }) => {
  const [sheets, setSheets] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const sections = window.KX?.CLASS_SECTIONS || [];
  const labelOf = (id) => sections.find((c) => c.id === id)?.label || "?";

  const load = React.useCallback(() => {
    window.KXApi.get("/admin/sheets").then(setSheets).catch((e) => setErr(String(e.message || e)));
  }, []);
  React.useEffect(load, [load]);

  const remove = async (e, s) => {
    e.stopPropagation();
    if (!window.confirm(`Delete "${s.title}"? Everything typed into it goes too.`)) return;
    try { await window.KXApi.del(`/admin/sheets/${s.id}`); load(); }
    catch (ex) { setErr(String(ex.message || ex)); }
  };

  return (
    <div className="cx-page">
      <style>{SB_CSS}</style>
      <CX.PageHead
        eyebrow="Operations"
        title="Sheet Builder"
        onBack={() => setScreen("finance")}
        actions={<button className="cx-btn-dark" onClick={onNew}><CX.Icon name="plus" size={16}/> New sheet</button>}
      />
      {err && <div className="sb-err">{err}</div>}
      {!sheets && !err && <CX.Card><CX.Skeleton rows={1} height={120}/></CX.Card>}
      {sheets && sheets.length === 0 && (
        <CX.Card>
          <CX.Empty icon="file" title="No sheets yet"
            hint="Make a class list with your own columns — Collected marksheet, Applied for scholarship, Documents submitted — tick it on screen and export it as CSV, Excel or PDF."/>
          <div style={{ display: "flex", justifyContent: "center", paddingBottom: 8 }}>
            <button className="cx-btn-dark sm" onClick={onNew}><CX.Icon name="plus" size={14}/> New sheet</button>
          </div>
        </CX.Card>
      )}
      {sheets && sheets.length > 0 && (
        <div className="sb-grid">
          {sheets.map((s) => (
            <button key={s.id} className="cx-card sb-tile" onClick={() => onOpen(s.id)}>
              <div style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
                <span className="sb-tile-title" style={{ flex: 1 }}>{s.title}</span>
                <span className="sb-ibtn" role="button" title="Delete sheet" aria-label="Delete sheet"
                      onClick={(e) => remove(e, s)}>✕</span>
              </div>
              <div className="sb-tile-meta">
                {s.class_section_ids.length
                  ? s.class_section_ids.map(labelOf).join(", ")
                  : "Whole school"}
                {s.student_type === "boarder" ? " · boarders" : s.student_type === "day_scholar" ? " · day scholars" : ""}
              </div>
              <div className="sb-tile-meta">
                {s.custom_columns.map((c) => c.label).join(" · ") || `${s.fields.length} roster columns`}
              </div>
              <div className="sb-tile-meta" style={{ marginTop: 4 }}>
                Updated {CX.relTime(s.updated_at)}{s.created_by_name ? ` · by ${s.created_by_name}` : ""}
              </div>
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

/* ---------- One text/number/date cell ----------
   Holds its own draft so typing never re-renders the whole table, and
   commits on blur or Enter — one save per finished entry, not per key. */
const SbCellInput = ({ type, value, onCommit }) => {
  const [draft, setDraft] = React.useState(value ?? "");
  React.useEffect(() => { setDraft(value ?? ""); }, [value]);
  const commit = () => { if (String(draft) !== String(value ?? "")) onCommit(draft); };
  return (
    <input className="sb-cell"
           type={type === "number" ? "number" : type === "date" ? "date" : "text"}
           value={draft}
           onChange={(e) => setDraft(e.target.value)}
           onBlur={commit}
           onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }}/>
  );
};

/* ---------- Editor ---------- */

const SbEditor = ({ sheetId, onBack, onCreated }) => {
  const [sheet, setSheet] = React.useState(sheetId ? null : sbBlankSheet());
  const [fieldsCatalog, setFieldsCatalog] = React.useState([]);
  const [roster, setRoster] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [status, setStatus] = React.useState(sheetId ? "saved" : "new"); // new | saving | saved | error
  const [showSetup, setShowSetup] = React.useState(!sheetId);
  const [search, setSearch] = React.useState("");
  const [onlyMissing, setOnlyMissing] = React.useState(""); // column id
  const [exporting, setExporting] = React.useState(null);
  const [newColLabel, setNewColLabel] = React.useState("");
  const [newColType, setNewColType] = React.useState("check");

  const sections = React.useMemo(() => {
    const all = window.KX?.CLASS_SECTIONS || [];
    return [...all].sort((a, b) =>
      String(b.academic_year).localeCompare(String(a.academic_year))
      || sbClassRank(a.class_level) - sbClassRank(b.class_level)
      || String(a.section || "").localeCompare(String(b.section || "")));
  }, []);
  const years = React.useMemo(() => [...new Set(sections.map((c) => c.academic_year))], [sections]);
  const [year, setYear] = React.useState(years[0] || "");

  // ── Load ──
  React.useEffect(() => {
    window.KXApi.get("/admin/sheets/fields").then(setFieldsCatalog).catch((e) => setErr(String(e.message || e)));
  }, []);
  React.useEffect(() => {
    if (!sheetId) return;
    window.KXApi.get(`/admin/sheets/${sheetId}`)
      .then((s) => {
        setSheet({ ...s, cell_values: s.cell_values || {} });
        // Open on the year the sheet's classes belong to.
        const y = sections.find((c) => c.id === s.class_section_ids?.[0])?.academic_year;
        if (y) setYear(y);
      })
      .catch((e) => setErr(String(e.message || e)));
  }, [sheetId]);

  const rosterKey = sheet ? `${[...sheet.class_section_ids].sort().join(",")}|${sheet.student_type}` : null;
  React.useEffect(() => {
    if (!sheet) return;
    let alive = true;
    setRoster(null);
    const qs = new URLSearchParams();
    if (sheet.class_section_ids.length) qs.set("class_section_ids", sheet.class_section_ids.join(","));
    if (sheet.student_type) qs.set("student_type", sheet.student_type);
    window.KXApi.get(`/admin/sheets/roster?${qs}`)
      .then((r) => { if (alive) setRoster(r); })
      .catch((e) => { if (alive) setErr(String(e.message || e)); });
    return () => { alive = false; };
  }, [rosterKey]);

  // ── Config autosave (saved sheets only) ──
  const configKey = sheet ? JSON.stringify([sheet.title, sheet.class_section_ids, sheet.student_type, sheet.fields, sheet.custom_columns]) : "";
  const lastSavedConfig = React.useRef(null);
  React.useEffect(() => {
    if (!sheet?.id) return;
    if (lastSavedConfig.current === null) { lastSavedConfig.current = configKey; return; }
    if (lastSavedConfig.current === configKey) return;
    if (!sheet.title.trim()) return; // the server refuses an untitled sheet
    if (!sheet.fields.length && !sheet.custom_columns.some((c) => c.label.trim())) return;
    setStatus("saving");
    const t = setTimeout(async () => {
      try {
        const cfg = sheetConfig(sheet);
        await window.KXApi.put(`/admin/sheets/${sheet.id}`, cfg);
        lastSavedConfig.current = configKey;
        // The server drops the cells of removed columns; do the same locally.
        // Filtering rather than adopting the response's cell_values, so a tick
        // PATCHed while this PUT was in flight is not rolled back on screen.
        const keep = new Set(cfg.custom_columns.map((c) => c.id));
        setSheet((s) => ({
          ...s,
          cell_values: Object.fromEntries(Object.entries(s.cell_values || {}).map(([sid, row]) => [
            sid, Object.fromEntries(Object.entries(row).filter(([cid]) => keep.has(cid))),
          ])),
        }));
        setStatus("saved"); setErr(null);
      } catch (e) { setStatus("error"); setErr(String(e.message || e)); }
    }, 700);
    return () => clearTimeout(t);
  }, [configKey, sheet?.id]);

  const sheetConfig = (s) => ({
    title: s.title.trim(),
    class_section_ids: s.class_section_ids,
    student_type: s.student_type,
    fields: s.fields,
    custom_columns: s.custom_columns.filter((c) => c.label.trim()).map((c) => ({ ...c, label: c.label.trim() })),
  });

  const saveNew = async () => {
    if (!sheet.title.trim()) { setErr("Give the sheet a name first — e.g. \"Class X marksheet collection\"."); return; }
    setStatus("saving"); setErr(null);
    try {
      const out = await window.KXApi.post("/admin/sheets", { ...sheetConfig(sheet), cell_values: sheet.cell_values });
      lastSavedConfig.current = null;
      setSheet({ ...out, cell_values: out.cell_values || {} });
      setStatus("saved");
      onCreated(out.id);
    } catch (e) { setStatus("error"); setErr(String(e.message || e)); }
  };

  // ── Edits ──
  const patch = (fn) => setSheet((s) => ({ ...s, ...fn(s) }));

  const setCell = async (studentId, col, raw) => {
    const value = col.type === "check" ? (raw ? true : null)
      : (raw === "" || raw === null || raw === undefined) ? null
      : col.type === "number" ? Number(raw) : raw;
    const prev = sheet.cell_values?.[studentId]?.[col.id];
    const write = (v) => setSheet((s) => {
      const row = { ...(s.cell_values[studentId] || {}) };
      if (v === null || v === undefined) delete row[col.id]; else row[col.id] = v;
      return { ...s, cell_values: { ...s.cell_values, [studentId]: row } };
    });
    write(value);
    if (!sheet.id) return; // unsaved: kept locally, sent with the first save
    try {
      setStatus("saving");
      await window.KXApi.patch(`/admin/sheets/${sheet.id}/cells`, { student_id: studentId, column_id: col.id, value });
      setStatus("saved");
    } catch (e) {
      write(prev ?? null);
      setStatus("error"); setErr(`Not saved: ${String(e.message || e)}`);
    }
  };

  const toggleSection = (id) => patch((s) => ({
    class_section_ids: s.class_section_ids.includes(id)
      ? s.class_section_ids.filter((x) => x !== id)
      : [...s.class_section_ids, id],
  }));
  const toggleField = (key) => patch((s) => ({
    fields: s.fields.includes(key) ? s.fields.filter((k) => k !== key) : [...s.fields, key],
  }));
  const moveIn = (listKey, idx, dir) => patch((s) => {
    const arr = [...s[listKey]];
    const j = idx + dir;
    if (j < 0 || j >= arr.length) return {};
    [arr[idx], arr[j]] = [arr[j], arr[idx]];
    return { [listKey]: arr };
  });
  const addColumn = (label, type) => {
    const l = String(label || "").trim();
    if (!l) return;
    patch((s) => ({ custom_columns: [...s.custom_columns, { id: sbNewId(), label: l, type }] }));
    setNewColLabel("");
  };
  const updateColumn = (id, change) => patch((s) => ({
    custom_columns: s.custom_columns.map((c) => (c.id === id ? { ...c, ...change } : c)),
  }));
  const removeColumn = (col) => {
    const filled = Object.values(sheet.cell_values || {}).filter((r) => r[col.id] != null).length;
    if (filled && !window.confirm(`Remove "${col.label}"? The ${filled} entr${filled === 1 ? "y" : "ies"} in it will be deleted.`)) return;
    patch((s) => ({ custom_columns: s.custom_columns.filter((c) => c.id !== col.id) }));
    if (onlyMissing === col.id) setOnlyMissing("");
  };

  // ── Derived table ──
  const fieldByKey = React.useMemo(() => Object.fromEntries(fieldsCatalog.map((f) => [f.key, f])), [fieldsCatalog]);
  const sectionById = React.useMemo(() => Object.fromEntries(sections.map((c) => [c.id, c])), [sections]);
  const fieldCols = sheet ? sheet.fields.map((k) => fieldByKey[k]).filter(Boolean) : [];
  const customCols = sheet ? sheet.custom_columns.filter((c) => c.label.trim()) : [];

  const ordered = React.useMemo(() => {
    if (!roster) return [];
    return [...roster].sort((a, b) => {
      const ca = sectionById[a.class_section_id], cb = sectionById[b.class_section_id];
      return sbClassRank(ca?.class_level) - sbClassRank(cb?.class_level)
        || String(ca?.section || "").localeCompare(String(cb?.section || ""))
        || sbRollKey(a.roll_no) - sbRollKey(b.roll_no)
        || String(a.roll_no).localeCompare(String(b.roll_no))
        || String(a.name).localeCompare(String(b.name));
    });
  }, [roster, sectionById]);

  const shown = React.useMemo(() => {
    const q = search.trim().toLowerCase();
    return ordered.filter((r) => {
      if (onlyMissing && sheet?.cell_values?.[r.id]?.[onlyMissing] != null) return false;
      if (!q) return true;
      return [r.name, r.roll_no, r.admission_no, r.ledger_student_uid, r.father_name, r.mother_name, r.guardian_name]
        .some((v) => String(v || "").toLowerCase().includes(q));
    });
  }, [ordered, search, onlyMissing, sheet?.cell_values]);

  const filledCount = (colId) => ordered.filter((r) => sheet.cell_values?.[r.id]?.[colId] != null).length;

  // ── Export ──
  const describe = () => {
    const cls = sheet.class_section_ids.length
      ? sheet.class_section_ids.map((id) => sectionById[id]?.label || "?").join(", ")
      : "All classes";
    const type = sheet.student_type === "boarder" ? " · boarders" : sheet.student_type === "day_scholar" ? " · day scholars" : "";
    const filt = onlyMissing ? ` · only rows missing "${customCols.find((c) => c.id === onlyMissing)?.label}"` : "";
    const q = search.trim() ? ` · matching "${search.trim()}"` : "";
    return `${cls}${type}${filt}${q} · ${shown.length} student${shown.length === 1 ? "" : "s"}`;
  };
  const exportColumns = () => [
    ...fieldCols.map((f) => ({ header: f.label, width: f.width, type: f.type || "text" })),
    ...customCols.map((c) => ({ header: c.label, width: sbCustomWidth(c), type: c.type })),
  ];
  const exportRows = () => shown.map((r, i) => [
    ...fieldCols.map((f) => (f.key === "sl" ? i + 1 : r[f.key] ?? "")),
    ...customCols.map((c) => {
      const v = sheet.cell_values?.[r.id]?.[c.id];
      return c.type === "check" ? v === true : v ?? "";
    }),
  ]);

  const doExport = async (kind) => {
    if (!fieldCols.length && !customCols.length) { setErr("Pick at least one column to export."); return; }
    const title = sheet.title.trim() || "Sheet";
    setErr(null);
    if (kind === "csv") {
      const cols = exportColumns();
      sbDownloadCsv(`${sbSlug(title)}.csv`, [
        cols.map((c) => c.header),
        ...exportRows().map((row) => row.map((v) => (v === true ? "Yes" : v === false ? "" : v))),
      ]);
      return;
    }
    setExporting(kind);
    try {
      const { blobUrl, filename } = await window.KXApi.download(`/admin/sheets/export.${kind}`, {
        title, subtitle: describe(), columns: exportColumns(), rows: exportRows(),
      });
      const a = document.createElement("a");
      if (kind === "pdf" && window.open(blobUrl, "_blank")) {
        // Opened in the browser's viewer, where Print is.
      } else {
        a.href = blobUrl; a.download = filename; a.click();
      }
      // Revoked late — Safari drops the blob under a viewer still loading it.
      setTimeout(() => URL.revokeObjectURL(blobUrl), 60000);
    } catch (e) { setErr(String(e.message || e)); }
    finally { setExporting(null); }
  };

  if (!sheet) {
    return (
      <div className="cx-page">
        <style>{SB_CSS}</style>
        <CX.PageHead eyebrow="Operations · Sheet Builder" title="Loading…" onBack={onBack}/>
        {err ? <div className="sb-err">{err}</div> : <CX.Card><CX.Skeleton rows={1} height={120}/></CX.Card>}
      </div>
    );
  }

  const yearSections = sections.filter((c) => c.academic_year === year);
  const unusedFields = fieldsCatalog.filter((f) => !sheet.fields.includes(f.key));
  const unusedPresets = SB_PRESETS.filter((p) => !sheet.custom_columns.some((c) => c.label.trim().toLowerCase() === p.label.toLowerCase()));
  const statusText = { new: "Not saved yet", saving: "Saving…", saved: "All changes saved", error: "Not saved" }[status];

  return (
    <div className="cx-page">
      <style>{SB_CSS}</style>
      <CX.PageHead
        eyebrow="Operations · Sheet Builder"
        title={sheet.title.trim() || "New sheet"}
        onBack={onBack}
        actions={
          <>
            <span className="sb-status" style={{ color: status === "error" ? "var(--red)" : undefined }}>{statusText}</span>
            {!sheet.id && (
              <button className="cx-btn-dark" onClick={saveNew} disabled={status === "saving"}>Save sheet</button>
            )}
            <button className="btn" onClick={() => doExport("csv")} disabled={!roster}>CSV</button>
            <button className="btn" onClick={() => doExport("xlsx")} disabled={!roster || !!exporting}>
              <CX.Icon name="download" size={14}/> {exporting === "xlsx" ? "Building…" : "Excel"}
            </button>
            <button className="btn" onClick={() => doExport("pdf")} disabled={!roster || !!exporting}>
              <CX.Icon name="file" size={14}/> {exporting === "pdf" ? "Building…" : "PDF"}
            </button>
          </>
        }
      />

      {err && <div className="sb-err">{err}</div>}

      {/* ── Setup ── */}
      <CX.Card style={{ marginBottom: 18 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", marginBottom: showSetup ? 18 : 0 }}>
          <input className="input" style={{ flex: "1 1 260px", fontSize: 15, fontWeight: 600 }}
                 placeholder="Sheet name — e.g. Class X marksheet collection"
                 value={sheet.title} maxLength={160}
                 onChange={(e) => patch(() => ({ title: e.target.value }))}/>
          <button className="btn" onClick={() => setShowSetup((v) => !v)}>
            {showSetup ? "Hide columns & classes" : "Edit columns & classes"}
          </button>
        </div>

        {showSetup && (
          <div className="sb-setup">
            {/* 1. Students */}
            <div>
              <div className="sb-step-label">1 · Students</div>
              <div style={{ display: "flex", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
                {years.length > 1 && (
                  <select className="select input" style={{ width: "auto" }} value={year} onChange={(e) => setYear(e.target.value)}>
                    {years.map((y) => <option key={y} value={y}>{y}</option>)}
                  </select>
                )}
                <CX.Seg value={sheet.student_type} onChange={(v) => patch(() => ({ student_type: v }))}
                        options={[{ value: "", label: "All" }, { value: "boarder", label: "Boarders" }, { value: "day_scholar", label: "Day" }]}/>
              </div>
              <div className="sb-chips">
                <button className={`sb-chip${sheet.class_section_ids.length === 0 ? " on" : ""}`}
                        onClick={() => patch(() => ({ class_section_ids: [] }))}>Whole school</button>
                {yearSections.map((c) => (
                  <button key={c.id} className={`sb-chip${sheet.class_section_ids.includes(c.id) ? " on" : ""}`}
                          onClick={() => toggleSection(c.id)}>
                    {c.label}{c.student_count != null ? <span style={{ opacity: .6 }}> · {c.student_count}</span> : null}
                  </button>
                ))}
              </div>
            </div>

            {/* 2. Roster fields */}
            <div>
              <div className="sb-step-label">2 · From the roster</div>
              <div className="sb-list">
                {sheet.fields.map((k, i) => (
                  <div key={k} className="sb-item">
                    <span className="sb-item-label">{fieldByKey[k]?.label || k}</span>
                    <button className="sb-ibtn" disabled={i === 0} onClick={() => moveIn("fields", i, -1)} aria-label="Move up">↑</button>
                    <button className="sb-ibtn" disabled={i === sheet.fields.length - 1} onClick={() => moveIn("fields", i, 1)} aria-label="Move down">↓</button>
                    <button className="sb-ibtn" onClick={() => toggleField(k)} aria-label="Remove">✕</button>
                  </div>
                ))}
              </div>
              {unusedFields.length > 0 && (
                <div className="sb-chips">
                  {unusedFields.map((f) => (
                    <button key={f.key} className="sb-chip add" onClick={() => toggleField(f.key)}>+ {f.label}</button>
                  ))}
                </div>
              )}
            </div>

            {/* 3. Hand-entered columns */}
            <div>
              <div className="sb-step-label">3 · Your columns (typed or ticked)</div>
              <div className="sb-list">
                {sheet.custom_columns.map((c, i) => (
                  <div key={c.id} className="sb-item">
                    <input className="input" style={{ flex: 1, minWidth: 0 }} value={c.label} maxLength={80}
                           placeholder="Column name"
                           onChange={(e) => updateColumn(c.id, { label: e.target.value })}/>
                    <select className="select input" value={c.type} onChange={(e) => updateColumn(c.id, { type: e.target.value })}>
                      {SB_COLUMN_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
                    </select>
                    <button className="sb-ibtn" disabled={i === 0} onClick={() => moveIn("custom_columns", i, -1)} aria-label="Move up">↑</button>
                    <button className="sb-ibtn" disabled={i === sheet.custom_columns.length - 1} onClick={() => moveIn("custom_columns", i, 1)} aria-label="Move down">↓</button>
                    <button className="sb-ibtn" onClick={() => removeColumn(c)} aria-label="Remove">✕</button>
                  </div>
                ))}
              </div>
              <form className="sb-item" style={{ background: "transparent", padding: 0, marginBottom: 10 }}
                    onSubmit={(e) => { e.preventDefault(); addColumn(newColLabel, newColType); }}>
                <input className="input" style={{ flex: 1, minWidth: 0 }} placeholder="New column, e.g. Doc submitted"
                       value={newColLabel} maxLength={80} onChange={(e) => setNewColLabel(e.target.value)}/>
                <select className="select input" value={newColType} onChange={(e) => setNewColType(e.target.value)}>
                  {SB_COLUMN_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
                </select>
                <button className="btn sm" type="submit" disabled={!newColLabel.trim()}>Add</button>
              </form>
              {unusedPresets.length > 0 && (
                <div className="sb-chips">
                  {unusedPresets.map((p) => (
                    <button key={p.label} className="sb-chip add" onClick={() => addColumn(p.label, p.type)}>+ {p.label}</button>
                  ))}
                </div>
              )}
            </div>
          </div>
        )}
      </CX.Card>

      {/* ── Table ── */}
      <CX.Card>
        <div className="sb-toolbar">
          <input className="input" style={{ flex: "1 1 220px", maxWidth: 340 }} placeholder="Search name, roll, parent…"
                 value={search} onChange={(e) => setSearch(e.target.value)}/>
          {customCols.length > 0 && (
            <select className="select input" style={{ width: "auto" }} value={onlyMissing} onChange={(e) => setOnlyMissing(e.target.value)}>
              <option value="">All rows</option>
              {customCols.map((c) => <option key={c.id} value={c.id}>Only missing: {c.label}</option>)}
            </select>
          )}
          <span className="sb-status" style={{ marginLeft: "auto" }}>
            {roster ? `${shown.length} of ${ordered.length} students · exports what is shown` : "Loading students…"}
          </span>
        </div>

        {!roster ? <CX.Skeleton rows={1} height={120}/> : ordered.length === 0 ? (
          <CX.Empty icon="users" title="No students here" hint="Pick other classes, or switch Boarders / Day back to All."/>
        ) : (
          <div className="sb-table-wrap">
            <table className="sb-table">
              <thead>
                <tr>
                  {fieldCols.map((f) => <th key={f.key}>{f.label}</th>)}
                  {customCols.map((c) => (
                    <th key={c.id} className={`sb-own${c.type === "check" ? " sb-tick" : ""}`}>
                      {c.label}
                      <span className="sb-count">{filledCount(c.id)}/{ordered.length}</span>
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {shown.map((r, i) => (
                  <tr key={r.id}>
                    {fieldCols.map((f) => (
                      f.key === "sl"
                        ? <td key={f.key} className="sb-num">{i + 1}</td>
                        : <td key={f.key} style={{ whiteSpace: f.key === "home_address" ? "normal" : "nowrap" }}>{r[f.key] || <span style={{ color: "var(--ink-4)" }}>—</span>}</td>
                    ))}
                    {customCols.map((c) => {
                      const v = sheet.cell_values?.[r.id]?.[c.id];
                      return c.type === "check" ? (
                        <td key={c.id} className="sb-own sb-tick">
                          <input type="checkbox" checked={v === true}
                                 aria-label={`${c.label} — ${r.name}`}
                                 onChange={(e) => setCell(r.id, c, e.target.checked)}/>
                        </td>
                      ) : (
                        <td key={c.id} className="sb-own">
                          <SbCellInput type={c.type} value={v} onCommit={(nv) => setCell(r.id, c, nv)}/>
                        </td>
                      );
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </CX.Card>
    </div>
  );
};

/* ---------- Screen ---------- */

const SheetBuilderScreen = ({ setScreen }) => {
  // undefined = the list; null = a new, unsaved sheet; "<uuid>" = a saved one.
  const [openId, setOpenId] = React.useState(undefined);
  // A new sheet's first save hands back an id; the editor keeps running
  // (same component instance) rather than remounting and losing its state.
  const [editorKey, setEditorKey] = React.useState(0);

  if (openId === undefined) {
    return <SbList setScreen={setScreen}
                   onOpen={(id) => { setOpenId(id); setEditorKey((k) => k + 1); }}
                   onNew={() => { setOpenId(null); setEditorKey((k) => k + 1); }}/>;
  }
  return <SbEditor key={editorKey} sheetId={openId}
                   onBack={() => setOpenId(undefined)}
                   onCreated={() => { /* stays open; the editor now holds the id */ }}/>;
};

window.SheetBuilderScreen = SheetBuilderScreen;
