/* ============================================================
   Officer + Admin views
   Verification queue, roster, test-report filling, mark attendance,
   post notices, plus the front-office issuance & overview.
   ============================================================ */

/* ============================================================
   OFFICER OVERVIEW — cohort dashboard
   ============================================================ */
function OfficerOverview({ ctx }) {
  const students = window.Store.listStudents();
  const counts = aggregate(students);

  return (
    <div className="module">
      <ModuleHeader
        crumbs="INSTRUCTOR CONSOLE"
        title={`Welcome, ${ctx.session.userName.split(' ').slice(-1)[0]}.`}
        sub="Cohort at a glance. Open the queue when you have a few minutes — it's where verifications pile up."
      />
      <div className="grid g-4">
        <div className="kpi accent">
          <div className="lbl">Active students</div>
          <div className="val">{students.length}</div>
          <div className="delta">Batch {students[0]?.profile?.batch || '—'}</div>
        </div>
        <div className="kpi">
          <div className="lbl">Pending verifications</div>
          <div className="val">{counts.pendingDocs + counts.pendingMedical + counts.pendingReg}</div>
          <div className="delta">{counts.pendingDocs} docs · {counts.pendingMedical} medical · {counts.pendingReg} forms</div>
        </div>
        <div className="kpi">
          <div className="lbl">Tests filed</div>
          <div className="val">{counts.testsFiled}<span className="unit">/{students.length*3}</span></div>
          <div className="delta">{students.length ? Math.round(counts.testsFiled / (students.length*3) * 100) : 0}% complete</div>
        </div>
        <div className="kpi">
          <div className="lbl">Ready for certificate</div>
          <div className="val">{counts.ready}</div>
          <div className="delta">{students.length - counts.ready} still in progress</div>
        </div>
      </div>

      <div className="grid g-side">
        <div className="card">
          <div className="card-hd">
            <div><h3>Student progress</h3><div className="sub">Click a row to dive into a student's file.</div></div>
            <button className="btn btn-sm" onClick={()=>ctx.setRoute('roster')}>View roster →</button>
          </div>
          <table className="tbl">
            <thead><tr>
              <th>Student</th>
              <th>Batch</th>
              <th>Progress</th>
              <th className="right">Overall</th>
              <th>Pending</th>
            </tr></thead>
            <tbody>
              {students.map(s => {
                const p = window.calcProgress(s);
                return (
                  <tr key={s.id} onClick={()=>{
                    ctx.setRoute('roster');
                    sessionStorage.setItem('roster.focus', s.id);
                  }} style={{cursor:'pointer'}}>
                    <td>
                      <div className="row" style={{gap:8}}>
                        <div className="brand-mark" style={{background:'var(--bg-2)',color:'var(--ink-2)'}}>{UI.initials(s.profile.fullName)}</div>
                        <div>
                          <div className="fw-6 small">{s.profile.fullName}</div>
                          <div className="tiny muted mono">{s.profile.rollNo || s.id}</div>
                        </div>
                      </div>
                    </td>
                    <td className="mono small">{s.profile.batch || <span className="muted">—</span>}</td>
                    <td style={{minWidth:160}}>
                      <div className="bar"><i style={{width:`${p.overall}%`,
                        background: p.overall>=85 ? 'var(--good)' : p.overall>=50 ? 'var(--accent)' : 'var(--warn)'}}/></div>
                    </td>
                    <td className="right mono fw-6">{p.overall}%</td>
                    <td><PendingFlags rec={s}/></td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        <div className="stack">
          <div className="card">
            <div className="card-hd"><h3>Quick actions</h3></div>
            <div className="card-body" style={{padding:'8px 0'}}>
              <button className="list-row" onClick={()=>ctx.setRoute('queue')}>
                <div className="ix">Q</div>
                <div className="nm"><div className="ttl">Verification queue</div>
                  <div className="desc">{counts.pendingDocs + counts.pendingMedical + counts.pendingReg} items</div></div>
              </button>
              <button className="list-row" onClick={()=>ctx.setRoute('tests-fill')}>
                <div className="ix">T</div>
                <div className="nm"><div className="ttl">Fill a test report</div>
                  <div className="desc">Progress / skill examinations</div></div>
              </button>
              <button className="list-row" onClick={()=>ctx.setRoute('attendance-mark')}>
                <div className="ix">A</div>
                <div className="nm"><div className="ttl">Mark attendance</div>
                  <div className="desc">Today's roster</div></div>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// Match OfficerQueue exactly — only doc IDs still in the live DOCUMENTS
// list count as pending. Stale schema (aadhar / govt-id / logbook /
// basic-knowledge etc) is ignored so counters and the queue agree.
const DOC_IDS = () => new Set((window.DOCUMENTS || []).map(d => d.id));

function aggregate(students) {
  const docIds = DOC_IDS();
  let pendingDocs = 0, pendingMedical = 0, pendingReg = 0, testsFiled = 0, ready = 0;
  students.forEach(s => {
    Object.entries(s.documents || {}).forEach(([id, d]) => {
      if (docIds.has(id) && d && d.status === 'uploaded') pendingDocs++;
    });
    if (s.medical?.status === 'endorsed') pendingMedical++;
    if (s.registration?.submitted && !s.registration?.verifiedByOfficer) pendingReg++;
    ['simulator','rpa','skill'].forEach(k => { if ((s.tests || {})[k]?.finalised) testsFiled++; });
    if (window.calcProgress(s)?.overall >= 85) ready++;
  });
  return { pendingDocs, pendingMedical, pendingReg, testsFiled, ready };
}

function PendingFlags({ rec }) {
  const docIds = DOC_IDS();
  const flags = [];
  if (rec.registration?.submitted && !rec.registration?.verifiedByOfficer) flags.push({c:'pill-warn',l:'Reg'});
  if (rec.medical?.status === 'endorsed') flags.push({c:'pill-warn',l:'Med'});
  const pdocs = Object.entries(rec.documents || {})
    .filter(([id, d]) => docIds.has(id) && d && d.status === 'uploaded').length;
  if (pdocs > 0) flags.push({c:'pill-warn',l:`${pdocs} doc${pdocs>1?'s':''}`});
  if (!flags.length) return <span className="pill pill-good"><i className="dotty"/>None</span>;
  return <div className="row" style={{gap:4}}>{flags.map((f,i)=><span key={i} className={cls('pill',f.c)}>{f.l}</span>)}</div>;
}

/* ============================================================
   OFFICER ROSTER — batch-first drill-in
   Top level: a grid of batches (active + Unassigned + Archived)
   with create + archive + drag-drop reassignment. Click into a
   batch to see its students with the existing left-list / right-
   file layout. Single-source-of-truth for "Students" navigation.
   ============================================================ */
function OfficerRoster({ ctx }) {
  // Remember which batch was open across the queue / student-file
  // jumps so the instructor doesn't lose context.
  const initialBatch = sessionStorage.getItem('roster.batch');
  const [focusBatch, setFocusBatch] = useState(initialBatch); // null = batches grid, '' = Unassigned, otherwise batch name
  const openBatch = (name) => {
    setFocusBatch(name);
    sessionStorage.setItem('roster.batch', name);
  };
  // Open a specific student — drills into their batch (or Unassigned)
  // and pre-focuses them so the instructor sees the StudentFile
  // immediately, without first having to scroll the roster list.
  const openStudent = (sid) => {
    const s = window.Store.getStudent(sid);
    const batchName = (s && s.profile && s.profile.batch) || '';
    sessionStorage.setItem(`roster.focus.${batchName || '_unassigned'}`, sid);
    setFocusBatch(batchName);
    sessionStorage.setItem('roster.batch', batchName);
  };
  const backToBatches = () => {
    setFocusBatch(null);
    sessionStorage.removeItem('roster.batch');
  };

  if (focusBatch === null) {
    return <RosterBatches ctx={ctx} onPick={openBatch} onOpenStudent={openStudent}/>;
  }
  return <RosterInBatch ctx={ctx} batchName={focusBatch} onBack={backToBatches}/>;
}

/* ---------- top level: batches grid ---------- */
// Each chip / batch card is its own component (and lives at module
// scope rather than inside the parent) so React doesn't remount it
// on every parent re-render — remounts would re-bind the touch DnD
// listeners and the user would see drag handles flicker.
function UnassignedChip({ student, onOpen }) {
  const ref = window.useDraggable(student.id);
  // Click → drill into Unassigned roster focused on this student so
  // the instructor can review docs / verify without first assigning.
  // Stop propagation so the parent Unassigned card doesn't also fire.
  const onClick = (e) => { e.stopPropagation(); onOpen && onOpen(student.id); };
  return (
    <div ref={ref}
      onClick={onClick}
      role="button" tabIndex={0}
      title={`Open ${student.profile.fullName || student.id}'s file · drag to a batch to assign`}
      style={{
        display:'inline-flex',alignItems:'center',gap:6,
        padding:'4px 10px 4px 4px',borderRadius:999,
        border:'1px solid var(--line-strong)',
        background:'var(--surface)',
        userSelect:'none',maxWidth:'100%',
      }}>
      <span className="brand-mark" style={{width:22,height:22,fontSize:10,flexShrink:0}}>
        {UI.initials(student.profile.fullName)}
      </span>
      <span className="small" style={{
        overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',
        maxWidth:160,
      }}>
        {student.profile.fullName || '—'}
      </span>
    </div>
  );
}

function BatchCard({ batch, roster, isArchived, assignTo, onOpen, onArchive, onUnarchive, onDelete }) {
  const onDrop = useCallback((sid) => assignTo(sid, batch.name), [assignTo, batch.name]);
  const dropRef = window.useDropZone(onDrop);
  const avg = roster.length
    ? Math.round(roster.reduce((s, st) => s + ((window.calcProgress(st) || {}).overall || 0), 0) / roster.length)
    : 0;
  return (
    <div className="card" ref={dropRef}
      style={{padding:0,border:'1px solid var(--line-strong)',
              opacity: isArchived ? 0.7 : 1, position:'relative'}}>
      <div className="card-hd" role="button" tabIndex={0}
        onClick={()=>onOpen(batch.name)}
        style={{borderBottom:'1px solid var(--line)',cursor:'pointer'}}>
        <div style={{minWidth:0}}>
          <h3 style={{margin:0,fontSize:14}}>{batch.name}</h3>
          <div className="sub" style={{margin:'2px 0 0'}}>
            {batch.course ? batch.course : 'Course not set'}{batch.startDate ? ` · ${batch.startDate}` : ''}
          </div>
        </div>
        {isArchived
          ? <span className="pill pill-ghost"><i className="dotty"/>Archived</span>
          : <span className="pill pill-good"><i className="dotty"/>Active</span>}
      </div>
      <div className="card-body" style={{padding:'12px 16px'}}>
        <div className="row" style={{justifyContent:'space-between',marginBottom:8}}>
          <div>
            <div className="tiny upper muted fw-6">Students</div>
            <div style={{fontSize:22,fontWeight:700}}>{roster.length}</div>
          </div>
          <div style={{textAlign:'right'}}>
            <div className="tiny upper muted fw-6">Avg progress</div>
            <div style={{fontSize:22,fontWeight:700}}>{avg}%</div>
          </div>
        </div>
        <div className="bar" style={{marginBottom:8}}>
          <i style={{width:`${avg}%`,
            background: avg>=85 ? 'var(--good)' : avg>=50 ? 'var(--accent)' : 'var(--warn)'}}/>
        </div>
        <div className="row" style={{gap:6,flexWrap:'wrap'}}>
          {!isArchived && (
            <button className="btn btn-xs" onClick={()=>onArchive(batch)}
              title="Archive once the course is complete">Archive</button>
          )}
          {isArchived && (
            <button className="btn btn-xs" onClick={()=>onUnarchive(batch)}
              title="Move back to active">Restore</button>
          )}
          <button className="btn btn-xs btn-bad" onClick={()=>onDelete(batch)}>Delete</button>
          <span style={{flex:1}}/>
          <span className="tiny muted">Tap header to open →</span>
        </div>
        {isArchived && batch.archivedAt && (
          <div className="tiny muted" style={{marginTop:6}}>
            Archived {UI.fmtDate(batch.archivedAt)}{batch.archivedBy ? ` · ${batch.archivedBy}` : ''}
          </div>
        )}
      </div>
    </div>
  );
}

function UnassignedDropCard({ unassigned, assignTo, onOpen, onOpenStudent }) {
  const onDrop = useCallback((sid) => assignTo(sid, ''), [assignTo]);
  const dropRef = window.useDropZone(onDrop);
  return (
    <div className="card" ref={dropRef}
      style={{padding:0,border:'1px dashed var(--line-strong)'}}>
      <div className="card-hd" role="button" tabIndex={0}
        onClick={()=>onOpen('')}
        style={{borderBottom:'1px solid var(--line)',cursor:'pointer'}}>
        <div><h3 style={{margin:0,fontSize:14}}>Unassigned</h3>
          <div className="sub" style={{margin:'2px 0 0'}}>
            Click a chip to open the student's file · drag a chip onto a batch to assign
          </div></div>
        <span className="pill pill-warn"><i className="dotty"/>{unassigned.length}</span>
      </div>
      {unassigned.length > 0 ? (
        <div className="card-body" style={{padding:'10px 14px'}}>
          <div className="row-wrap" style={{gap:6}}>
            {unassigned.map(s => (
              <UnassignedChip key={s.id} student={s} onOpen={onOpenStudent}/>
            ))}
          </div>
        </div>
      ) : (
        <div className="card-body small muted" style={{padding:'10px 16px'}}>
          Nothing here — every student is in a batch.
        </div>
      )}
    </div>
  );
}

function RosterBatches({ ctx, onPick, onOpenStudent }) {
  const batches = ctx.store && ctx.store.batches ? Object.values(ctx.store.batches) : [];
  // Live-filtered to role='student' so former-students / orphan
  // records don't leak into the batches grid + Unassigned bucket.
  const students = window.Store.listStudents();
  const [showArchived, setShowArchived] = useState(false);
  const [creatingName, setCreatingName] = useState('');
  const [creatingCourse, setCreatingCourse] = useState('');
  const [busy, setBusy] = useState(false);

  // Group students by batch.
  const byBatch = {};
  students.forEach(s => {
    const k = (s.profile && s.profile.batch) || '';
    (byBatch[k] = byBatch[k] || []).push(s);
  });

  const active = batches.filter(b => !b.archived).sort((a,b) => (a.name||'').localeCompare(b.name||''));
  const archived = batches.filter(b => b.archived).sort((a,b) => (a.archivedAt||'').localeCompare(b.archivedAt||''));
  const unassigned = byBatch[''] || [];

  const createBatch = async (e) => {
    e.preventDefault();
    const nm = creatingName.trim();
    if (!nm) return;
    if (batches.some(b => b.name === nm)) {
      ctx.setToast(`Batch "${nm}" already exists`);
      return;
    }
    setBusy(true);
    try {
      await window.Store.addBatch({ name: nm, course: creatingCourse });
      window.Store.logAudit('Batch created', nm, creatingCourse || '');
      setCreatingName(''); setCreatingCourse('');
      ctx.setToast(`Batch "${nm}" created`);
    } catch (err) {
      ctx.setToast(err.message || 'Could not create batch');
    } finally { setBusy(false); }
  };

  const archiveBatch = async (b) => {
    const roster = byBatch[b.name] || [];
    const msg = `Archive batch "${b.name}"? ${roster.length} student${roster.length===1?'':'s'} stay assigned, but the batch is collapsed into the Archived section.`;
    if (!confirm(msg)) return;
    try {
      await window.Store.updateBatch(b.id, {
        archived: true,
        archivedAt: new Date().toISOString(),
        archivedBy: ctx.session.userName,
      });
      window.Store.logAudit('Batch archived', b.name, `${roster.length} students`);
      ctx.setToast(`Batch "${b.name}" archived`);
    } catch (err) { ctx.setToast(err.message || 'Could not archive batch'); }
  };
  const unarchiveBatch = async (b) => {
    try {
      await window.Store.updateBatch(b.id, {
        archived: false, archivedAt: null, archivedBy: '',
      });
      window.Store.logAudit('Batch unarchived', b.name, '');
      ctx.setToast(`Batch "${b.name}" restored`);
    } catch (err) { ctx.setToast(err.message || 'Could not restore batch'); }
  };
  const deleteBatch = async (b) => {
    const roster = byBatch[b.name] || [];
    const msg = roster.length > 0
      ? `Delete batch "${b.name}"? ${roster.length} student${roster.length===1?'':'s'} will move to Unassigned. This cannot be undone.`
      : `Delete batch "${b.name}"? This cannot be undone.`;
    if (!confirm(msg)) return;
    try {
      await window.Store.deleteBatch(b.id);
      window.Store.logAudit('Batch deleted', b.name, '');
      ctx.setToast(`Batch "${b.name}" deleted`);
    } catch (err) { ctx.setToast(err.message || 'Could not delete batch'); }
  };

  // Reassign on drop. targetBatch === '' ⇒ Unassigned.
  const assignTo = useCallback((sid, targetBatch) => {
    const student = students.find(s => s.id === sid);
    const prev = (student && student.profile && student.profile.batch) || '';
    if (prev === targetBatch) return;
    window.Store.patchStudent(sid, (r) => { r.profile.batch = targetBatch; return r; });
    window.Store.logAudit('Batch reassigned',
      student?.profile?.fullName || sid,
      `${prev || 'Unassigned'} → ${targetBatch || 'Unassigned'}`);
    ctx.setToast(targetBatch
      ? `${student?.profile?.fullName || 'Student'} moved to ${targetBatch}`
      : `${student?.profile?.fullName || 'Student'} marked unassigned`);
  }, [students, ctx]);

  // BatchCard and UnassignedDropCard are hoisted (defined OUTSIDE
  // RosterBatches, near the top of this file) so React doesn't
  // remount them on every parent re-render — that would tear down
  // and rebind the touch DnD listeners.

  return (
    <div className="module">
      <ModuleHeader
        crumbs="INSTRUCTOR CONSOLE · STUDENTS"
        title="Students"
        sub="Students are grouped by batch. Click a batch to drill in, or drag a student between batches to reassign."
        right={window.CohortExportButton ? <CohortExportButton ctx={ctx}/> : null}
      />

      <div className="grid g-side">
        <div className="stack">
          {/* Active batches grid */}
          {active.length === 0 ? (
            <div className="card">
              <div className="card-body small muted" style={{padding:'24px',textAlign:'center'}}>
                No active batches yet. Use the form on the right to create your first batch.
              </div>
            </div>
          ) : (
            <div className="grid" style={{gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:14}}>
              {active.map(b => (
                <BatchCard key={b.id} batch={b} roster={byBatch[b.name]||[]}
                  isArchived={false}
                  assignTo={assignTo}
                  onOpen={onPick}
                  onArchive={archiveBatch} onUnarchive={unarchiveBatch} onDelete={deleteBatch}/>
              ))}
            </div>
          )}

          {/* Unassigned card — also a drop zone + chip source. */}
          <UnassignedDropCard unassigned={unassigned}
            assignTo={assignTo} onOpen={onPick} onOpenStudent={onOpenStudent}/>

          {/* Archived */}
          {archived.length > 0 && (
            <div>
              <div className="row" style={{justifyContent:'space-between',alignItems:'baseline',margin:'10px 4px'}}>
                <div className="tiny upper muted fw-6">Archived batches · {archived.length}</div>
                <button className="btn-link tiny" onClick={()=>setShowArchived(v=>!v)}>
                  {showArchived ? 'Hide' : 'Show'}
                </button>
              </div>
              {showArchived && (
                <div className="grid" style={{gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:14}}>
                  {archived.map(b => (
                    <BatchCard key={b.id} batch={b} roster={byBatch[b.name]||[]}
                      isArchived
                      assignTo={assignTo}
                      onOpen={onPick}
                      onArchive={archiveBatch} onUnarchive={unarchiveBatch} onDelete={deleteBatch}/>
                  ))}
                </div>
              )}
            </div>
          )}
        </div>

        {/* Side — create batch */}
        <div className="stack">
          <form className="card" onSubmit={createBatch}>
            <div className="card-hd"><div><h3>New batch</h3>
              <div className="sub">Short name and (optionally) the course it runs.</div></div></div>
            <div className="card-body stack-sm" style={{gap:10}}>
              <div className="field">
                <label>Batch name<span className="req">*</span></label>
                <input className="input" type="text" value={creatingName}
                  onChange={e=>setCreatingName(e.target.value)}
                  placeholder="e.g. SC-2026-05"/>
              </div>
              <div className="field">
                <label>Course (optional)</label>
                <select className="select" value={creatingCourse} onChange={e=>setCreatingCourse(e.target.value)}>
                  <option value="">— select —</option>
                  {(window.COURSES || []).map(c => <option key={c} value={c}>{c}</option>)}
                </select>
              </div>
              <button className="btn btn-primary" type="submit" disabled={busy || !creatingName.trim()}>
                {busy ? 'Creating…' : 'Create batch'}
              </button>
            </div>
          </form>

          <div className="card">
            <div className="card-hd"><h3>Tips</h3></div>
            <div className="card-body small muted" style={{lineHeight:1.6}}>
              <p style={{margin:'0 0 8px'}}>New students land in <b>Unassigned</b> when they sign up.</p>
              <p style={{margin:'0 0 8px'}}>Drag a student chip from inside an open batch back here to move them.</p>
              <p style={{margin:0}}>When a course finishes, click <b>Archive</b> on the batch — students stay assigned, but the batch collapses into the Archived section so your active grid stays clean.</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ---------- second level: roster inside one batch ---------- */
function RosterInBatch({ ctx, batchName, onBack }) {
  const allStudents = window.Store.listStudents();
  const students = allStudents.filter(s => ((s.profile && s.profile.batch) || '') === batchName);
  const [q, setQ] = useState('');
  const filtered = students.filter(s =>
    (s.profile.fullName || '').toLowerCase().includes(q.toLowerCase()) ||
    (s.profile.rollNo || '').toLowerCase().includes(q.toLowerCase()));

  const sessionKey = `roster.focus.${batchName || '_unassigned'}`;
  const [focusId, setFocusId] = useState(() =>
    sessionStorage.getItem(sessionKey) || filtered[0]?.id || students[0]?.id || null);
  const focus = students.find(s => s.id === focusId);
  const pick = (id) => { setFocusId(id); sessionStorage.setItem(sessionKey, id); };

  // The drop-back-to-batches drag handler — students inside the
  // open batch list can be dragged back to the title bar to
  // mark them Unassigned.
  const moveTo = (sid, targetBatch) => {
    const st = allStudents.find(x => x.id === sid);
    const prev = (st && st.profile && st.profile.batch) || '';
    if (prev === targetBatch) return;
    window.Store.patchStudent(sid, (r) => { r.profile.batch = targetBatch; return r; });
    window.Store.logAudit('Batch reassigned',
      st?.profile?.fullName || sid,
      `${prev || 'Unassigned'} → ${targetBatch || 'Unassigned'}`);
    ctx.setToast(targetBatch
      ? `${st?.profile?.fullName || 'Student'} moved to ${targetBatch}`
      : `${st?.profile?.fullName || 'Student'} unassigned`);
  };

  const title = batchName ? `Batch · ${batchName}` : 'Unassigned students';
  const sub = `${students.length} student${students.length===1?'':'s'} · pick one to open their file`;

  // Bulk theory-test unlock for the entire batch. Skips students who
  // are already in the desired state and audit-logs each change.
  const bulkSetTheory = (unlock) => {
    const candidates = students.filter(s => !!(s.theory && s.theory.unlocked) !== unlock);
    if (!candidates.length) {
      ctx.setToast(unlock ? 'Every student already has theory unlocked' : 'No theory unlocks to undo');
      return;
    }
    const verb = unlock ? 'Unlock' : 'Re-lock';
    if (!confirm(`${verb} the DGCA theory test for ${candidates.length} student${candidates.length===1?'':'s'} in ${batchName || 'Unassigned'}?`)) return;
    candidates.forEach(s => {
      window.Store.patchStudent(s.id, (r) => {
        r.theory = r.theory || {};
        r.theory.unlocked = unlock;
        r.theory.unlockedAt = unlock ? new Date().toISOString() : null;
        r.theory.unlockedBy = unlock ? ctx.session.userName : '';
        return r;
      });
      window.Store.logAudit(
        unlock ? 'Theory test unlocked' : 'Theory test locked',
        s.profile?.fullName || s.id,
        `Bulk · batch ${batchName || 'Unassigned'}`);
    });
    ctx.setToast(`${verb}ed theory test for ${candidates.length} student${candidates.length===1?'':'s'}`);
  };
  const unlockedCount = students.filter(s => s.theory && s.theory.unlocked).length;
  const lockedCount = students.length - unlockedCount;

  return (
    <div className="module">
      <div className="row" style={{gap:8,marginBottom:10,alignItems:'center',flexWrap:'wrap'}}>
        <button className="btn btn-sm" onClick={onBack}>← All batches</button>
        {students.length > 0 && (
          <>
            {lockedCount > 0 && (
              <button className="btn btn-sm btn-good"
                onClick={()=>bulkSetTheory(true)}
                title="Unlock the DGCA theory test for every student in this batch">
                Unlock theory test for all {lockedCount > 0 ? `(${lockedCount})` : ''}
              </button>
            )}
            {unlockedCount > 0 && (
              <button className="btn btn-sm"
                onClick={()=>bulkSetTheory(false)}
                title="Re-lock the DGCA theory test for every student in this batch">
                Re-lock theory ({unlockedCount})
              </button>
            )}
          </>
        )}
      </div>
      <ModuleHeader
        crumbs="INSTRUCTOR CONSOLE · STUDENTS"
        title={title}
        sub={sub}
        right={window.CohortExportButton ? <CohortExportButton ctx={ctx}/> : null}
      />

      {students.length === 0 ? (
        <div className="card">
          <div className="card-body small muted" style={{padding:'40px',textAlign:'center'}}>
            No students in this batch yet. Drag someone in from the batches grid.
          </div>
        </div>
      ) : (
        <div className="grid roster-split" style={{gridTemplateColumns:'280px minmax(0,1fr)', gap:14}}>
          <RosterListPane
            students={students} filtered={filtered}
            q={q} setQ={setQ}
            focusId={focusId} pick={pick}
            batchName={batchName} moveTo={moveTo}/>
          {focus
            ? <StudentFile rec={focus} ctx={ctx}/>
            : <div className="muted" style={{padding:20}}>Pick a student on the left.</div>}
        </div>
      )}
    </div>
  );
}

function RosterListPane({ students, filtered, q, setQ, focusId, pick, batchName, moveTo }) {
  return (
    <div className="card roster-pane" style={{height:'fit-content'}}>
      <div className="card-hd">
        <div style={{flex:1}}>
          <div className="search">
            {window.NavIcons.search}
            <input placeholder="Search students…" value={q} onChange={e=>setQ(e.target.value)}/>
          </div>
        </div>
      </div>
      <div className="list">
        {filtered.map(s => (
          <RosterRow key={s.id} s={s} active={s.id === focusId} pick={pick}/>
        ))}
        {filtered.length === 0 && (
          <div className="small muted" style={{padding:'14px 18px'}}>No matches.</div>
        )}
      </div>
    </div>
  );
}


function RosterRow({ s, active, pick }) {
  const p = window.calcProgress(s) || {};
  const profile = s.profile || {};
  // Fall back to a SHORT slice of the uid when the student has no roll
  // yet — the full Firebase uid is an unbreakable monospace string
  // that pushes the row's min-content past the viewport on phones.
  const subId = profile.rollNo || (s.id ? `id ${s.id.slice(0, 8)}…` : '—');
  return (
    <button className="list-row"
      style={{background: active ? 'var(--accent-soft)' : 'transparent'}}
      onClick={()=>pick(s.id)}>
      <div className="brand-mark"
        style={{background: active ? 'var(--accent)':'var(--bg-2)', color: active ? '#fff':'var(--ink-2)'}}>
        {UI.initials(profile.fullName || '?')}
      </div>
      <div className="nm" style={{minWidth:0}}>
        <div className="ttl" style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{profile.fullName || 'Unnamed'}</div>
        <div className="desc mono" style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{subId}</div>
      </div>
      <div className="meta">
        <div className="mono small fw-6">{p.overall || 0}%</div>
      </div>
    </button>
  );
}

/* Read-only view of everything a student submitted on the
   registration form — so a verifier sees what they're approving. */
function RegistrationReview({ rec }) {
  const p = rec.profile || {};
  const reg = rec.registration || {};
  const end = window.courseEndDate(p.startDate, p.course);
  const Group = ({ title, rows }) => (
    <div style={{marginBottom:14}}>
      <div className="tiny upper muted fw-6" style={{marginBottom:6}}>{title}</div>
      <div className="grid g-2" style={{gap:'6px 18px'}}>
        {rows.filter(([,v]) => v !== undefined).map(([k,v]) => (
          <div key={k} className="row" style={{justifyContent:'space-between',gap:10,
            padding:'4px 0',borderBottom:'1px solid var(--line)'}}>
            <span className="tiny muted" style={{flexShrink:0}}>{k}</span>
            <span className="small" style={{textAlign:'right',wordBreak:'break-word'}}>{v || '—'}</span>
          </div>
        ))}
      </div>
    </div>
  );
  return (
    <div className="card-body">
      {!reg.submitted && (
        <div className="banner" style={{marginBottom:12}}>
          <div className="body">Not submitted yet — this is the student's current draft.</div>
        </div>
      )}
      <Group title="Personal" rows={[
        ['Full name', p.fullName], ["Father's name", p.fathersName],
        ['Date of birth', p.dob], ['Qualification', p.qualification],
        ['Organisation', p.organisation], ['Batch', p.batch]]}/>
      <Group title="Contact" rows={[
        ['Email', p.email], ['Phone', p.phone],
        ['Address', p.address], ['Pincode', p.pincode]]}/>
      <Group title="Identification" rows={[
        ['Aadhaar', p.aadhaar], ['Passport', p.passport], ['Other Govt. ID', p.govtId]]}/>
      <Group title="Course" rows={[
        ['Course', p.course], ['Roll / Student No.', p.rollNo],
        ['Start date', p.startDate ? UI.fmtDate(p.startDate) : ''],
        ['Expected completion', end ? UI.fmtDate(end) : '']]}/>
      <Group title="Biometric" rows={[
        ['Height', p.height], ['Weight', p.weight], ['Chest', p.chest]]}/>
      <Group title="Declaration" rows={[
        ['Accepted declaration', reg.declarationAccepted ? 'Yes' : 'No'],
        ['Place', reg.place], ['Date', reg.date],
        ['Signed', reg.signature ? 'Yes' : 'No']]}/>
    </div>
  );
}
window.RegistrationReview = RegistrationReview;

/* ============================================================
   InstructorUploads — fixed slots (Logbook, IDA cert, eGCA RPC)
   plus a free-form Extras bucket. End-of-course artefacts that
   ship with the DGCA submission packet.
   ============================================================ */
function InstructorUploads({ rec, ctx }) {
  const doc = rec.instructorDocs || {
    logbook: { files: [] }, idaCert: { files: [] }, egcaRpc: { files: [] }, extras: [],
  };
  const fixed = [
    { key: 'logbook', title: 'Completed Logbook',
      desc: 'Pilot logbook with all training flights logged and signed.' },
    { key: 'idaCert', title: 'IDA Issued Certificate',
      desc: 'IDA RPTO completion certificate.' },
    { key: 'egcaRpc', title: 'eGCA RPC',
      desc: 'Remote Pilot Certificate downloaded from the eGCA portal.' },
  ];

  const onUploadSlot = (slot, files) => {
    window.Store.patchStudent(rec.id, (r) => {
      r.instructorDocs = r.instructorDocs || { logbook:{files:[]}, idaCert:{files:[]}, egcaRpc:{files:[]}, extras:[] };
      const cell = r.instructorDocs[slot] || { files: [] };
      r.instructorDocs[slot] = {
        ...cell,
        files: [...(cell.files || []), ...files],
        updatedAt: new Date().toISOString(),
        updatedBy: ctx.session.userName,
      };
      return r;
    });
    window.Store.logAudit('Instructor doc uploaded',
      (rec.profile?.fullName || 'Student'), fixed.find(f=>f.key===slot)?.title || slot);
    ctx.setToast('Uploaded');
  };
  const removeSlotFile = (slot, ix) => {
    const cell = doc[slot] || { files: [] };
    const gone = (cell.files || [])[ix];
    window.Store.patchStudent(rec.id, (r) => {
      r.instructorDocs[slot].files.splice(ix, 1);
      return r;
    });
    if (gone?.path) window.Store.deleteFile(gone.path);
  };

  const addExtra = () => {
    const title = (prompt('Document title (e.g. "Insurance certificate"):') || '').trim();
    if (!title) return;
    const id = 'x' + Date.now().toString(36);
    window.Store.patchStudent(rec.id, (r) => {
      r.instructorDocs = r.instructorDocs || { logbook:{files:[]}, idaCert:{files:[]}, egcaRpc:{files:[]}, extras:[] };
      r.instructorDocs.extras = [...(r.instructorDocs.extras || []), {
        id, title, files: [],
        addedAt: new Date().toISOString(),
        addedBy: ctx.session.userName,
      }];
      return r;
    });
    ctx.setToast(`Added "${title}"`);
  };
  const uploadExtra = (exId, files) => {
    window.Store.patchStudent(rec.id, (r) => {
      const arr = r.instructorDocs.extras || [];
      const ix = arr.findIndex(x => x.id === exId);
      if (ix < 0) return r;
      arr[ix] = { ...arr[ix], files: [...(arr[ix].files || []), ...files],
        updatedAt: new Date().toISOString(), updatedBy: ctx.session.userName };
      return r;
    });
  };
  const removeExtra = (exId) => {
    if (!confirm('Remove this extra document and its files?')) return;
    const ex = (doc.extras || []).find(x => x.id === exId);
    window.Store.patchStudent(rec.id, (r) => {
      r.instructorDocs.extras = (r.instructorDocs.extras || []).filter(x => x.id !== exId);
      return r;
    });
    (ex?.files || []).forEach(f => f.path && window.Store.deleteFile(f.path));
    ctx.setToast('Removed');
  };
  const removeExtraFile = (exId, ix) => {
    const ex = (doc.extras || []).find(x => x.id === exId);
    const gone = (ex?.files || [])[ix];
    window.Store.patchStudent(rec.id, (r) => {
      const arr = r.instructorDocs.extras || [];
      const i = arr.findIndex(x => x.id === exId);
      if (i < 0) return r;
      arr[i].files.splice(ix, 1);
      return r;
    });
    if (gone?.path) window.Store.deleteFile(gone.path);
  };

  return (
    <div className="card">
      <div className="card-hd">
        <div>
          <h3>Instructor uploads</h3>
          <div className="sub">End-of-course documents you add to the student's file. Used in the DGCA submission packet.</div>
        </div>
        <button className="btn btn-sm" onClick={addExtra}>+ Add other document</button>
      </div>
      <div className="card-body stack-sm" style={{gap:14}}>
        {fixed.map(slot => {
          const cell = doc[slot.key] || { files: [] };
          const has = (cell.files || []).length > 0;
          return (
            <div key={slot.key} style={{border:'1px solid var(--line)',borderRadius:8,padding:'12px 14px',background:has?'var(--good-soft)':'var(--bg)'}}>
              <div className="row" style={{gap:10,alignItems:'flex-start',justifyContent:'space-between'}}>
                <div style={{flex:1,minWidth:0}}>
                  <div className="small" style={{fontWeight:600}}>{slot.title}</div>
                  <div className="tiny muted">{slot.desc}</div>
                  {cell.updatedAt && (
                    <div className="tiny" style={{marginTop:4,color:'var(--muted)'}}>
                      Last updated {UI.fmtDateTime(cell.updatedAt)}{cell.updatedBy ? ` · ${cell.updatedBy}` : ''}
                    </div>
                  )}
                </div>
                <StatusPill status={has ? 'verified' : 'not-started'}/>
              </div>
              {has && (
                <div className="row-wrap" style={{marginTop:8}}>
                  {(cell.files || []).map((f,i) => (
                    <FileChip key={i} file={f} onRemove={()=>removeSlotFile(slot.key, i)}/>
                  ))}
                </div>
              )}
              <div style={{marginTop:10}}>
                <FileDrop onFiles={(files)=>onUploadSlot(slot.key, files)}
                  studentId={rec.id} docId={`instructor-${slot.key}`} multiple
                  hint={`Add ${slot.title} · PDF, JPG or PNG · under 2 MB each`}/>
              </div>
            </div>
          );
        })}

        {(doc.extras || []).length > 0 && (
          <div style={{marginTop:6,borderTop:'1px dashed var(--line-strong)',paddingTop:14}}>
            <div className="tiny upper muted fw-6" style={{marginBottom:8}}>Extras</div>
            <div className="stack-sm" style={{gap:10}}>
              {(doc.extras || []).map(ex => (
                <div key={ex.id} style={{border:'1px solid var(--line)',borderRadius:8,padding:'12px 14px',background:'var(--bg)'}}>
                  <div className="row" style={{justifyContent:'space-between',gap:10,alignItems:'flex-start'}}>
                    <div style={{flex:1,minWidth:0}}>
                      <div className="small" style={{fontWeight:600}}>{ex.title}</div>
                      {ex.addedAt && (
                        <div className="tiny muted" style={{marginTop:2}}>
                          Added {UI.fmtDateTime(ex.addedAt)}{ex.addedBy ? ` · ${ex.addedBy}` : ''}
                        </div>
                      )}
                    </div>
                    <button className="btn btn-xs btn-bad" onClick={()=>removeExtra(ex.id)}>Remove</button>
                  </div>
                  {(ex.files || []).length > 0 && (
                    <div className="row-wrap" style={{marginTop:8}}>
                      {(ex.files || []).map((f,i) => (
                        <FileChip key={i} file={f} onRemove={()=>removeExtraFile(ex.id, i)}/>
                      ))}
                    </div>
                  )}
                  <div style={{marginTop:10}}>
                    <FileDrop onFiles={(files)=>uploadExtra(ex.id, files)}
                      studentId={rec.id} docId={`instructor-extra-${ex.id}`} multiple
                      hint="PDF, JPG or PNG · under 2 MB each"/>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
window.InstructorUploads = InstructorUploads;

/* ============================================================
   EnrolmentCard
   Instructor-controlled fields: batch, roll number, theory unlock.
   Students cannot edit any of these.
   ============================================================ */
function EnrolmentCard({ rec, ctx, onSave, onToggleTheory }) {
  const [batch, setBatch] = useState(rec.profile?.batch || '');
  const [rollNo, setRollNo] = useState(rec.profile?.rollNo || '');
  const [creating, setCreating] = useState(false);
  // Re-sync local state when switching between students.
  useEffect(() => {
    setBatch(rec.profile?.batch || '');
    setRollNo(rec.profile?.rollNo || '');
  }, [rec.id]);
  const dirty = (batch !== (rec.profile?.batch || '')) || (rollNo !== (rec.profile?.rollNo || ''));
  const t = rec.theory || {};
  const batches = (ctx && ctx.store && ctx.store.batches) ? Object.values(ctx.store.batches) : [];
  // Ensure the currently assigned batch is still selectable even if it
  // was deleted from the batches collection.
  const options = (() => {
    const names = batches.map(b => b.name).filter(Boolean);
    if (rec.profile?.batch && !names.includes(rec.profile?.batch)) {
      names.unshift(rec.profile?.batch);
    }
    return Array.from(new Set(names)).sort((a, b) => a.localeCompare(b));
  })();
  const createBatch = async () => {
    const name = (prompt('Name for the new batch (e.g. SC-2026-05):') || '').trim();
    if (!name) return;
    if (options.includes(name)) {
      ctx && ctx.setToast && ctx.setToast(`Batch "${name}" already exists`);
      setBatch(name);
      return;
    }
    setCreating(true);
    try {
      await window.Store.addBatch({ name });
      window.Store.logAudit('Batch created', name, '');
      setBatch(name);
      ctx && ctx.setToast && ctx.setToast(`Batch "${name}" created — assigning to student`);
    } catch (e) {
      ctx && ctx.setToast && ctx.setToast(e.message || 'Could not create batch');
    } finally {
      setCreating(false);
    }
  };
  return (
    <div className="card">
      <div className="card-hd">
        <div><h3>Enrolment</h3>
          <div className="sub">Batch &amp; roll number are set by the instructor. The theory test stays locked until you open it.</div></div>
      </div>
      <div className="card-body">
        <div className="row enrol-row" style={{gap:12,alignItems:'flex-end',flexWrap:'wrap'}}>
          <div className="field enrol-field" style={{flex:'1 1 220px',minWidth:0}}>
            <label>Batch</label>
            <div className="row" style={{gap:6}}>
              <select className="select" value={batch} onChange={e=>setBatch(e.target.value)}
                style={{flex:1,minWidth:0}}>
                <option value="">— Unassigned —</option>
                {options.map(name => <option key={name} value={name}>{name}</option>)}
              </select>
              <button type="button" className="btn btn-sm" onClick={createBatch} disabled={creating}
                title="Create a new batch" style={{flexShrink:0}}>
                {creating ? '…' : '+ New'}
              </button>
            </div>
            <div className="input-help">
              {batches.length === 0
                ? 'No batches yet — create one to group students together.'
                : `${batches.length} batch${batches.length===1?'':'es'} available`}
            </div>
          </div>
          <div className="field enrol-field" style={{flex:'1 1 180px',minWidth:0}}>
            <label>Roll / Student No.</label>
            <input className="input" type="text" value={rollNo}
              placeholder="e.g. RPA-001"
              onChange={e=>setRollNo(e.target.value)}/>
          </div>
          <button className="btn btn-primary btn-sm enrol-save" disabled={!dirty}
            onClick={()=>onSave(batch.trim(), rollNo.trim())}>Save</button>
        </div>
        <div style={{marginTop:14,paddingTop:14,borderTop:'1px solid var(--line)'}}>
          <div className="row" style={{gap:12,alignItems:'center',justifyContent:'space-between',flexWrap:'wrap'}}>
            <div>
              <div className="small" style={{fontWeight:600}}>Theory test</div>
              <div className="tiny muted">
                {t.unlocked
                  ? <>Unlocked {t.unlockedAt ? `on ${UI.fmtDateTime(t.unlockedAt)}` : ''}{t.unlockedBy ? ` by ${t.unlockedBy}` : ''} — student can take the exam.</>
                  : <>Locked — the student cannot take the exam until you unlock it.</>}
              </div>
            </div>
            <button className={cls('btn btn-sm', t.unlocked ? 'btn-bad' : 'btn-good')}
              onClick={onToggleTheory}>
              {t.unlocked ? 'Re-lock theory test' : 'Unlock theory test'}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function StudentFile({ rec, ctx }) {
  const p = window.calcProgress(rec);
  // Defensive aliases — legacy student records may be missing entire
  // subtrees (registration, medical, tests, documents, profile). All
  // subsequent reads MUST go through these so a missing subtree just
  // renders an empty cell instead of crashing the page.
  const profile     = rec.profile      || {};
  const medical     = rec.medical      || {};
  const tests       = rec.tests        || {};
  const documents   = rec.documents    || {};
  const verifyDoc = (id, ok, note) => {
    // On reject: collect the files about to be discarded so we can
    // delete them from Storage AFTER the record patch lands. The
    // end-state per slot is one verified document — never a verified
    // PLUS an orphan rejected one.
    const filesToPurge = !ok
      ? (((rec.documents || {})[id] || {}).files || []).filter(f => f && f.path)
      : [];
    const text = ok ? '' : (note || 'Please re-upload').trim();

    window.Store.patchStudent(rec.id, (r) => {
      r.documents = r.documents || {};
      const cell = r.documents[id] || { status: 'not-started', files: [] };
      cell.status = ok ? 'verified' : 'rejected';
      cell.verifiedAt = new Date().toISOString();
      cell.verifiedBy = ctx.session.userName;
      cell.rejectionNote = text;
      if (!ok) {
        cell.files = [];                          // wipe on reject
        cell.messages = [...(cell.messages || []), {
          ts: new Date().toISOString(),
          from: ctx.session.userName, role: 'instructor',
          kind: 'reject', text,
        }];
      }
      r.documents[id] = cell;
      return r;
    });
    filesToPurge.forEach(f => window.Store.deleteFile(f.path));

    const dt = window.DOCUMENTS.find(d=>d.id===id)?.title || id;
    window.Store.logAudit(ok ? 'Document verified' : 'Document rejected',
      (rec.profile?.fullName || 'Student'), dt + (ok ? '' : ` — ${text || 'Please re-upload'}`));
    ctx.setToast(`${dt} · ${ok?'verified':'rejected'}`);
  };
  const verifyMedical = () => {
    window.Store.patchStudent(rec.id, (r) => {
      r.medical.status = 'verified';
      r.medical.verifiedAt = new Date().toISOString();
      r.medical.verifiedBy = ctx.session.userName;
      return r;
    });
    window.Store.logAudit('Medical verified', (rec.profile?.fullName || 'Student'), '');
    ctx.setToast('Medical certificate verified');
  };
  const verifyReg = () => {
    window.Store.patchStudent(rec.id, (r) => { r.registration.verifiedByOfficer = true; return r; });
    window.Store.logAudit('Registration verified', (rec.profile?.fullName || 'Student'), '');
    ctx.setToast('Registration verified');
  };
  const saveEnrolment = (batch, rollNo) => {
    const prev = { batch: rec.profile?.batch || '', rollNo: rec.profile?.rollNo || '' };
    window.Store.patchStudent(rec.id, (r) => {
      r.profile.batch = batch;
      r.profile.rollNo = rollNo;
      return r;
    });
    const diffs = [];
    if ((prev.batch || '') !== (batch || '')) diffs.push(`batch ${prev.batch || '—'} → ${batch || '—'}`);
    if ((prev.rollNo || '') !== (rollNo || '')) diffs.push(`roll ${prev.rollNo || '—'} → ${rollNo || '—'}`);
    if (diffs.length) {
      window.Store.logAudit('Enrolment updated', (rec.profile?.fullName || 'Student'), diffs.join(' · '));
      ctx.setToast('Enrolment updated');
    }
  };
  const toggleTheoryUnlock = () => {
    const next = !rec.theory.unlocked;
    window.Store.patchStudent(rec.id, (r) => {
      r.theory.unlocked = next;
      r.theory.unlockedAt = next ? new Date().toISOString() : null;
      r.theory.unlockedBy = next ? ctx.session.userName : '';
      return r;
    });
    window.Store.logAudit(next ? 'Theory test unlocked' : 'Theory test locked',
      (rec.profile?.fullName || 'Student'), '');
    ctx.setToast(next ? 'Theory test unlocked for student' : 'Theory test re-locked');
  };

  return (
    <div className="stack">
      <div className="card">
        <div className="card-body student-file-hd">
          <div className="brand-mark" style={{width:48,height:48,fontSize:18}}>{UI.initials(profile.fullName || '?')}</div>
          <div className="student-file-id" style={{minWidth:0}}>
            <div style={{fontSize:18,fontWeight:600}}>{profile.fullName || 'Unnamed student'}</div>
            <div className="small muted">{profile.course || 'Course not set'} · Batch {profile.batch || '—'} · Roll {profile.rollNo || '—'}</div>
            <div className="row-wrap" style={{gap:14,marginTop:6}}>
              <span className="tiny muted mono">📧 {profile.email || '—'}</span>
              <span className="tiny muted mono">📞 {profile.phone || '—'}</span>
              <span className="tiny muted mono">DOB {profile.dob || '—'}</span>
            </div>
          </div>
          <Donut value={p.overall} size={86} stroke={8} sub="overall"/>
        </div>
      </div>

      {/* Enrolment — instructor-controlled batch / roll / theory unlock */}
      <EnrolmentCard rec={rec} ctx={ctx} onSave={saveEnrolment} onToggleTheory={toggleTheoryUnlock}/>

      {/* Registration card — full submitted form, read-only */}
      <div className="card">
        <div className="card-hd">
          <div><h3>Registration form</h3><div className="sub">Submitted {rec.registration.submittedAt ? UI.fmtDateTime(rec.registration.submittedAt) : '—'}</div></div>
          <div className="row" style={{gap:8}}>
            <StatusPill status={rec.registration.verifiedByOfficer ? 'verified' : rec.registration.submitted ? 'submitted' : 'draft'}/>
            {rec.registration.submitted && !rec.registration.verifiedByOfficer && (
              <button className="btn btn-sm btn-good" onClick={verifyReg}>Verify</button>
            )}
          </div>
        </div>
        <RegistrationReview rec={rec}/>
      </div>

      {/* Medical card */}
      <div className="card">
        <div className="card-hd">
          <div><h3>Medical fitness</h3><div className="sub">{medical.doctorName ? `${medical.doctorName} · Reg ${medical.doctorRegNo}` : 'No endorsement yet'}</div></div>
          <div className="row" style={{gap:8}}>
            <StatusPill status={medical.status}/>
            {medical.status === 'endorsed' && (
              <button className="btn btn-sm btn-good" onClick={verifyMedical}>Verify</button>
            )}
          </div>
        </div>
        {(medical.files || []).length > 0 && (
          <div className="card-body">
            <div className="row-wrap">{medical.files.map((f,i)=><FileChip key={i} file={f}/>)}</div>
          </div>
        )}
      </div>

      {/* Documents */}
      <div className="card" style={{padding:0}}>
        <div className="card-hd">
          <div><h3>Documents</h3><div className="sub">Verify uploaded items</div></div>
          <div className="pill pill-ghost">{window.DOCUMENTS.filter(d=>documents[d.id]?.status==='verified').length}/{window.DOCUMENTS.length} verified</div>
        </div>
        <table className="tbl">
          <thead><tr><th>Document</th><th>Uploaded files</th><th>Status</th><th>Action</th></tr></thead>
          <tbody>
            {window.DOCUMENTS.filter(d => d.source === 'upload').map(d => {
              const ds = documents[d.id] || { status: 'not-started', files: [] };
              const files = ds.files || [];
              const idTypeLabel = d.requiresIdType && ds.idType
                ? (window.PHOTO_ID_TYPES || []).find(t => t.id === ds.idType)?.label
                : null;
              return (
                <tr key={d.id}>
                  <td style={{verticalAlign:'top'}}>
                    <div className="fw-6 small">{d.title}</div>
                    <div className="tiny muted">{d.desc}</div>
                    {idTypeLabel && (
                      <div className="tiny" style={{marginTop:4,fontWeight:600,color:'var(--accent)'}}>
                        ID type: {idTypeLabel}
                      </div>
                    )}
                    {(ds.messages || []).length > 0 && (
                      <div style={{marginTop:6}}>
                        <MessageThread messages={ds.messages}/>
                      </div>
                    )}
                    {ds.status === 'rejected' && ds.rejectionNote && !(ds.messages || []).length && (
                      <div className="tiny" style={{color:'var(--bad)',marginTop:4}}>Rejected: {ds.rejectionNote}</div>
                    )}
                  </td>
                  <td style={{verticalAlign:'top'}}>
                    {files.length === 0
                      ? <span className="tiny muted">— none —</span>
                      : <div className="row-wrap" style={{gap:6}}>
                          {files.map((f,i) => <FileChip key={i} file={f}/>)}
                        </div>}
                  </td>
                  <td style={{verticalAlign:'top'}}><StatusPill status={ds.status}/></td>
                  <td style={{verticalAlign:'top'}}>
                    {(ds.status === 'uploaded' || ds.status === 'rejected') && files.length > 0 && (
                      <div className="row" style={{gap:5}}>
                        <button className="btn btn-xs btn-good" onClick={()=>verifyDoc(d.id, true)}>Verify</button>
                        <button className="btn btn-xs btn-bad" onClick={()=>{
                          const note = prompt('Reason for rejection?', 'Unclear scan — please re-upload');
                          if (note) verifyDoc(d.id, false, note);
                        }}>Reject</button>
                      </div>
                    )}
                    {ds.status === 'verified' && <span className="tiny muted">✓ {ds.verifiedBy}</span>}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      {/* Tests inline summary */}
      <div className="card">
        <div className="card-hd">
          <div><h3>Tests</h3><div className="sub">{p.testFinalised}/3 finalised</div></div>
          <button className="btn btn-sm" onClick={()=>{
            sessionStorage.setItem('tests.focus', JSON.stringify({studentId:rec.id, tab:'simulator'}));
            ctx.setRoute('tests-fill');
          }}>Open test report →</button>
        </div>
        <div className="card-body">
          <div className="grid g-3">
            {['simulator','rpa','skill'].map(k => (
              <div key={k} style={{padding:'10px 14px',borderRadius:8,background:'var(--bg)',border:'1px solid var(--line)'}}>
                <div className="tiny upper muted fw-6" style={{marginBottom:6}}>{k}</div>
                <StatusPill status={(tests[k] && tests[k].finalised) ? 'verified' : 'draft'}/>
                {(tests[k] && tests[k].examinerName) && <div className="tiny muted" style={{marginTop:6}}>{tests[k].examinerName}</div>}
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Instructor uploads — end-of-course artefacts that ship with
          the DGCA submission packet. */}
      <InstructorUploads rec={rec} ctx={ctx}/>

      {/* DGCA packet builder + per-student verified-files export.
          Provided by instructor-packet.jsx (loaded after officer.jsx). */}
      {window.DGCAPacketCard && <DGCAPacketCard rec={rec} ctx={ctx}/>}
    </div>
  );
}

/* ============================================================
   OFFICER — VERIFICATION QUEUE
   ============================================================ */
function OfficerQueue({ ctx }) {
  const students = window.Store.listStudents();
  const docIds = DOC_IDS();
  const [openItem, setOpenItem] = useState(null);          // 'studentId:kind:docId?'
  const [collapsedStudents, setCollapsedStudents] = useState({}); // sid → bool

  // Build the queue grouped by student so each student gets one card
  // with all of their pending items beneath. Stale schema doc-ids are
  // skipped so the counter and the queue agree exactly. Every read on
  // s.* is defensive — a partially-written legacy doc must not crash
  // the whole queue.
  const groups = [];
  students.forEach(s => {
    const items = [];
    const reg = s.registration || {};
    const med = s.medical || {};
    if (reg.submitted && !reg.verifiedByOfficer) {
      items.push({ kind: 'registration', label: 'Registration form',
        meta: UI.ago(reg.submittedAt) });
    }
    if (med.status === 'endorsed') {
      items.push({ kind: 'medical', label: 'Medical certificate',
        meta: UI.ago(med.endorsedAt) });
    }
    Object.entries(s.documents || {}).forEach(([id, d]) => {
      if (!docIds.has(id) || !d || d.status !== 'uploaded') return;
      const doc = window.DOCUMENTS.find(x => x.id === id);
      if (!doc) return;
      items.push({ kind: 'doc', docId: id, label: doc.title,
        meta: UI.ago(d.files?.[0]?.uploadedAt) });
    });
    if (items.length) groups.push({ student: s, items });
  });

  const totalItems = groups.reduce((n, g) => n + g.items.length, 0);

  const handle = (student, item, ok, note) => {
    const filesToPurge = (!ok && item.kind === 'doc')
      ? (((student.documents || {})[item.docId] || {}).files || []).filter(f => f && f.path)
      : [];
    const text = ok ? '' : (note || 'Please re-upload').trim();

    window.Store.patchStudent(student.id, (r) => {
      if (item.kind === 'registration') {
        r.registration.verifiedByOfficer = true;
      } else if (item.kind === 'medical') {
        r.medical.status = 'verified';
        r.medical.verifiedAt = new Date().toISOString();
        r.medical.verifiedBy = ctx.session.userName;
      } else if (item.kind === 'doc') {
        r.documents = r.documents || {};
        const cell = r.documents[item.docId] || { status: 'not-started', files: [] };
        cell.status = ok ? 'verified' : 'rejected';
        cell.verifiedAt = new Date().toISOString();
        cell.verifiedBy = ctx.session.userName;
        cell.rejectionNote = text;
        if (!ok) {
          cell.files = [];
          cell.messages = [...(cell.messages || []), {
            ts: new Date().toISOString(),
            from: ctx.session.userName, role: 'instructor',
            kind: 'reject', text,
          }];
        }
        r.documents[item.docId] = cell;
      }
      return r;
    });
    filesToPurge.forEach(f => window.Store.deleteFile(f.path));

    window.Store.logAudit(
      ok ? 'Verified (' + item.kind + ')' : 'Rejected (' + item.kind + ')',
      student.profile.fullName,
      item.label + (ok ? '' : ` — ${text || 'Please re-upload'}`));
    ctx.setToast(ok
      ? `${item.label} verified for ${student.profile.fullName}`
      : `${item.label} rejected`);
  };

  const approveAll = (student, items) => {
    if (!confirm(`Verify ALL ${items.length} pending items for ${(student.profile?.fullName || 'this student')}?`)) return;
    // Coalesce every item into a SINGLE patchStudent call so the cache
    // can't be clobbered by an intervening snapshot between sibling
    // sequential patches (each previous handle() builds the next
    // record from cache.students). Audit + toast still fan out per
    // item so the activity log is complete.
    const userName = ctx.session.userName;
    const nowIso = new Date().toISOString();
    window.Store.patchStudent(student.id, (r) => {
      items.forEach((it) => {
        if (it.kind === 'registration') {
          r.registration.verifiedByOfficer = true;
        } else if (it.kind === 'medical') {
          r.medical.status = 'verified';
          r.medical.verifiedAt = nowIso;
          r.medical.verifiedBy = userName;
        } else if (it.kind === 'doc') {
          r.documents = r.documents || {};
          const cell = r.documents[it.docId] || { status: 'not-started', files: [] };
          cell.status = 'verified';
          cell.verifiedAt = nowIso;
          cell.verifiedBy = userName;
          cell.rejectionNote = '';
          r.documents[it.docId] = cell;
        }
      });
      return r;
    });
    items.forEach(it => {
      window.Store.logAudit('Verified (' + it.kind + ')',
        (student.profile?.fullName || 'Student'), it.label);
    });
    ctx.setToast(`${items.length} items verified for ${(student.profile?.fullName || 'Student')}`);
  };

  return (
    <div className="module">
      <ModuleHeader
        crumbs="INSTRUCTOR CONSOLE · QUEUE"
        title="Verification queue"
        sub={totalItems
          ? `${totalItems} item${totalItems===1?'':'s'} across ${groups.length} student${groups.length===1?'':'s'} awaiting your review.`
          : 'Nothing pending. New uploads will appear here.'}
      />
      {totalItems === 0 ? (
        <div className="card">
          <div className="card-body" style={{textAlign:'center',padding:40,color:'var(--muted)'}}>
            <div style={{fontSize:32,marginBottom:8}}>🎉</div>
            <div>You're all caught up.</div>
          </div>
        </div>
      ) : (
        <div className="stack">
          {groups.map(({ student, items }) => {
            const collapsed = !!collapsedStudents[student.id];
            const itemKey = (it) => `${student.id}:${it.kind}:${it.docId || ''}`;
            return (
              <div className="card" key={student.id}>
                <div className="card-hd" style={{flexWrap:'wrap',rowGap:8}}>
                  <button className="row" style={{gap:10,alignItems:'center',background:'transparent',border:0,cursor:'pointer',padding:0,flex:'1 1 200px',textAlign:'left',minWidth:0}}
                    onClick={()=>setCollapsedStudents(c => ({ ...c, [student.id]: !collapsed }))}>
                    <span style={{width:14,display:'inline-block',color:'var(--muted)',flexShrink:0}}>
                      <svg width="10" height="10" viewBox="0 0 10 10" fill="none"
                        style={{transform: collapsed ? '' : 'rotate(90deg)', transition:'transform .15s'}}>
                        <path d="M3.5 2L6.5 5 3.5 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
                      </svg>
                    </span>
                    <div className="brand-mark" style={{width:30,height:30,fontSize:11,flexShrink:0}}>{UI.initials(student.profile?.fullName || '?')}</div>
                    <div style={{minWidth:0,overflow:'hidden'}}>
                      <h3 style={{margin:0,fontSize:14,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{student.profile?.fullName || 'Unnamed'}</h3>
                      <div className="sub" style={{margin:0,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
                        {student.profile?.course || 'Course not set'} · Roll {student.profile?.rollNo || '—'} · Batch {student.profile?.batch || '—'}
                      </div>
                    </div>
                  </button>
                  <div className="row-wrap" style={{gap:8}}>
                    <span className="pill pill-warn"><i className="dotty"/>{items.length} pending</span>
                    <button className="btn btn-sm btn-good" onClick={()=>approveAll(student, items)}>Verify all</button>
                  </div>
                </div>
                {!collapsed && (
                  <table className="tbl">
                    <thead><tr>
                      <th style={{width:24}}></th>
                      <th>Item</th>
                      <th style={{width:120}}>Submitted</th>
                      <th style={{width:90}}>Type</th>
                      <th style={{width:160}}>Action</th>
                    </tr></thead>
                    <tbody>
                      {items.map(it => {
                        const k = itemKey(it);
                        const isOpen = openItem === k;
                        const rej = () => {
                          const note = prompt('Reason for rejection?', 'Unclear scan — please re-upload');
                          if (note) handle(student, it, false, note);
                        };
                        return (
                          <React.Fragment key={k}>
                            <tr style={{cursor:'pointer'}} onClick={()=>setOpenItem(isOpen ? null : k)}>
                              <td style={{color:'var(--muted)'}}>
                                <svg width="10" height="10" viewBox="0 0 10 10" fill="none"
                                  style={{transform:isOpen?'rotate(90deg)':'',transition:'transform .15s'}}>
                                  <path d="M3.5 2L6.5 5 3.5 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
                                </svg>
                              </td>
                              <td><div className="fw-6 small">{it.label}</div>
                                <div className="tiny muted">{isOpen ? 'Hide' : 'Review'} before deciding</div></td>
                              <td className="small muted mono">{it.meta}</td>
                              <td><span className="tag">{it.kind}</span></td>
                              <td onClick={e=>e.stopPropagation()}>
                                <div className="row" style={{gap:5}}>
                                  <button className="btn btn-xs btn-good" onClick={()=>handle(student, it, true)}>Verify</button>
                                  {it.kind === 'doc' && (
                                    <button className="btn btn-xs btn-bad" onClick={rej}>Reject</button>
                                  )}
                                </div>
                              </td>
                            </tr>
                            {isOpen && (
                              <tr>
                                <td colSpan={5} style={{background:'var(--bg)',padding:0}}>
                                  <div style={{padding:'4px 8px 14px'}}>
                                    {it.kind === 'registration' && <RegistrationReview rec={student}/>}
                                    {it.kind === 'medical' && (
                                      <div className="card-body">
                                        <div className="grid g-2" style={{gap:'6px 18px',marginBottom:10}}>
                                          {[['Doctor', student.medical.doctorName],
                                            ['Reg. No.', student.medical.doctorRegNo],
                                            ['Place', student.medical.doctorPlace],
                                            ['Date', student.medical.doctorDate]].map(([k2,v])=>(
                                            <div key={k2} className="row" style={{justifyContent:'space-between',gap:10,
                                              padding:'4px 0',borderBottom:'1px solid var(--line)'}}>
                                              <span className="tiny muted">{k2}</span><span className="small">{v||'—'}</span>
                                            </div>
                                          ))}
                                        </div>
                                        <div className="row-wrap" style={{gap:6}}>
                                          {(student.medical.files||[]).length
                                            ? student.medical.files.map((f,j)=><FileChip key={j} file={f}/>)
                                            : <span className="tiny muted">No file uploaded</span>}
                                        </div>
                                      </div>
                                    )}
                                    {it.kind === 'doc' && (() => {
                                      const ds = (student.documents || {})[it.docId] || {};
                                      const idLabel = ds.idType
                                        ? (window.PHOTO_ID_TYPES || []).find(t => t.id === ds.idType)?.label
                                        : null;
                                      return (
                                        <div className="card-body">
                                          <div className="small muted" style={{marginBottom:8}}>
                                            {window.DOCUMENTS.find(d=>d.id===it.docId)?.desc}
                                          </div>
                                          {idLabel && (
                                            <div className="tiny" style={{marginBottom:8,fontWeight:600,color:'var(--accent)'}}>ID type: {idLabel}</div>
                                          )}
                                          {(ds.messages || []).length > 0 && (
                                            <div style={{marginBottom:10}}>
                                              <MessageThread messages={ds.messages}/>
                                            </div>
                                          )}
                                          <div className="row-wrap" style={{gap:6}}>
                                            {(ds.files || []).length
                                              ? ds.files.map((f,j)=><FileChip key={j} file={f}/>)
                                              : <span className="tiny muted">No file uploaded</span>}
                                          </div>
                                        </div>
                                      );
                                    })()}
                                  </div>
                                </td>
                              </tr>
                            )}
                          </React.Fragment>
                        );
                      })}
                    </tbody>
                  </table>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

/* ============================================================
   OFFICER — TEST REPORT FILLING
   ============================================================ */
function OfficerTests({ ctx }) {
  const students = window.Store.listStudents();
  const [{studentId, tab}, setFocus] = useState(() => {
    const stored = sessionStorage.getItem('tests.focus');
    if (stored) try { return JSON.parse(stored); } catch {}
    return { studentId: students[0]?.id, tab: 'simulator' };
  });
  const rec = students.find(s => s.id === studentId);
  if (!rec) return <div>No student selected</div>;
  const t = rec.tests[tab];

  const set = (patcher) => window.Store.patchStudent(studentId, (r) => { patcher(r.tests[tab]); return r; });
  const setItem = (id, patch) => set(test => { Object.assign(test.items[id], patch); });

  const finalise = () => {
    if (!t.examinerName) return ctx.setToast('Enter examiner name first');
    if (!t.date) return ctx.setToast('Pick the test date');
    set(test => {
      test.finalised = true;
      test.finalisedAt = new Date().toISOString();
      test.examinerSig = test.examinerSig || ' ';
    });
    window.Store.logAudit('Test finalised', (rec.profile?.fullName || 'Student'), `${tab} report`);
    ctx.setToast(`${tab} report finalised — student can now sign`);
  };

  const itemGroups = tab === 'skill'
    ? [{ id:'all', label:'Skill test items', items: window.SKILL_ITEMS }]
    : [
        { id:'pre', label:'Pre-flight', items: window.PROGRESS_ITEMS.preflight },
        { id:'perf', label:'Performance', items: window.PROGRESS_ITEMS.performance },
        { id:'sum', label:'Overall', items: window.PROGRESS_ITEMS.summary },
      ];

  return (
    <div className="module">
      <ModuleHeader
        crumbs="INSTRUCTOR CONSOLE · TEST REPORTS"
        title="Fill a test report"
        sub="Pick a student and test type, then mark each item. The student can sign once you finalise."
      />

      <div className="row-wrap" style={{gap:10, alignItems:'flex-end'}}>
        <div className="field" style={{flex:'1 1 220px', maxWidth:280, minWidth:0}}>
          <label>Student</label>
          <select className="select" value={studentId} onChange={e=>{
            setFocus({studentId:e.target.value, tab});
            sessionStorage.setItem('tests.focus', JSON.stringify({studentId:e.target.value, tab}));
          }}>
            {students.map(s => <option key={s.id} value={s.id}>{s.profile?.fullName} · {s.profile?.rollNo}</option>)}
          </select>
        </div>
        <div className="tabs" style={{marginBottom:0, flex:'1 1 240px', borderBottom:0, minWidth:0}}>
          {['simulator','rpa','skill'].map(o => (
            <button key={o} className={cls('tab', tab===o && 'active')} onClick={()=>setFocus({studentId, tab:o})}>
              {o[0].toUpperCase()+o.slice(1)}
              <span className="count">{(rec.tests && rec.tests[o] && rec.tests[o].finalised) ? '✓' : '·'}</span>
            </button>
          ))}
        </div>
        {!t.finalised ? (
          <button className="btn btn-primary" onClick={finalise}>Finalise report</button>
        ) : (
          <div className="pill pill-good"><i className="dotty"/>Finalised {UI.ago(t.finalisedAt)}</div>
        )}
      </div>

      <div className="grid g-side">
        <div className="stack">
          {/* Examiner header */}
          <div className="card">
            <div className="card-hd"><h3>Examiner & metadata</h3></div>
            <div className="card-body grid g-2">
              <Field label="Examiner name" v={t.examinerName} on={v=>set(test=>test.examinerName=v)}/>
              <Field label="Examiner ID" v={t.examinerId} on={v=>set(test=>test.examinerId=v)}/>
              <Field label="Date" type="date" v={t.date} on={v=>set(test=>test.date=v)}/>
              {tab === 'skill' && <>
                <Field label="Type" as="select" v={t.rpaType} on={v=>set(test=>test.rpaType=v)}
                  opts={['rotary','fixed','other']}/>
                <Field label="Time of day" as="select" v={t.timeOfDay} on={v=>set(test=>test.timeOfDay=v)}
                  opts={['day','night']}/>
                <Field label="Duration" v={t.duration} on={v=>set(test=>test.duration=v)} hint="e.g. 25 min"/>
              </>}
            </div>
          </div>

          {/* Items */}
          {itemGroups.map(g => (
            <div key={g.id} className="card" style={{padding:0}}>
              <div className="card-hd"><h3>{g.label}</h3></div>
              <table className="score-tbl">
                <thead><tr><th style={{width:'55%'}}>Item</th><th style={{width:230}}>Result</th><th>Remark</th></tr></thead>
                <tbody>
                  {g.items.map(it => {
                    const s = t.items[it.id];
                    return (
                      <tr key={it.id}>
                        <td>{it.label}</td>
                        <td>
                          <div className="row" style={{gap:4}}>
                            {['sat','unsat','na'].map(v => (
                              <button key={v} className={cls('opt', s?.status===v && `on-${v}`)}
                                disabled={t.finalised} onClick={()=>setItem(it.id, {status: s?.status===v?null:v})}>
                                {v === 'sat' ? 'Sat.' : v === 'unsat' ? 'Unsat.' : 'N/A'}
                              </button>
                            ))}
                          </div>
                        </td>
                        <td>
                          <input className="input" style={{fontSize:12.5, padding:'4px 8px'}}
                                 disabled={t.finalised}
                                 value={s?.remark || ''} placeholder="—"
                                 onChange={e=>setItem(it.id, {remark: e.target.value})}/>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          ))}

          {/* Comments */}
          <div className="card">
            <div className="card-hd"><h3>Examiner remarks</h3></div>
            <div className="card-body">
              <textarea className="textarea" disabled={t.finalised}
                value={t.comments} onChange={e=>set(test=>test.comments=e.target.value)}
                placeholder="Add any narrative observations for the student…"/>
            </div>
          </div>

          {/* Skill test result */}
          {tab === 'skill' && (
            <div className="card">
              <div className="card-hd"><h3>Final result</h3></div>
              <div className="card-body row" style={{gap:8}}>
                <button className={cls('btn', t.result==='pass' && 'btn-good')} disabled={t.finalised}
                  onClick={()=>set(test=>test.result='pass')}>PASS</button>
                <button className={cls('btn', t.result==='fail' && 'btn-bad')} disabled={t.finalised}
                  onClick={()=>set(test=>test.result='fail')}>FAIL</button>
              </div>
            </div>
          )}
        </div>

        {/* Side */}
        <div className="stack">
          <div className="card">
            <div className="card-hd"><h3>Student</h3></div>
            <div className="card-body">
              <div className="fw-6">{(rec.profile?.fullName || 'Student')}</div>
              <div className="small muted">{rec.profile?.course}</div>
              <div className="small muted mono">Roll {rec.profile?.rollNo}</div>
              <div className="divider"/>
              <div className="tiny upper muted fw-6" style={{marginBottom:6}}>This test</div>
              <div className="small">Status · {t.finalised ? 'Finalised' : 'Draft'}</div>
              <div className="small">Student signature · {t.traineeSig ? 'Received' : 'Pending'}</div>
            </div>
          </div>
          <div className="card">
            <div className="card-hd"><h3>Source</h3></div>
            <div className="card-body">
              <a className="btn btn-sm" href="assets/DGCA-Progress-Skill-Test.pdf" target="_blank" rel="noopener">
                {window.NavIcons.ext} DGCA paper template
              </a>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   OFFICER — MARK ATTENDANCE
   ============================================================ */
function OfficerAttendance({ ctx }) {
  const SCHED = window.ATTENDANCE_SCHEDULE;
  const [sessionId, setSessionId] = useState(SCHED[0].id);
  const session = SCHED.find(s => s.id === sessionId) || SCHED[0];
  const [batchFilter, setBatchFilter] = useState('all');
  const allStudents = window.Store.listStudents();

  // Build the batch dropdown from the students currently in the
  // cohort so unused batches don't clutter it.
  const batchOptions = Array.from(new Set(allStudents.map(s => (s.profile?.batch || '')))).sort();
  const students = batchFilter === 'all'
    ? allStudents
    : allStudents.filter(s => ((s.profile?.batch || '') === batchFilter));

  const mark = (studentId, status) => {
    window.Store.patchStudent(studentId, (r) => {
      r.attendance[sessionId] = { status, markedAt: new Date().toISOString(), markedBy: ctx.session.userName };
      return r;
    });
  };
  const markAll = (status) => {
    if (students.length === 0) {
      ctx.setToast('No students to mark');
      return;
    }
    const scope = batchFilter === 'all'
      ? `all ${students.length} student${students.length===1?'':'s'}`
      : `${students.length} student${students.length===1?'':'s'} in ${batchFilter || 'Unassigned'}`;
    const verb = status === 'pending' ? 'reset' : `marked ${status}`;
    if (!confirm(`${status === 'pending' ? 'Clear' : 'Mark'} attendance for ${scope}?`)) return;
    students.forEach(s => mark(s.id, status));
    window.Store.logAudit(`Bulk attendance · ${status}`,
      session.title, `${students.length} student${students.length===1?'':'s'}${batchFilter!=='all' ? ` · batch ${batchFilter || 'Unassigned'}` : ''}`);
    ctx.setToast(`${scope} ${verb} for this session`);
  };

  const kinds = [
    { id:'ground', label:'Ground' },
    { id:'sim',    label:'Simulator' },
    { id:'flying', label:'Flying' },
  ];
  const present = students.filter(s => (s.attendance || {})[sessionId]?.status === 'present').length;
  const absent  = students.filter(s => (s.attendance || {})[sessionId]?.status === 'absent').length;

  return (
    <div className="module">
      <ModuleHeader
        crumbs="INSTRUCTOR CONSOLE · ATTENDANCE"
        title="Mark attendance"
        sub="Pick a session, then mark each student. Dates are relative to each student's course start date. Saves as you click."
        right={
          <div className="row" style={{gap:8}}>
            <div className="pill pill-good"><i className="dotty"/>{present} present</div>
            <div className="pill pill-bad"><i className="dotty"/>{absent} absent</div>
          </div>
        }
      />

      {/* Visual session picker — grouped by kind, no dropdown */}
      <div className="card">
        <div className="card-hd"><div><h3>Session</h3>
          <div className="sub">Day {session.day} · {session.title} · {session.hours}h</div></div></div>
        <div className="card-body stack-sm" style={{gap:14}}>
          {kinds.map(k => {
            const list = SCHED.filter(s => s.kind === k.id);
            if (!list.length) return null;
            return (
              <div key={k.id}>
                <div className="tiny upper muted fw-6" style={{marginBottom:8}}>{k.label}</div>
                <div className="row-wrap" style={{gap:8}}>
                  {list.map(s => {
                    const on = s.id === sessionId;
                    return (
                      <button key={s.id} aria-pressed={on}
                        onClick={()=>setSessionId(s.id)}
                        className="btn"
                        style={{
                          flexDirection:'column', alignItems:'flex-start', gap:2,
                          minWidth:170, textAlign:'left', padding:'8px 12px',
                          borderColor: on ? 'var(--accent)' : 'var(--line)',
                          background: on ? 'var(--accent-soft)' : 'var(--surface)',
                          boxShadow: on ? 'inset 0 0 0 1px var(--accent)' : 'none',
                        }}>
                        <span className="tiny upper muted fw-6">Day {s.day} · {s.hours}h</span>
                        <span className="small fw-6" style={{whiteSpace:'normal'}}>{s.title}</span>
                      </button>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Roster for the selected session */}
      <div className="card" style={{padding:0}}>
        <div className="card-hd" style={{flexWrap:'wrap',gap:8,rowGap:10}}>
          <div style={{flex:'1 1 240px',minWidth:0}}>
            <h3 style={{whiteSpace:'normal',overflowWrap:'break-word'}}>{session.title}</h3>
            <div className="sub">Day {session.day} · {session.kind} · {session.hours}h</div></div>
          <div className="row" style={{gap:6,flexWrap:'wrap',alignItems:'center'}}>
            <select className="select" style={{padding:'4px 8px',fontSize:12,maxWidth:180}}
              value={batchFilter} onChange={e=>setBatchFilter(e.target.value)}
              title="Limit bulk actions + roster to one batch">
              <option value="all">All batches ({allStudents.length})</option>
              {batchOptions.map(b => (
                <option key={b || '_un'} value={b}>
                  {b || 'Unassigned'} ({allStudents.filter(s => (s.profile?.batch || '') === b).length})
                </option>
              ))}
            </select>
            <button className="btn btn-xs btn-good" onClick={()=>markAll('present')}>All present</button>
            <button className="btn btn-xs btn-bad"  onClick={()=>markAll('absent')}>All absent</button>
            <button className="btn btn-xs"          onClick={()=>markAll('pending')}>Clear</button>
          </div>
        </div>
        <table className="tbl">
          <thead><tr><th>Student</th><th>Roll</th><th>Session date</th><th>Status</th><th>Action</th></tr></thead>
          <tbody>
            {students.length === 0 && (
              <tr><td colSpan={5} className="small muted" style={{padding:'18px',textAlign:'center'}}>No students yet.</td></tr>
            )}
            {students.map(s => {
              const a = (s.attendance || {})[sessionId];
              const d = window.sessionDate(s.profile.startDate, session.day);
              return (
                <tr key={s.id}>
                  <td><div className="row" style={{gap:8}}>
                    <div className="brand-mark" style={{width:24,height:24,fontSize:10}}>{UI.initials(s.profile.fullName)}</div>
                    <span className="small fw-6">{s.profile.fullName}</span>
                  </div></td>
                  <td className="small mono muted">{s.profile.rollNo || s.id}</td>
                  <td className="small mono muted">{d ? UI.fmtDate(d) : <span title="Student has no start date set">Day {session.day}</span>}</td>
                  <td><StatusPill status={a?.status || 'pending'}/></td>
                  <td><div className="row" style={{gap:5}}>
                    <button className={cls('btn btn-xs', a?.status==='present' && 'btn-good')} onClick={()=>mark(s.id,'present')}>P</button>
                    <button className={cls('btn btn-xs', a?.status==='absent' && 'btn-bad')} onClick={()=>mark(s.id,'absent')}>A</button>
                    <button className="btn btn-xs btn-ghost" onClick={()=>mark(s.id,'pending')}>—</button>
                  </div></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* ============================================================
   OFFICER — POST NOTICE
   ============================================================ */
function OfficerNotices({ ctx }) {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const [tag, setTag] = useState('reminder');
  const [busy, setBusy] = useState(false);
  const notices = (ctx.store && Object.values(ctx.store.announcements || {})
    .sort((a, b) => (b.date || '').localeCompare(a.date || ''))) || [];
  const consoleLabel = ctx.session.role === 'manager'
    ? 'ACCOUNTABLE MANAGER · NOTICES' : 'INSTRUCTOR CONSOLE · NOTICES';

  const post = async () => {
    if (!title.trim()) return ctx.setToast('Add a title');
    if (busy) return;
    setBusy(true);
    try {
      await window.Store.addAnnouncement({ title, body, tag, from: ctx.session.userName });
      window.Store.logAudit('Notice posted', title, tag);
      ctx.setToast('Notice posted — visible to all students');
      setTitle(''); setBody('');
    } catch (e) {
      ctx.setToast(e.message || 'Could not post notice');
    } finally {
      setBusy(false);
    }
  };
  const remove = async (id) => {
    if (!confirm('Delete this notice for everyone?')) return;
    try { await window.Store.deleteAnnouncement(id); window.Store.logAudit('Notice deleted', id, ''); ctx.setToast('Notice deleted'); }
    catch (e) { ctx.setToast(e.message || 'Could not delete'); }
  };

  return (
    <div className="module">
      <ModuleHeader
        crumbs={consoleLabel}
        title="Post a notice"
        sub="Broadcasts to every student in real time."
        right={<div className="pill pill-info"><i className="dotty"/>{notices.length} live</div>}
      />
      <div className="grid g-side">
        <div className="card">
          <div className="card-body stack-sm">
            <Field label="Title" v={title} on={setTitle}/>
            <Field label="Type" as="select" v={tag} on={setTag} opts={['schedule','action','reminder','feedback']}/>
            <div className="field">
              <label>Message</label>
              <textarea className="textarea" value={body} onChange={e=>setBody(e.target.value)} style={{minHeight:140}}/>
            </div>
            <div className="row">
              <button className="btn btn-primary" disabled={busy} onClick={post}>
                {busy ? 'Posting…' : 'Post to all students'}
              </button>
            </div>
          </div>
        </div>
        <div className="stack">
          <div className="card">
            <div className="card-hd"><h3>Preview</h3></div>
            <div className={cls('ann', tag)} style={{padding:'14px 16px'}}>
              <div className="stripe"/>
              <div className="body">
                <div className="meta">
                  <div className="who"><b style={{color:'var(--ink)'}}>{ctx.session.userName}</b> · <span className="tag">{tag}</span></div>
                  <div className="when">just now</div>
                </div>
                <h4>{title || 'Title appears here'}</h4>
                <p>{body || 'Your message…'}</p>
              </div>
            </div>
          </div>
          <div className="card" style={{padding:0}}>
            <div className="card-hd"><div><h3>Posted notices</h3><div className="sub">Live — editable by instructors & the manager.</div></div></div>
            <div>
              {notices.length === 0 && <div className="card-body small muted">No notices posted yet.</div>}
              {notices.map(a => (
                <div key={a.id} className={cls('ann', a.tag)} style={{padding:'14px 16px'}}>
                  <div className="stripe"/>
                  <div className="body">
                    <div className="meta">
                      <div className="who"><b style={{color:'var(--ink)'}}>{a.from}</b> · <span className="tag">{a.tag}</span></div>
                      <div className="when">{UI.ago(a.date)}</div>
                    </div>
                    <h4>{a.title}</h4>
                    <p>{a.body}</p>
                    <button className="btn btn-xs" style={{marginTop:8}} onClick={()=>remove(a.id)}>Delete</button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

window.OfficerOverview = OfficerOverview;
window.OfficerRoster = OfficerRoster;
window.OfficerQueue = OfficerQueue;
window.OfficerTests = OfficerTests;
window.OfficerAttendance = OfficerAttendance;
window.OfficerNotices = OfficerNotices;
