/* ==================================================================
   Guided setup (docs/SELF_SERVE.md, "Guided setup")
   ------------------------------------------------------------------
   Nine steps that build a real school, each saving through the admin
   APIs (class sections, subjects, teachers, period times) or the
   onboarding API (/admin/onboarding/*: profile, students via the
   Ledger, fees in the Ledger, portal links, invites, finish). Every
   step can be filled with sample data, and "Remove sample data"
   deletes exactly those rows.

   Shown while schools.onboarded_at is null (app.jsx opens it once per
   session, the dashboard banner and the sidebar resume it).

   Top-level names are prefixed with setup or Setup: every text/babel
   script shares ONE global scope.
   ================================================================== */

const SETUP_LADDER = ["Nursery", "LKG", "UKG", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
const SETUP_STEPS = [
  { id: "profile",  title: "School profile",     icon: "building",  sub: "Name, logo, board, contact details and the school day." },
  { id: "classes",  title: "Classes & sections", icon: "layers",    sub: "The classes you run this year, and their sections." },
  { id: "subjects", title: "Subjects",           icon: "book",      sub: "What is taught, and which class takes which subject." },
  { id: "staff",    title: "Staff",              icon: "users",     sub: "Your teachers. They sign in with their phone number." },
  { id: "students", title: "Students",           icon: "cap",       sub: "Add a student, import a spreadsheet, or start with sample students." },
  { id: "fees",     title: "Fees",               icon: "rupee",     sub: "Monthly and one-time fees per class, kept in the Ledger." },
  { id: "portals",  title: "Try the portals",    icon: "monitor",   sub: "See the Teacher, Parent and Student apps as your users will." },
  { id: "invite",   title: "Invite the team",    icon: "userPlus",  sub: "Send each teacher a sign-in link on WhatsApp." },
  { id: "finish",   title: "Finish",             icon: "checkCircle", sub: "Close the guided setup. You can reopen it from your account menu." },
];
const SETUP_FILLABLE = new Set(["profile", "classes", "subjects", "staff", "students", "fees"]);

// Styles live with the screen (theme tokens only; see theme.css).
(function setupInjectCss() {
  if (document.getElementById("setup-css")) return;
  const css = `
  .su-wrap{display:grid;grid-template-columns:260px minmax(0,1fr);gap:20px;align-items:start}
  .su-steps{position:sticky;top:12px;display:flex;flex-direction:column;gap:2px;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:8px}
  .su-step{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;border:0;background:transparent;text-align:left;cursor:pointer;color:var(--ink-1);font:inherit;font-size:13px}
  .su-step:hover{background:var(--bg-2)}
  .su-step.on{background:var(--bg-3);font-weight:600;color:var(--ink-0)}
  .su-step .n{width:22px;height:22px;flex-shrink:0;border-radius:999px;display:grid;place-items:center;font-size:11px;font-weight:600;background:var(--bg-3);color:var(--ink-3);border:1px solid var(--line)}
  .su-step.done .n{background:var(--green-solid);color:var(--on-solid);border-color:transparent}
  .su-step .t{flex:1;min-width:0}
  .su-step .s{font-size:11px;color:var(--ink-3);font-weight:400}
  .su-progress{height:4px;border-radius:999px;background:var(--bg-4);overflow:hidden;margin:6px 10px 10px}
  .su-progress i{display:block;height:100%;background:var(--green-solid);border-radius:999px;transition:width .3s}
  .su-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}
  .su-grid .wide{grid-column:1/-1}
  .su-foot{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between;margin-top:18px;padding-top:14px;border-top:1px solid var(--line-soft)}
  .su-foot .l,.su-foot .r{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
  .su-chips{display:flex;flex-wrap:wrap;gap:6px}
  .su-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 6px 4px 10px;border-radius:999px;background:var(--bg-2);border:1px solid var(--line);font-size:12.5px;color:var(--ink-1)}
  .su-chip button{border:0;background:transparent;color:var(--ink-3);cursor:pointer;padding:0 2px;display:grid;place-items:center}
  .su-chip.sample{border-style:dashed}
  .su-tag{font-size:10.5px;font-weight:600;letter-spacing:.3px;text-transform:uppercase;color:var(--amber);background:var(--amber-bg);border-radius:4px;padding:1px 5px}
  .su-matrix{border-collapse:collapse;font-size:12.5px;width:100%}
  .su-matrix th,.su-matrix td{border-bottom:1px solid var(--line-soft);padding:6px 8px;text-align:center;white-space:nowrap}
  .su-matrix th:first-child,.su-matrix td:first-child{text-align:left;position:sticky;left:0;background:var(--surface)}
  .su-matrix thead th{font-weight:600;color:var(--ink-2);font-size:12px}
  .su-portals{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}
  .su-portal{display:flex;flex-direction:column;gap:10px;padding:18px;border:1px solid var(--line);border-radius:12px;background:var(--surface)}
  .su-portal .ic{width:40px;height:40px;border-radius:10px;display:grid;place-items:center;background:var(--bg-3);color:var(--ink-1)}
  .su-portal h3{margin:0;font-size:15px}
  .su-portal p{margin:0;font-size:12.5px;color:var(--ink-3);flex:1}
  .su-logo{width:56px;height:56px;border-radius:12px;border:1px solid var(--line);display:grid;place-items:center;overflow:hidden;background:var(--bg-2);color:var(--ink-3)}
  .su-logo img{width:100%;height:100%;object-fit:contain}
  .su-banner{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:12px 16px;margin-bottom:16px;border-radius:12px;border:1px solid var(--line);background:var(--surface)}
  .su-banner .msg{flex:1;min-width:200px;font-size:13px;color:var(--ink-1)}
  .su-banner .msg strong{color:var(--ink-0)}
  .su-table{width:100%;border-collapse:collapse;font-size:13px}
  .su-table th,.su-table td{padding:7px 8px;border-bottom:1px solid var(--line-soft);text-align:left}
  .su-table th{font-size:12px;color:var(--ink-3);font-weight:500;white-space:nowrap}
  .su-table input.input{min-height:30px;padding:4px 8px;max-width:110px}
  .su-scroll{overflow-x:auto}
  @media (max-width: 900px){
    .su-wrap{grid-template-columns:1fr}
    .su-steps{position:static;flex-direction:row;overflow-x:auto;padding:6px}
    .su-step{flex-shrink:0}
    .su-step .t .s{display:none}
    .su-progress{display:none}
  }`;
  const el = document.createElement("style");
  el.id = "setup-css";
  el.textContent = css;
  document.head.appendChild(el);
})();

const setupIcon = (name, size = 15) => <window.AppUI.Icon name={name} size={size}/>;
const setupErr = (e) => (e && (e.message || e.error)) || "Something went wrong.";

// multipart upload with the admin session (AppApi only speaks JSON).
async function setupUpload(path, field, file) {
  const fd = new FormData();
  fd.append(field, file);
  const token = window.AppApi.getToken();
  const r = await fetch(window.AppApi.base + path, {
    method: "POST", body: fd, headers: token ? { authorization: `Bearer ${token}` } : {},
  });
  const data = await r.json().catch(() => ({}));
  if (!r.ok) throw new Error(data.message || data.error || `Upload failed (${r.status})`);
  return data;
}

// Bell times for a day from start/end and a period count: equal periods with
// a 15-minute break after the third and a 30-minute lunch after the sixth.
function setupBell(start, end, n) {
  const toMin = (s) => { const [h, m] = s.split(":").map(Number); return h * 60 + m; };
  const hm = (m) => `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
  const a = toMin(start) + 10, b = toMin(end) - 15; // assembly first, a short wind-down at the end
  const breaks = (n > 3 ? 15 : 0) + (n > 6 ? 30 : 0);
  const len = Math.max(25, Math.floor((b - a - breaks) / n));
  const out = [];
  let t = a;
  for (let p = 1; p <= n; p++) {
    out.push([hm(t), hm(t + len)]);
    t += len;
    if (p === 3 && n > 3) t += 15;
    if (p === 6 && n > 6) t += 30;
  }
  return out;
}

/* ---------------- shared step chrome ---------------- */

const SetupStepFoot = ({ step, busy, onSample, onNext, nextLabel = "Save & continue", nextDisabled, extra }) => (
  <div className="su-foot">
    <div className="l">
      {SETUP_FILLABLE.has(step) && onSample && (
        <button type="button" className="btn" disabled={busy} onClick={onSample} data-test="fill-sample">
          {setupIcon("sparkle")} Fill with sample data
        </button>
      )}
      {extra}
    </div>
    <div className="r">
      {onNext && (
        <button type="button" className="btn primary" disabled={busy || nextDisabled} onClick={onNext} data-test="next">
          {nextLabel} {setupIcon("arrowRight", 14)}
        </button>
      )}
    </div>
  </div>
);

/* ---------------- 1. profile ---------------- */

const SetupProfile = ({ data, busy, run, onSample, next }) => {
  const p = data.profile || {};
  const [f, setF] = useState(() => ({
    name: p.name || "", board: p.board || "", address: p.address || "", phone: p.phone || "",
    email: p.email || "", website: p.website || "", academic_year: p.academic_year || "",
    day_starts_at: p.day_starts_at || "08:00", day_ends_at: p.day_ends_at || "14:00", periods: 7,
  }));
  const [setBell, setSetBell] = useState(!(data.counts && data.counts.bell_periods > 0));
  const [logo, setLogo] = useState(p.logo_url || null);
  useEffect(() => {
    setF((x) => ({ ...x, name: p.name || x.name, board: p.board || x.board, address: p.address || x.address,
      phone: p.phone || x.phone, email: p.email || x.email, website: p.website || x.website,
      day_starts_at: p.day_starts_at || x.day_starts_at, day_ends_at: p.day_ends_at || x.day_ends_at }));
    setLogo(p.logo_url || null);
  }, [data.profile]);
  const set = (k) => (e) => setF((x) => ({ ...x, [k]: e.target.value }));
  const save = () => run(async () => {
    if (setBell) {
      const bell = setupBell(f.day_starts_at, f.day_ends_at, Math.max(1, Math.min(8, Number(f.periods) || 7)));
      const periods = [];
      for (let d = 1; d <= 6; d++) {
        const n = d === 6 ? Math.ceil(bell.length / 2) : bell.length;
        for (let i = 0; i < n; i++) periods.push({ day_of_week: d, period_no: i + 1, starts_at: bell[i][0], ends_at: bell[i][1] });
      }
      await window.AppApi.put("/routine/period-times", { periods, confirm_remove: true });
    }
    // Only fields changed here count as typed by hand (sample fields stay sample).
    const hand_fields = ["board", "address", "phone", "email", "website", "day_starts_at", "day_ends_at"].filter((k) => (f[k] || "") !== (p[k] || ""));
    await window.AppApi.put("/admin/onboarding/profile", { ...f, bell_saved: setBell, hand_fields });
    next();
  });
  const onLogo = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    run(async () => {
      const r = await setupUpload("/admin/onboarding/logo", "logo", file);
      setLogo(r.logo_url);
      if (window.BRAND) window.BRAND.logoUrl = r.logo_url;
    }, { quiet: true });
  };
  return (
    <>
      <div className="su-grid">
        <label className="field wide">
          <span className="label">School name</span>
          <input className="input" value={f.name} onChange={set("name")} maxLength={120} data-test="name"/>
        </label>
        <div className="field wide" style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
          <span className="su-logo">{logo ? <img src={logo} alt="School logo"/> : setupIcon("building", 20)}</span>
          <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
            <span className="label">Logo</span>
            <input type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml" onChange={onLogo} disabled={busy}/>
            <span className="hint">PNG, JPEG, WebP or SVG, up to 2 MB. Shown in every app and on printouts.</span>
          </div>
        </div>
        <label className="field">
          <span className="label">Board</span>
          <select className="select" value={f.board} onChange={set("board")} data-test="board">
            <option value="">Choose…</option>
            {["CBSE", "CISCE (ICSE)", "WBBSE", "State board", "Other"].map((b) => <option key={b} value={b}>{b}</option>)}
          </select>
        </label>
        <label className="field">
          <span className="label">Academic year</span>
          <input className="input" value={f.academic_year} onChange={set("academic_year")} placeholder="2026-27"/>
        </label>
        <label className="field">
          <span className="label">Office phone</span>
          <input className="input" value={f.phone} onChange={set("phone")} placeholder="10-digit number" inputMode="tel" data-test="phone"/>
        </label>
        <label className="field">
          <span className="label">Email</span>
          <input className="input" value={f.email} onChange={set("email")} placeholder="office@yourschool.in" type="email"/>
        </label>
        <label className="field wide">
          <span className="label">Address</span>
          <input className="input" value={f.address} onChange={set("address")} placeholder="Street, town, district, PIN" data-test="address"/>
        </label>
        <label className="field">
          <span className="label">Website</span>
          <input className="input" value={f.website} onChange={set("website")} placeholder="yourschool.in"/>
        </label>
        <label className="field">
          <span className="label">School starts</span>
          <input className="input" type="time" value={f.day_starts_at} onChange={set("day_starts_at")}/>
        </label>
        <label className="field">
          <span className="label">School ends</span>
          <input className="input" type="time" value={f.day_ends_at} onChange={set("day_ends_at")}/>
        </label>
        <label className="field">
          <span className="label">Periods a day</span>
          <input className="input" type="number" min={1} max={8} value={f.periods} onChange={set("periods")}/>
        </label>
        <label className="wide" style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 13, color: "var(--ink-1)" }}>
          <input type="checkbox" checked={setBell} onChange={(e) => setSetBell(e.target.checked)}/>
          Set the bell times from these hours (Monday–Friday; Saturday is half a day)
        </label>
      </div>
      <SetupStepFoot step="profile" busy={busy} onSample={onSample} onNext={save} nextDisabled={!f.name.trim()}/>
    </>
  );
};

/* ---------------- 2. classes ---------------- */

const SetupClasses = ({ data, busy, run, onSample, next, catalog, reloadCatalog }) => {
  const [level, setLevel] = useState("I");
  const [custom, setCustom] = useState("");
  const [secs, setSecs] = useState("A");
  const list = (catalog.classSections || []).filter((c) => !data.profile || c.academic_year === data.profile.academic_year);
  const sample = new Set(catalog.sampleIds?.class_sections || []);
  const add = (levels) => run(async () => {
    const sections = secs.split(/[\s,]+/).map((s) => s.trim().toUpperCase()).filter(Boolean);
    for (const l of levels) for (const s of (sections.length ? sections : ["A"])) {
      if (list.some((c) => c.class_level === l && (c.section || "") === s)) continue;
      await window.AppApi.post("/admin/class-sections", { class_level: l, section: s });
    }
    await reloadCatalog();
  }, { quiet: true });
  const remove = (c) => run(async () => { await window.AppApi.del(`/admin/class-sections/${c.id}`); await reloadCatalog(); }, { quiet: true });
  const presets = [["Nursery–UKG", ["Nursery", "LKG", "UKG"]], ["I–V", ["I", "II", "III", "IV", "V"]], ["VI–VIII", ["VI", "VII", "VIII"]], ["IX–X", ["IX", "X"]], ["XI–XII", ["XI", "XII"]]];
  const sorted = list.slice().sort((a, b) => (SETUP_LADDER.indexOf(a.class_level) + 100 * (SETUP_LADDER.indexOf(a.class_level) < 0)) - (SETUP_LADDER.indexOf(b.class_level) + 100 * (SETUP_LADDER.indexOf(b.class_level) < 0)) || String(a.section).localeCompare(String(b.section)));
  return (
    <>
      <div className="su-grid">
        <label className="field">
          <span className="label">Sections for each class</span>
          <input className="input" value={secs} onChange={(e) => setSecs(e.target.value)} placeholder="A, B" data-test="sections"/>
          <span className="hint">Comma-separated, e.g. A, B</span>
        </label>
        <div className="field">
          <span className="label">Add a range</span>
          <div className="su-chips">
            {presets.map(([label, lv]) => (
              <button key={label} type="button" className="btn sm" disabled={busy} onClick={() => add(lv)} data-test={`preset-${label}`}>{setupIcon("plus", 13)} {label}</button>
            ))}
          </div>
        </div>
        <div className="field">
          <span className="label">Or one class</span>
          <div style={{ display: "flex", gap: 6 }}>
            <select className="select" value={level} onChange={(e) => setLevel(e.target.value)} style={{ maxWidth: 120 }} data-test="class-level">
              {SETUP_LADDER.map((l) => <option key={l} value={l}>{l}</option>)}
              <option value="__custom">Other…</option>
            </select>
            {level === "__custom" && <input className="input" value={custom} onChange={(e) => setCustom(e.target.value)} placeholder="Class name"/>}
            <button type="button" className="btn" disabled={busy || (level === "__custom" && !custom.trim())} onClick={() => add([level === "__custom" ? custom.trim() : level])} data-test="add-class">Add</button>
          </div>
        </div>
      </div>
      <div style={{ marginTop: 18 }}>
        <div className="label" style={{ marginBottom: 8 }}>{sorted.length ? `${sorted.length} class-section${sorted.length === 1 ? "" : "s"} for ${data.profile?.academic_year || "this year"}` : "No classes yet."}</div>
        <div className="su-chips" data-test="class-chips">
          {sorted.map((c) => (
            <span key={c.id} className={`su-chip${sample.has(c.id) ? " sample" : ""}`} title={sample.has(c.id) ? "Sample data" : undefined}>
              {c.label || [c.class_level, c.section].filter(Boolean).join("-")}
              {c.student_count > 0 && <span className="hint">· {c.student_count}</span>}
              <button type="button" aria-label={`Remove ${c.label}`} disabled={busy || c.student_count > 0} onClick={() => remove(c)}>{setupIcon("x", 12)}</button>
            </span>
          ))}
        </div>
      </div>
      <SetupStepFoot step="classes" busy={busy} onSample={onSample} onNext={next} nextLabel="Continue" nextDisabled={!sorted.length}/>
    </>
  );
};

/* ---------------- 3. subjects ---------------- */

const SETUP_COMMON_SUBJECTS = [["English", "ENG"], ["Bengali", "BEN"], ["Hindi", "HIN"], ["Mathematics", "MATH"], ["Environmental Studies", "EVS"], ["Science", "SCI"], ["Social Science", "SST"], ["Computer", "COMP"], ["Drawing", "ART"]];

const SetupSubjects = ({ busy, run, onSample, next, catalog, reloadCatalog }) => {
  const [name, setName] = useState("");
  const [code, setCode] = useState("");
  const [map, setMap] = useState(null);
  const subjects = catalog.subjects || [];
  const levels = [...new Set((catalog.classSections || []).map((c) => c.class_level))]
    .sort((a, b) => (SETUP_LADDER.indexOf(a) + 100 * (SETUP_LADDER.indexOf(a) < 0)) - (SETUP_LADDER.indexOf(b) + 100 * (SETUP_LADDER.indexOf(b) < 0)));
  const loadMap = async () => {
    const r = await window.AppApi.get("/admin/onboarding/class-subjects");
    setMap(Object.fromEntries((r.classes || []).map((c) => [c.class_level, new Set(c.subject_ids)])));
  };
  useEffect(() => { loadMap().catch(() => setMap({})); }, [catalog]);
  const add = (n, c) => run(async () => {
    await window.AppApi.post("/admin/subjects", { name: n, short_code: c || undefined });
    setName(""); setCode("");
    await reloadCatalog();
  }, { quiet: true });
  const toggle = (level, sid) => run(async () => {
    const cur = new Set(map[level] || []);
    if (cur.has(sid)) cur.delete(sid); else cur.add(sid);
    setMap((m) => ({ ...m, [level]: cur }));
    await window.AppApi.put("/admin/onboarding/class-subjects", { class_level: level, subject_ids: [...cur] });
  }, { quiet: true });
  const missing = SETUP_COMMON_SUBJECTS.filter(([n]) => !subjects.some((s) => s.name.toLowerCase() === n.toLowerCase()));
  return (
    <>
      <div className="su-grid">
        <div className="field wide">
          <span className="label">Common subjects</span>
          <div className="su-chips">
            {missing.map(([n, c]) => <button key={n} type="button" className="btn sm" disabled={busy} onClick={() => add(n, c)}>{setupIcon("plus", 13)} {n}</button>)}
            {!missing.length && <span className="hint">All added.</span>}
          </div>
        </div>
        <label className="field">
          <span className="label">Another subject</span>
          <input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Sanskrit" data-test="subject-name"/>
        </label>
        <label className="field">
          <span className="label">Short code (optional)</span>
          <div style={{ display: "flex", gap: 6 }}>
            <input className="input" value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} placeholder="SKT" maxLength={8}/>
            <button type="button" className="btn" disabled={busy || !name.trim()} onClick={() => add(name.trim(), code.trim())} data-test="add-subject">Add</button>
          </div>
        </label>
      </div>
      {subjects.length > 0 && levels.length > 0 && (
        <div style={{ marginTop: 18 }}>
          <div className="label" style={{ marginBottom: 8 }}>Which class takes which subject</div>
          <div className="su-scroll">
            <table className="su-matrix">
              <thead><tr><th>Class</th>{subjects.map((s) => <th key={s.id} title={s.name}>{s.short_code || s.name}</th>)}</tr></thead>
              <tbody>
                {levels.map((l) => (
                  <tr key={l}>
                    <td>{l}</td>
                    {subjects.map((s) => (
                      <td key={s.id}>
                        <input type="checkbox" aria-label={`${l} takes ${s.name}`} disabled={busy || !map}
                          checked={!!(map && map[l] && map[l].has(s.id))} onChange={() => toggle(l, s.id)}/>
                      </td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}
      <SetupStepFoot step="subjects" busy={busy} onSample={onSample} onNext={next} nextLabel="Continue" nextDisabled={!subjects.length}/>
    </>
  );
};

/* ---------------- 4. staff ---------------- */

const SetupStaff = ({ data, busy, run, onSample, next, catalog, reloadCatalog }) => {
  const [f, setF] = useState({ name: "", phone: "", email: "", subject_id: "" });
  const [teachers, setTeachers] = useState(null);
  const [flash, setFlash] = useState(null);
  const load = async () => setTeachers((await window.AppApi.get("/admin/teachers")).filter((t) => t.role !== "support_staff"));
  useEffect(() => { load().catch(() => setTeachers([])); }, [catalog]);
  const add = () => run(async () => {
    const digits = f.phone.replace(/\D/g, "").slice(-10);
    const email = f.email.trim() || (digits ? `${digits}@${data.slug}.staff.invalid` : `${f.name.toLowerCase().replace(/[^a-z]+/g, ".")}.${Date.now() % 100000}@${data.slug}.staff.invalid`);
    const t = await window.AppApi.post("/admin/teachers", { name: f.name.trim(), email, role: "teacher" });
    if (digits) await window.AppApi.patch(`/admin/teachers/${t.id}`, { phone: f.phone });
    if (f.subject_id) {
      const map = (await window.AppApi.get("/admin/onboarding/class-subjects")).classes || [];
      const takes = new Set(map.filter((c) => (c.subject_ids || []).includes(f.subject_id)).map((c) => c.class_level));
      const secs = (catalog.classSections || []).filter((c) => !map.length || takes.has(c.class_level));
      await window.AppApi.put(`/admin/teachers/${t.id}/assignments`, { assignments: secs.map((c) => ({ class_section_id: c.id, subject_id: f.subject_id })) });
    }
    setFlash({ name: t.name, username: t.username, password: t.temp_password });
    setF({ name: "", phone: "", email: "", subject_id: "" });
    await load();
  }, { quiet: true });
  const staff = teachers || [];
  return (
    <>
      <div className="su-grid">
        <label className="field">
          <span className="label">Name</span>
          <input className="input" value={f.name} onChange={(e) => setF({ ...f, name: e.target.value })} placeholder="Ritu Sharma" data-test="teacher-name"/>
        </label>
        <label className="field">
          <span className="label">Mobile</span>
          <input className="input" value={f.phone} onChange={(e) => setF({ ...f, phone: e.target.value })} placeholder="10-digit number" inputMode="tel" data-test="teacher-phone"/>
        </label>
        <label className="field">
          <span className="label">Teaches</span>
          <select className="select" value={f.subject_id} onChange={(e) => setF({ ...f, subject_id: e.target.value })} data-test="teacher-subject">
            <option value="">Any / later</option>
            {(catalog.subjects || []).map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
          </select>
        </label>
        <label className="field">
          <span className="label">Email (optional)</span>
          <div style={{ display: "flex", gap: 6 }}>
            <input className="input" value={f.email} onChange={(e) => setF({ ...f, email: e.target.value })} placeholder="name@school.in"/>
            <button type="button" className="btn" disabled={busy || !f.name.trim()} onClick={add} data-test="add-teacher">Add</button>
          </div>
        </label>
      </div>
      {flash && (
        <div className="ui-alert green" style={{ marginTop: 12 }}>
          {flash.name} added. Username <strong>{flash.username}</strong>, temporary password <strong>{flash.password}</strong>. They can also sign in with their phone (step 8 sends a link).
        </div>
      )}
      <div style={{ marginTop: 18 }}>
        <div className="label" style={{ marginBottom: 8 }}>{staff.length ? `${staff.length} staff` : "No teachers yet."}</div>
        {staff.length > 0 && (
          <div className="su-scroll"><table className="su-table">
            <thead><tr><th>Name</th><th>Role</th><th>Mobile</th><th>Classes</th></tr></thead>
            <tbody>{staff.slice(0, 40).map((t) => (
              <tr key={t.id}><td>{t.name}</td><td className="muted">{t.role}</td><td className="mono">{t.phone || "—"}</td><td>{t.assignment_count}</td></tr>
            ))}</tbody>
          </table></div>
        )}
      </div>
      <SetupStepFoot step="staff" busy={busy} onSample={onSample} onNext={next} nextLabel="Continue" nextDisabled={!staff.length}/>
    </>
  );
};

/* ---------------- 5. students ---------------- */

const SetupStudents = ({ data, busy, run, onSample, next, catalog, reloadCatalog }) => {
  const secs = (catalog.classSections || []).filter((c) => !data.profile || c.academic_year === data.profile.academic_year);
  const [f, setF] = useState({ name: "", class_section_id: "", guardian_name: "", guardian_phone: "", student_type: "DAY_SCHOLAR", avails_transport: false });
  const [result, setResult] = useState(null);
  const [students, setStudents] = useState(null);
  const load = async () => setStudents(await window.AppApi.get("/admin/students"));
  useEffect(() => { load().catch(() => setStudents([])); }, [catalog]);
  const add = () => run(async () => {
    const r = await window.AppApi.post("/admin/onboarding/students", { students: [f] });
    setResult(r);
    if (r.admitted) setF({ ...f, name: "", guardian_name: "", guardian_phone: "" });
    await reloadCatalog(); await load();
  }, { quiet: true });
  const onFile = (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file) return;
    run(async () => { setResult(await setupUpload("/admin/onboarding/students/import", "file", file)); await reloadCatalog(); await load(); }, { quiet: true });
  };
  const template = () => run(async () => {
    const d = await window.AppApi.download("/admin/onboarding/students/template");
    const a = document.createElement("a"); a.href = d.blobUrl; a.download = d.filename; a.click();
  }, { quiet: true });
  const list = students || [];
  return (
    <>
      {!secs.length && <div className="ui-alert amber" style={{ marginBottom: 12 }}>Add classes first (step 2), or fill this step with sample data and the classes come with it.</div>}
      <div className="su-grid">
        <label className="field">
          <span className="label">Student name</span>
          <input className="input" value={f.name} onChange={(e) => setF({ ...f, name: e.target.value })} placeholder="Ananya Ghosh" data-test="student-name"/>
        </label>
        <label className="field">
          <span className="label">Class</span>
          <select className="select" value={f.class_section_id} onChange={(e) => setF({ ...f, class_section_id: e.target.value })} data-test="student-class">
            <option value="">Choose…</option>
            {secs.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}
          </select>
        </label>
        <label className="field">
          <span className="label">Guardian name</span>
          <input className="input" value={f.guardian_name} onChange={(e) => setF({ ...f, guardian_name: e.target.value })} data-test="guardian-name"/>
        </label>
        <label className="field">
          <span className="label">Guardian mobile</span>
          <input className="input" value={f.guardian_phone} onChange={(e) => setF({ ...f, guardian_phone: e.target.value })} inputMode="tel" placeholder="10-digit number" data-test="guardian-phone"/>
        </label>
        <label className="field">
          <span className="label">Type</span>
          <select className="select" value={f.student_type} onChange={(e) => setF({ ...f, student_type: e.target.value })}>
            <option value="DAY_SCHOLAR">Day scholar</option><option value="BOARDER">Boarder</option>
          </select>
        </label>
        <label className="field" style={{ justifyContent: "flex-end" }}>
          <span style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 13, minHeight: 34 }}>
            <input type="checkbox" checked={f.avails_transport} onChange={(e) => setF({ ...f, avails_transport: e.target.checked })}/> Takes the school bus
          </span>
        </label>
        <div className="field wide" style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
          <button type="button" className="btn primary" disabled={busy || !f.name.trim() || !f.class_section_id} onClick={add} data-test="add-student">{setupIcon("userPlus")} Add student</button>
          <span className="hint" style={{ margin: "0 6px" }}>or</span>
          <label className="btn" style={{ cursor: "pointer" }}>{setupIcon("upload")} Import .xlsx / .csv
            <input type="file" accept=".xlsx,.csv" onChange={onFile} style={{ display: "none" }} data-test="import-file"/>
          </label>
          <button type="button" className="btn ghost" disabled={busy} onClick={template}>{setupIcon("download")} Template</button>
        </div>
      </div>
      <p className="hint" style={{ marginTop: 10 }}>Students are admitted in the Ledger (where their fees live) and appear here straight away.</p>
      {result && (
        <div className={`ui-alert ${result.errors && result.errors.length ? "amber" : "green"}`} style={{ marginTop: 10, display: "block" }}>
          {result.admitted ? `${result.admitted} student${result.admitted === 1 ? "" : "s"} admitted.` : "No students admitted."}
          {(result.errors || []).slice(0, 8).map((e, i) => <div key={i}>{e.row ? `Row ${e.row}: ` : ""}{e.error}</div>)}
        </div>
      )}
      <div style={{ marginTop: 18 }}>
        <div className="label" style={{ marginBottom: 8 }}>{list.length ? `${list.length} students` : "No students yet."}</div>
        {list.length > 0 && (
          <div className="su-scroll"><table className="su-table">
            <thead><tr><th>Name</th><th>Class</th><th>Roll</th><th>Ledger ID</th></tr></thead>
            <tbody>{list.slice(0, 12).map((s) => (
              <tr key={s.id}><td>{s.name}</td><td>{s.class_label}</td><td className="mono">{s.roll_no}</td><td className="mono muted">{s.ledger_student_uid || "—"}</td></tr>
            ))}</tbody>
          </table></div>
        )}
      </div>
      <SetupStepFoot step="students" busy={busy} onSample={onSample} onNext={next} nextLabel="Continue"/>
    </>
  );
};

/* ---------------- 6. fees ---------------- */

const SETUP_FEE_COLS = [
  ["monthly_tuition", "Tuition / month"], ["monthly_transport", "Bus / month"], ["monthly_hostel", "Hostel / month"],
  ["admission_fee", "Admission"], ["readmission_fee", "Re-admission"], ["security_deposit", "Security deposit"],
];

const SetupFees = ({ data, busy, run, onSample, next, catalog }) => {
  const [fees, setFees] = useState(null);
  const [rows, setRows] = useState({});
  const [err, setErr] = useState(null);
  const [saved, setSaved] = useState(false);
  const load = async () => {
    try {
      const r = await window.AppApi.get("/admin/onboarding/fees");
      setFees(r);
      const byClass = Object.fromEntries((r.classes || []).map((c) => [c.class_name, c]));
      const levels = [...new Set([...(r.class_levels || []), ...Object.keys(byClass)])]
        .sort((a, b) => (SETUP_LADDER.indexOf(a) + 100 * (SETUP_LADDER.indexOf(a) < 0)) - (SETUP_LADDER.indexOf(b) + 100 * (SETUP_LADDER.indexOf(b) < 0)));
      setRows(Object.fromEntries(levels.map((l) => [l, { ...(byClass[l] || { class_name: l }) }])));
      setErr(null);
    } catch (e) { setErr(setupErr(e)); }
  };
  useEffect(() => { load(); }, [catalog]);
  const [dirty, setDirty] = useState({});
  const set = (l, k) => (e) => { setSaved(false); setDirty((d) => ({ ...d, [l]: true })); setRows((r) => ({ ...r, [l]: { ...r[l], [k]: e.target.value } })); };
  const copyDown = (k) => {
    const first = Object.values(rows).find((r) => r[k] !== undefined && r[k] !== "");
    if (!first) return;
    setRows((r) => Object.fromEntries(Object.entries(r).map(([l, v]) => {
      if (v[k] !== undefined && v[k] !== "") return [l, v];
      setDirty((d) => ({ ...d, [l]: true }));
      return [l, { ...v, [k]: first[k] }];
    })));
  };
  const save = () => run(async () => {
    // Only rows edited here are saved (and so stop being sample data).
    const classes = Object.values(rows).filter((r) => dirty[r.class_name] && SETUP_FEE_COLS.some(([k]) => r[k] !== undefined && r[k] !== ""))
      .map((r) => ({ class_name: r.class_name, ...Object.fromEntries(SETUP_FEE_COLS.map(([k]) => [k, Number(r[k]) || 0])) }));
    const any = Object.values(rows).some((r) => r.id);
    if (!classes.length && !any) throw new Error("Enter at least one amount.");
    if (classes.length) await window.AppApi.put("/admin/onboarding/fees", { classes });
    setDirty({});
    setSaved(true);
    await load();
    next();
  });
  const levels = Object.keys(rows);
  const ledgerUrl = data.apps && data.apps.ledger;
  return (
    <>
      {err && <div className="ui-alert red" style={{ marginBottom: 12 }}>{err}</div>}
      {fees && fees.session && <p className="hint" style={{ marginTop: 0 }}>Session {fees.session.academic_year} in the Ledger. Monthly fees run January to December.</p>}
      {!levels.length && !err && <div className="ui-alert amber">Add classes first (step 2).</div>}
      {levels.length > 0 && (
        <div className="su-scroll"><table className="su-table" data-test="fee-table">
          <thead><tr><th>Class</th>{SETUP_FEE_COLS.map(([k, label]) => (
            <th key={k}>{label}<button type="button" className="btn xs ghost" title="Copy the first amount down" onClick={() => copyDown(k)}>{setupIcon("chevronDown", 12)}</button></th>
          ))}</tr></thead>
          <tbody>{levels.map((l) => (
            <tr key={l}>
              <td>{l} {rows[l].sample && <span className="su-tag">sample</span>}</td>
              {SETUP_FEE_COLS.map(([k]) => (
                <td key={k}><input className="input" inputMode="numeric" value={rows[l][k] ?? ""} onChange={set(l, k)} placeholder="₹" data-test={`fee-${l}-${k}`}/></td>
              ))}
            </tr>
          ))}</tbody>
        </table></div>
      )}
      {saved && <div className="ui-alert green" style={{ marginTop: 10 }}>Fees saved in the Ledger.</div>}
      <SetupStepFoot step="fees" busy={busy} onSample={onSample} onNext={save} nextDisabled={!levels.length}
        extra={ledgerUrl && <a className="btn ghost" href={ledgerUrl} target="_blank" rel="noopener">{setupIcon("arrowUpRight")} Open Ledger</a>}/>
    </>
  );
};

/* ---------------- 7. try the portals ---------------- */

const SetupPortals = ({ busy, run, next }) => {
  const [as, setAs] = useState({});
  const open = (app) => {
    // Open the tab synchronously (popup blockers), then point it at the link.
    const w = window.open("about:blank", "_blank");
    run(async () => {
      const r = await window.AppApi.post(`/admin/onboarding/try/${app}`, {});
      setAs((x) => ({ ...x, [app]: r.as }));
      if (w) w.location.href = r.url; else window.open(r.url, "_blank");
    }, { quiet: true, onError: () => { if (w) w.close(); } });
  };
  const cards = [
    ["teacher", "Teacher app", "briefcase", "Today's routine, attendance, marks entry and parent messages."],
    ["parent", "Parent app", "users", "A family's view: attendance, results, fees and school notices."],
    ["student", "Student app", "cap", "Online tests, results and live classes."],
  ];
  return (
    <>
      <div className="su-portals">
        {cards.map(([app, title, icon, sub]) => (
          <div key={app} className="su-portal">
            <span className="ic">{setupIcon(icon, 20)}</span>
            <h3>{title}</h3>
            <p>{sub}{as[app] && <><br/><span style={{ color: "var(--ink-1)" }}>Signed in as {as[app]}.</span></>}</p>
            <button type="button" className="btn primary" disabled={busy} onClick={() => open(app)} data-test={`try-${app}`}>
              {setupIcon("arrowUpRight")} Open as a sample {app}
            </button>
          </div>
        ))}
      </div>
      <p className="hint" style={{ marginTop: 12 }}>Each button signs a sample user in, once, in a new tab. No password is shared and nothing is sent to anyone.</p>
      <SetupStepFoot step="portals" busy={busy} onNext={next} nextLabel="Continue"/>
    </>
  );
};

/* ---------------- 8. invite ---------------- */

const SetupInvite = ({ data, busy, run, next, catalog }) => {
  const [teachers, setTeachers] = useState(null);
  const [pick, setPick] = useState({});
  const [invites, setInvites] = useState(null);
  const [nf, setNf] = useState({ name: "", phone: "" });
  const load = async () => {
    const t = (await window.AppApi.get("/admin/teachers")).filter((x) => ["teacher", "hod", "coordinator"].includes(x.role) && x.status === "active" && x.active);
    setTeachers(t);
  };
  useEffect(() => { load().catch(() => setTeachers([])); }, [catalog]);
  const addByPhone = () => run(async () => {
    const digits = nf.phone.replace(/\D/g, "").slice(-10);
    if (digits.length !== 10) throw new Error("Enter a 10-digit mobile number.");
    const t = await window.AppApi.post("/admin/teachers", { name: nf.name.trim(), email: `${digits}@${data.slug}.staff.invalid`, role: "teacher" });
    await window.AppApi.patch(`/admin/teachers/${t.id}`, { phone: nf.phone });
    setNf({ name: "", phone: "" });
    setPick((p) => ({ ...p, [t.id]: true }));
    await load();
  }, { quiet: true });
  const send = () => run(async () => {
    const ids = Object.keys(pick).filter((k) => pick[k]);
    const r = await window.AppApi.post("/admin/onboarding/invite", { teacher_ids: ids });
    setInvites(r.invites);
  }, { quiet: true });
  const list = teachers || [];
  const chosen = Object.values(pick).filter(Boolean).length;
  return (
    <>
      <div className="su-grid">
        <label className="field">
          <span className="label">Teacher's name</span>
          <input className="input" value={nf.name} onChange={(e) => setNf({ ...nf, name: e.target.value })} data-test="invite-name"/>
        </label>
        <label className="field">
          <span className="label">Mobile</span>
          <div style={{ display: "flex", gap: 6 }}>
            <input className="input" value={nf.phone} onChange={(e) => setNf({ ...nf, phone: e.target.value })} inputMode="tel" placeholder="10-digit number" data-test="invite-phone"/>
            <button type="button" className="btn" disabled={busy || !nf.name.trim() || !nf.phone.trim()} onClick={addByPhone} data-test="invite-add">Add</button>
          </div>
        </label>
      </div>
      <div style={{ marginTop: 16 }}>
        <div className="label" style={{ marginBottom: 8, display: "flex", gap: 10, alignItems: "center" }}>
          Choose who to invite
          {list.length > 0 && <button type="button" className="btn xs ghost" onClick={() => setPick(Object.fromEntries(list.map((t) => [t.id, true])))}>Select all</button>}
        </div>
        <div className="su-scroll"><table className="su-table">
          <tbody>{list.map((t) => (
            <tr key={t.id}>
              <td style={{ width: 28 }}><input type="checkbox" checked={!!pick[t.id]} onChange={(e) => setPick({ ...pick, [t.id]: e.target.checked })} aria-label={`Invite ${t.name}`}/></td>
              <td>{t.name}</td><td className="mono muted">{t.phone || "no mobile"}</td><td className="muted">{t.role}</td>
            </tr>
          ))}</tbody>
        </table></div>
        <div style={{ marginTop: 10 }}>
          <button type="button" className="btn primary" disabled={busy || !chosen} onClick={send} data-test="invite-send">{setupIcon("message")} Create sign-in links ({chosen})</button>
        </div>
      </div>
      {invites && (
        <div style={{ marginTop: 16 }}>
          <div className="ui-alert blue" style={{ marginBottom: 10 }}>Send each link from your own WhatsApp. Links work once, for 60 minutes; after that teachers sign in with their mobile number.</div>
          <table className="su-table"><tbody>{invites.map((i) => (
            <tr key={i.id}>
              <td>{i.name}</td>
              <td>{i.error ? <span className="muted">{i.error}</span> : (
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                  {i.whatsapp_url && <a className="btn sm" href={i.whatsapp_url} target="_blank" rel="noopener">{setupIcon("message", 13)} WhatsApp</a>}
                  <button type="button" className="btn sm ghost" onClick={() => navigator.clipboard && navigator.clipboard.writeText(i.message)}>{setupIcon("copy", 13)} Copy message</button>
                </div>
              )}</td>
            </tr>
          ))}</tbody></table>
        </div>
      )}
      <SetupStepFoot step="invite" busy={busy} onNext={next} nextLabel="Continue"/>
    </>
  );
};

/* ---------------- 9. finish ---------------- */

const SetupFinish = ({ data, busy, run, onDone }) => {
  const steps = data.steps || [];
  const finish = () => run(async () => { await window.AppApi.post("/admin/onboarding/finish", {}); await onDone(); });
  return (
    <>
      <div className="su-chips" style={{ flexDirection: "column", alignItems: "stretch", gap: 6 }}>
        {SETUP_STEPS.filter((s) => s.id !== "finish").map((s) => {
          const st = steps.find((x) => x.id === s.id) || {};
          return (
            <div key={s.id} style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 13 }}>
              <span style={{ color: st.done ? "var(--green)" : "var(--ink-4)" }}>{setupIcon(st.done ? "checkCircle" : "dashedCircle", 16)}</span>
              {s.title}{st.count != null && st.count > 0 && <span className="hint">· {st.count}</span>}
              {st.sample && <span className="su-tag">sample</span>}
            </div>
          );
        })}
      </div>
      {data.sample_rows > 0 && <p className="hint" style={{ marginTop: 12 }}>Sample data is still in. Remove it any time with "Remove sample data" at the top of this page.</p>}
      <SetupStepFoot step="finish" busy={busy} onNext={finish} nextLabel="Finish setup"/>
    </>
  );
};

/* ---------------- the screen ---------------- */

const SetupScreen = ({ setScreen }) => {
  const Api = window.AppApi;
  const [data, setData] = useState(window.APP.ONBOARDING || null);
  const [catalog, setCatalog] = useState({});
  const [active, setActive] = useState(() => {
    const d = window.APP.ONBOARDING;
    const first = d && (d.steps || []).find((s) => !s.done);
    return (first && first.id) || "profile";
  });
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null);
  const [confirmRemove, setConfirmRemove] = useState(false);

  const reload = async () => {
    const d = await Api.get("/admin/onboarding");
    window.APP.ONBOARDING = d;
    setData(d);
    if (d.profile && window.APP.CURRENT_SCHOOL) window.APP.CURRENT_SCHOOL = { ...window.APP.CURRENT_SCHOOL, name: d.profile.name };
    return d;
  };
  const reloadCatalog = async () => {
    const [classSections, subjects] = await Promise.all([Api.get("/admin/class-sections").catch(() => []), Api.get("/admin/subjects").catch(() => [])]);
    window.APP.CLASS_SECTIONS = classSections; window.APP.SUBJECTS = subjects;
    setCatalog({ classSections, subjects });
    await reload().catch(() => {});
  };
  useEffect(() => { reloadCatalog(); }, []);

  // Runs an action with the busy flag; refreshes status; shows errors.
  const run = async (fn, opts = {}) => {
    setBusy(true); if (!opts.quiet) setMsg(null);
    try { await fn(); }
    catch (e) { setMsg({ tone: "red", text: setupErr(e) }); if (opts.onError) opts.onError(e); }
    finally { setBusy(false); reload().catch(() => {}); }
  };
  const go = (id) => { setActive(id); setMsg(null); window.scrollTo({ top: 0, behavior: "instant" }); };
  const nextOf = (id) => { const i = SETUP_STEPS.findIndex((s) => s.id === id); return SETUP_STEPS[Math.min(SETUP_STEPS.length - 1, i + 1)].id; };
  const next = () => go(nextOf(active));
  const fill = (step) => run(async () => {
    const r = await Api.post(`/admin/onboarding/sample/${step}`, {});
    const results = r.results || [r];
    const made = results.flatMap((x) => Object.entries(x.created || {})).filter(([, n]) => n > 0)
      .map(([k, n]) => `${n} ${k.replace(/_/g, " ")}`);
    const notes = results.flatMap((x) => x.notes || []);
    setMsg({ tone: "green", text: made.length ? `Sample data added: ${made.slice(0, 8).join(", ")}${made.length > 8 ? "…" : ""}.` : "Nothing to add: this step already has data.", notes });
    await reloadCatalog();
  });
  const removeSample = () => run(async () => {
    const r = await Api.post("/admin/onboarding/sample-remove", {});
    setConfirmRemove(false);
    const n = Object.values(r.removed || {}).reduce((a, b) => a + b, 0);
    setMsg({ tone: "green", text: `Sample data removed (${n} rows here, ${(r.ledger && r.ledger.students) || 0} students and ${(r.ledger && r.ledger.entries) || 0} fee entries in the Ledger).`, notes: (r.kept || []).map((k) => `Kept: ${k}`) });
    await reloadCatalog();
  });

  if (data && data.applicable === false) {
    return <div className="cx-page"><window.AppUI.EmptyState card icon="checkCircle" title="No guided setup here" hint="This school was set up before self-serve; everything is in the usual screens."/></div>;
  }
  const steps = (data && data.steps) || [];
  const doneOf = (id) => !!(steps.find((s) => s.id === id) || {}).done;
  const doneCount = SETUP_STEPS.filter((s) => doneOf(s.id)).length;
  const step = SETUP_STEPS.find((s) => s.id === active) || SETUP_STEPS[0];
  const props = { data: data || {}, busy, run, next, catalog, reloadCatalog, onSample: () => fill(active) };
  const Body = { profile: SetupProfile, classes: SetupClasses, subjects: SetupSubjects, staff: SetupStaff, students: SetupStudents,
    fees: SetupFees, portals: SetupPortals, invite: SetupInvite }[active];

  return (
    <div className="cx-page" data-test="setup">
      <window.AppUI.PageHeader
        eyebrow="Guided setup"
        title={`Set up ${(data && data.profile && data.profile.name) || "your school"}`}
        meta={[`${doneCount} of ${SETUP_STEPS.length} steps done`, data && data.kind === "prospect_demo" ? "Prospect demo" : null]}
        actions={<>
          <button type="button" className="btn" disabled={busy} onClick={() => fill("all")} data-test="fill-all">{setupIcon("sparkle")} Fill everything with sample data</button>
          {data && data.sample_rows > 0 && (
            <button type="button" className="btn danger" disabled={busy} onClick={() => setConfirmRemove(true)} data-test="remove-sample">{setupIcon("trash")} Remove sample data</button>
          )}
          <button type="button" className="btn ghost" onClick={() => setScreen("dashboard")}>Later</button>
        </>}
      />
      <div className="su-wrap">
        <nav className="su-steps" aria-label="Setup steps">
          <div className="su-progress" aria-hidden="true"><i style={{ width: `${Math.round((doneCount / SETUP_STEPS.length) * 100)}%` }}/></div>
          {SETUP_STEPS.map((s, i) => (
            <button key={s.id} type="button" className={`su-step${s.id === active ? " on" : ""}${doneOf(s.id) ? " done" : ""}`}
              aria-current={s.id === active ? "step" : undefined} onClick={() => go(s.id)} data-test={`step-${s.id}`}>
              <span className="n">{doneOf(s.id) ? setupIcon("check", 12) : i + 1}</span>
              <span className="t">{s.title}{(steps.find((x) => x.id === s.id) || {}).sample && <span className="su-tag" style={{ marginLeft: 6 }}>sample</span>}</span>
            </button>
          ))}
        </nav>
        <div style={{ minWidth: 0, display: "flex", flexDirection: "column", gap: 12 }}>
          {msg && (
            <div className={`ui-alert ${msg.tone}`} style={{ display: "block" }} data-test="setup-msg">
              {msg.text}
              {(msg.notes || []).map((n, i) => <div key={i} style={{ marginTop: 4 }}>{n}</div>)}
            </div>
          )}
          <window.AppUI.Card title={<span style={{ display: "flex", gap: 8, alignItems: "center" }}>{setupIcon(step.icon, 16)} {step.title}</span>} sub={step.sub}>
            {!data ? <div className="hint">Loading…</div>
              : active === "finish" ? <SetupFinish {...props} onDone={async () => { await reload().catch(() => {}); setScreen("dashboard"); }}/>
              : <Body key={active} {...props}/>}
          </window.AppUI.Card>
        </div>
      </div>

      <window.AppUI.Modal open={confirmRemove} onClose={() => setConfirmRemove(false)} title="Remove sample data?" icon="trash"
        footer={<>
          <button type="button" className="btn" onClick={() => setConfirmRemove(false)}>Keep it</button>
          <button type="button" className="btn danger" disabled={busy} onClick={removeSample} data-test="confirm-remove">{busy ? "Removing…" : "Remove sample data"}</button>
        </>}>
        <p style={{ margin: 0, fontSize: 13 }}>Deletes every class, subject, teacher, student, parent, fee and receipt that "Fill with sample data" made, here and in the Ledger. Anything you entered yourself stays.</p>
      </window.AppUI.Modal>
    </div>
  );
};

/* Dashboard banner: resume the guided setup (dismissible for this browser). */
const SetupResumeBanner = ({ setScreen }) => {
  const d = window.APP.ONBOARDING;
  const [hidden, setHidden] = useState(() => { try { return localStorage.getItem("setup.banner.hidden") === "1"; } catch { return false; } });
  if (!d || !d.applicable || d.onboarded_at || hidden) return null;
  const done = (d.steps || []).filter((s) => s.done).length;
  return (
    <div className="cx-page" style={{ paddingBottom: 0 }}>
      <div className="su-banner" role="region" aria-label="Guided setup" data-test="setup-banner">
        <span className="su-logo" style={{ width: 40, height: 40 }}>{setupIcon("sparkle", 18)}</span>
        <div className="msg"><strong>Finish setting up {d.profile ? d.profile.name : "your school"}.</strong> {done} of {SETUP_STEPS.length} steps done.</div>
        <button type="button" className="btn primary" onClick={() => setScreen("setup")} data-test="resume-setup">Resume setup</button>
        <button type="button" className="ui-iconbtn" aria-label="Hide" onClick={() => { try { localStorage.setItem("setup.banner.hidden", "1"); } catch {} setHidden(true); }}>{setupIcon("x", 15)}</button>
      </div>
    </div>
  );
};

/* After a prospect demo is claimed (?claimed=1): keep the sample, or start clean. */
const SetupClaimedModal = ({ setScreen }) => {
  const [open, setOpen] = useState(() => { try { return new URLSearchParams(window.location.search).get("claimed") === "1"; } catch { return false; } });
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const clearParam = () => {
    try {
      const u = new URL(window.location.href); u.searchParams.delete("claimed");
      window.history.replaceState(null, "", u.pathname + u.search + u.hash);
    } catch {}
  };
  const keep = () => { clearParam(); setOpen(false); setScreen("setup"); };
  const clean = async () => {
    setBusy(true); setErr(null);
    try {
      await window.AppApi.post("/admin/onboarding/sample-remove", {});
      window.APP.ONBOARDING = await window.AppApi.get("/admin/onboarding").catch(() => window.APP.ONBOARDING);
      clearParam(); setOpen(false); setScreen("setup");
    } catch (e) { setErr(setupErr(e)); } finally { setBusy(false); }
  };
  if (!open) return null;
  return (
    <window.AppUI.Modal open onClose={keep} title="Welcome to your school" icon="sparkle"
      footer={<>
        <button type="button" className="btn" disabled={busy} onClick={keep} data-test="claimed-keep">Keep the sample data</button>
        <button type="button" className="btn primary" disabled={busy} onClick={clean} data-test="claimed-clean">{busy ? "Clearing…" : "Start clean"}</button>
      </>}>
      <p style={{ margin: 0, fontSize: 13 }}>This school is now yours. Keep the sample teachers, students and fees to explore a little longer, or start clean and set up your real school. You can remove the sample data later too.</p>
      {err && <div className="ui-alert red" style={{ marginTop: 10 }}>{err}</div>}
    </window.AppUI.Modal>
  );
};

window.SetupScreen = SetupScreen;
window.SetupResumeBanner = SetupResumeBanner;
window.SetupClaimedModal = SetupClaimedModal;
