// SideJob.jsx — Side Jobs: a flat list of recurring cleaning / maintenance
// items that crew clear on the iPad, a completion Log, and a points
// Leaderboard. The old floor-plan / map (equipment shapes + walls) was
// removed in the v11.11 rework — items are now added one by one.
//
// Tabs:
//   Items        — manager CRUD. Each item = name, how-often (cycle_days),
//                  points, and rich-text instructions (QuillEditor; inline
//                  images upload to the training-media bucket). Reorder +
//                  archive. Shows each item's live status from the last done.
//   Log          — completions grouped by day over a date range (admin delete).
//   Leaderboard  — points per staff over a range, ranked.
//
// Data:
//   side_job_tasks       — the items (useSupaList). label = name, cycle_days,
//                          points, instructions_html, sort_order, archived.
//   side_job_completions — append-only "done" log (manual range fetch). The
//                          iPad inserts (staff_id, the_date, points +
//                          task_label snapshot). Feeds Log + Leaderboard.

const { useState: useSjState, useEffect: useSjEffect, useMemo: useSjMemo } = React;

const SJ_RANK_COLORS = ['#EAB308', '#9CA3AF', '#B45309'];

// ------------------------------------------------------------------
// Item mappers (side_job_tasks). toRow NEVER includes restaurant_id
// (useSupaList injects it on insert) and stamps updated_at.
// ------------------------------------------------------------------
const sjItemFromRow = (r) => ({
  id: r.id,
  name: r.label || '',
  cycleDays: Number(r.cycle_days) || 7,
  points: Number(r.points) || 0,
  instructionsHtml: r.instructions_html || '',
  sortOrder: Number(r.sort_order) || 0,
  archived: !!r.archived,
});
const sjItemToRow = (o) => ({
  id: o.id,
  label: o.name || '',
  cycle_days: Math.max(1, Number(o.cycleDays) || 7),
  points: Math.max(0, Number(o.points) || 0),
  instructions_html: o.instructionsHtml || '',
  sort_order: Number(o.sortOrder) || 0,
  archived: !!o.archived,
  updated_at: new Date().toISOString(),
});

// ------------------------------------------------------------------
// Date + status helpers. daysBetween uses local dates; today = device-local key.
// ------------------------------------------------------------------
const sjDaysBetween = (aISO, bISO) => {
  const [ay, am, ad] = aISO.split('-').map(Number);
  const [by, bm, bd] = bISO.split('-').map(Number);
  const a = new Date(ay, am - 1, ad);
  const b = new Date(by, bm - 1, bd);
  return Math.round((a.getTime() - b.getTime()) / 86400000);
};
const sjDaysAgoLabel = (lastISO, todayKey) => {
  if (!lastISO) return 'never done';
  const n = sjDaysBetween(todayKey, lastISO);
  if (n <= 0) return 'today';
  if (n === 1) return 'yesterday';
  return n + ' days ago';
};

// Status for one item given its last-done ISO date (or null = never done).
// tier: never | ok (green) | soon (amber) | overdue (red, incl. "due today").
const sjItemStatus = (item, lastISO, todayKey) => {
  if (!lastISO) return { tier: 'never', rem: null, ratio: 0, color: '#94A3B8', label: 'Never done' };
  const daysSince = sjDaysBetween(todayKey, lastISO);
  const rem = item.cycleDays - daysSince;
  const ratio = item.cycleDays > 0 ? daysSince / item.cycleDays : 1;
  if (rem < 0) return { tier: 'overdue', rem, ratio, color: '#DC2626', label: (-rem) + 'd overdue' };
  if (rem === 0) return { tier: 'overdue', rem, ratio, color: '#DC2626', label: 'Due today' };
  if (ratio >= 0.85) return { tier: 'soon', rem, ratio, color: '#D97706', label: rem + 'd left' };
  return { tier: 'ok', rem, ratio, color: '#16A34A', label: rem + 'd left' };
};

const sjStaffById = () => {
  const m = {};
  (window.SAMPLE_STAFF || []).forEach(s => { m[s.id] = s; });
  return m;
};

// Latest completion per task_id over a long window → { [task_id]: { date, staffId } }.
// Used to show each item's current status in the portal (the iPad computes its
// own live version). Fetch once on mount + a manual reload.
const useSjLastDone = () => {
  const [map, setMap] = useSjState({});
  const [tick, setTick] = useSjState(0);
  useSjEffect(() => {
    if (!window.supa) return;
    let cancelled = false;
    const start = new Date();
    start.setDate(start.getDate() - 400);
    window.supa.from('side_job_completions')
      .select('task_id, the_date, staff_id, done_at')
      .eq('restaurant_id', window.RESTAURANT_ID)
      .gte('the_date', window.isoDate(start))
      .order('done_at', { ascending: true })
      .then(({ data, error }) => {
        if (cancelled) return;
        if (error) { console.error('side_job last-done fetch failed', error); return; }
        const m = {};
        (data || []).forEach(c => {
          if (!c.task_id) return;
          const cur = m[c.task_id];
          if (!cur || c.the_date >= cur.date) m[c.task_id] = { date: c.the_date, staffId: c.staff_id };
        });
        setMap(m);
      });
    return () => { cancelled = true; };
  }, [tick]);
  return [map, () => setTick(t => t + 1)];
};

// ------------------------------------------------------------------
// Top-level component
// ------------------------------------------------------------------
const SideJob = () => {
  // The daily Log is the main page; Items + Leaderboard live behind a
  // "Manage" button (config subpage) next to the date picker.
  const [mode, setMode] = useSjState('log');            // 'log' | 'config'
  const [date, setDate] = useSjState(() => new Date()); // single day for the main Log
  const dateKey = window.isoDate(date);

  if (mode === 'config') {
    return <SjConfig onBack={() => setMode('log')} />;
  }

  return (
    <div className="portal-page-wide" style={{ maxWidth: 1000 }}>
      <PPageHeader
        title="Side Jobs"
        subtitle="Recurring cleaning + maintenance jobs cleared by the crew, day by day."
        right={<>
          <PBtn variant="secondary" size="sm" icon="settings" onClick={() => setMode('config')}>Manage</PBtn>
          <PDatePicker value={date} onChange={setDate} />
        </>}
        crewLead
        crewLeadDate={dateKey}
      />
      <SjLog date={date} />
    </div>
  );
};

// ------------------------------------------------------------------
// CONFIG SUBPAGE — the job list (Items) + the points Leaderboard, reached
// from the main Log via "Manage". Leaderboard keeps its own From/To range.
// ------------------------------------------------------------------
const SjConfig = ({ onBack }) => {
  const [subtab, setSubtab] = useSjState('items');
  const [items, setItems] = window.useSupaList('side_job_tasks', {
    fromRow: sjItemFromRow, toRow: sjItemToRow, initial: [],
  });
  const [lastDone] = useSjLastDone();

  const subtabs = [
    { id: 'items', label: 'Items' },
    { id: 'leaderboard', label: 'Leaderboard' },
  ];

  return (
    <div className="portal-page-wide" style={{ maxWidth: 1000 }}>
      <PPageHeader
        title="Side Jobs · Manage"
        subtitle="Edit the recurring job list and review the points leaderboard."
        right={<PBtn variant="secondary" size="sm" icon="chevronL" onClick={onBack}>Back to log</PBtn>}
      />
      <div style={{
        display: 'flex', gap: 0,
        borderBottom: '1px solid var(--border-2)',
        marginBottom: 24,
        overflowX: 'auto',
      }}>
        {subtabs.map(t => (
          <button key={t.id} onClick={() => setSubtab(t.id)} style={{
            padding: '10px 16px', background: 'transparent', border: 'none',
            borderBottom: '2px solid ' + (subtab === t.id ? 'var(--fg-1)' : 'transparent'),
            marginBottom: -1, color: subtab === t.id ? 'var(--fg-1)' : 'var(--fg-2)',
            fontSize: 14, fontWeight: subtab === t.id ? 600 : 500, cursor: 'pointer', whiteSpace: 'nowrap',
          }}>{t.label}</button>
        ))}
      </div>

      {subtab === 'items' && <SjItems items={items} setItems={setItems} lastDone={lastDone} />}
      {subtab === 'leaderboard' && <SjLeaderboard />}
    </div>
  );
};

// ------------------------------------------------------------------
// ITEMS TAB — add/edit items one by one, reorder, archive, live status.
// ------------------------------------------------------------------
const SjItems = ({ items, setItems, lastDone }) => {
  const todayKey = window.isoDate(new Date());
  const staffById = useSjMemo(sjStaffById, []);
  const [editing, setEditing] = useSjState(null);
  const [showArchived, setShowArchived] = useSjState(false);

  const live = useSjMemo(
    () => items.filter(i => !i.archived).sort((a, b) => (a.sortOrder - b.sortOrder) || a.name.localeCompare(b.name)),
    [items]
  );
  const archived = useSjMemo(() => items.filter(i => i.archived), [items]);

  const addItem = () => {
    const maxSort = items.reduce((m, i) => Math.max(m, i.sortOrder || 0), 0);
    setEditing({ id: crypto.randomUUID(), name: '', cycleDays: 7, points: 1, instructionsHtml: '', sortOrder: maxSort + 1, archived: false, __isNew: true });
  };
  const saveItem = (obj) => {
    const isNew = obj.__isNew;
    const clean = { id: obj.id, name: (obj.name || '').trim() || 'Untitled item', cycleDays: obj.cycleDays, points: obj.points, instructionsHtml: obj.instructionsHtml, sortOrder: obj.sortOrder, archived: !!obj.archived };
    setItems(list => isNew ? [...list, clean] : list.map(x => x.id === clean.id ? clean : x));
    setEditing(null);
  };
  const setArchived = (id, val) => setItems(list => list.map(x => x.id === id ? { ...x, archived: val } : x));
  const deleteItem = (id) => {
    if (!window.confirm('Delete this item? Its past completions stay in the log + leaderboard.')) return;
    setItems(list => list.filter(x => x.id !== id));
    setEditing(null);
  };
  const move = (idx, dir) => {
    const j = idx + dir;
    if (j < 0 || j >= live.length) return;
    const a = live[idx], b = live[j];
    setItems(list => list.map(x => x.id === a.id ? { ...x, sortOrder: b.sortOrder } : (x.id === b.id ? { ...x, sortOrder: a.sortOrder } : x)));
  };

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 13, color: 'var(--fg-3)' }}>
          {live.length} active item{live.length === 1 ? '' : 's'}
          {archived.length > 0 && (
            <button onClick={() => setShowArchived(v => !v)} style={{ marginLeft: 10, background: 'none', border: 'none', color: 'var(--fg-2)', cursor: 'pointer', fontSize: 13, textDecoration: 'underline' }}>
              {showArchived ? 'Hide' : 'Show'} archived ({archived.length})
            </button>
          )}
        </div>
        <PBtn variant="primary" icon="plus" onClick={addItem}>Add item</PBtn>
      </div>

      {live.length === 0 && !showArchived ? (
        <div className="portal-empty">
          <div style={{ fontSize: 14, color: 'var(--fg-2)' }}>No side jobs yet. Click <b>Add item</b> to create your first one.</div>
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {live.map((it, idx) => (
            <SjItemRow key={it.id} item={it} idx={idx} count={live.length} status={sjItemStatus(it, (lastDone[it.id] || {}).date || null, todayKey)}
              lastStaff={lastDone[it.id] ? staffById[lastDone[it.id].staffId] : null}
              lastDate={lastDone[it.id] ? lastDone[it.id].date : null} todayKey={todayKey}
              onEdit={() => setEditing({ ...it })} onMove={move} onArchive={() => setArchived(it.id, true)} />
          ))}
          {showArchived && archived.map(it => (
            <div key={it.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: '#FAFAF9', border: '1px solid var(--border-2)', borderRadius: 12, padding: '12px 16px', opacity: 0.75 }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 15, fontWeight: 500, color: 'var(--fg-2)', textDecoration: 'line-through' }}>{it.name}</div>
                <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 2 }}>every {it.cycleDays} days · {it.points} pts</div>
              </div>
              <PBtn size="xs" onClick={() => setArchived(it.id, false)}>Restore</PBtn>
              <button onClick={() => deleteItem(it.id)} title="Delete forever" style={sjIconBtn}><PIcon name="trash" size={14} color="var(--danger)" /></button>
            </div>
          ))}
        </div>
      )}

      {editing && (
        <SjItemEditor item={editing} onSave={saveItem} onDelete={() => deleteItem(editing.id)} onClose={() => setEditing(null)} />
      )}
    </div>
  );
};

const SjItemRow = ({ item, idx, count, status, lastStaff, lastDate, todayKey, onEdit, onMove, onArchive }) => {
  const w = Math.min(status.ratio, 1) * 100;
  return (
    <div style={{ display: 'flex', alignItems: 'stretch', gap: 12, background: '#FFFFFF', border: '1px solid var(--border-2)', borderRadius: 12, padding: '12px 14px' }}>
      <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 2 }}>
        <button onClick={() => onMove(idx, -1)} disabled={idx === 0} title="Move up" style={{ ...sjIconBtn, width: 26, height: 22, opacity: idx === 0 ? 0.3 : 1 }}><PIcon name="chevron-up" size={13} color="var(--fg-3)" /></button>
        <button onClick={() => onMove(idx, 1)} disabled={idx === count - 1} title="Move down" style={{ ...sjIconBtn, width: 26, height: 22, opacity: idx === count - 1 ? 0.3 : 1 }}><PIcon name="chevron-down" size={13} color="var(--fg-3)" /></button>
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 15.5, fontWeight: 500, color: 'var(--fg-1)' }}>{item.name}</span>
          <span style={{ fontSize: 11.5, fontWeight: 600, color: status.color, background: status.color + '18', padding: '2px 8px', borderRadius: 999 }}>{status.label}</span>
        </div>
        <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 3 }}>every {item.cycleDays} days · {item.points} pts</div>
        <div style={{ marginTop: 8, height: 6, background: 'var(--bg-sunken)', borderRadius: 999, overflow: 'hidden' }}>
          <div style={{ width: w + '%', height: '100%', background: status.color, borderRadius: 999 }} />
        </div>
        <div style={{ marginTop: 8, fontSize: 12, color: 'var(--fg-2)', display: 'flex', alignItems: 'center', gap: 6 }}>
          {lastStaff ? (
            <>Done by <PAvatar staff={lastStaff} size={18} /> <span style={{ fontWeight: 500 }}>{(lastStaff.name || '').split(' ')[0]}</span> · {sjDaysAgoLabel(lastDate, todayKey)}</>
          ) : (
            <span style={{ color: 'var(--fg-3)' }}>Not cleared yet</span>
          )}
        </div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-end' }}>
        <PBtn size="xs" icon="edit" onClick={onEdit}>Edit</PBtn>
        <button onClick={onArchive} title="Archive" style={sjIconBtn}><PIcon name="archive" size={14} color="var(--fg-3)" /></button>
      </div>
    </div>
  );
};

// Add/edit one item. Local draft; commits once on Save.
const SjItemEditor = ({ item, onSave, onDelete, onClose }) => {
  const [draft, setDraft] = useSjState(item);
  const upd = (patch) => setDraft(d => ({ ...d, ...patch }));
  return (
    <PModal open onClose={onClose} title={item.__isNew ? 'New item' : 'Edit item'} width={620} footer={
      <>
        {!item.__isNew && <PBtn variant="danger" icon="trash" onClick={onDelete}>Delete</PBtn>}
        <div style={{ flex: 1 }} />
        <PBtn variant="ghost" onClick={onClose}>Cancel</PBtn>
        <PBtn variant="primary" onClick={() => onSave(draft)}>Save</PBtn>
      </>
    }>
      <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 18 }}>
        <PField label="Name">
          <input className="portal-input" value={draft.name} placeholder="e.g. Hood filters"
            onChange={e => upd({ name: e.target.value })} style={{ height: 36 }} autoFocus />
        </PField>

        <div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
          <PField label="How often">
            <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'var(--fg-2)' }}>
              every
              <input type="number" min="1" value={draft.cycleDays}
                onChange={e => upd({ cycleDays: Math.max(1, parseInt(e.target.value, 10) || 1) })} style={sjNumInput} />
              days
            </label>
          </PField>
          <PField label="Points">
            <input type="number" min="0" value={draft.points}
              onChange={e => upd({ points: Math.max(0, parseInt(e.target.value, 10) || 0) })} style={sjNumInput} />
          </PField>
        </div>

        <PField label="Instructions" subtitle="What to do — rich text. Drop or paste photos inline; the crew sees these on the iPad.">
          {window.QuillEditor
            ? <window.QuillEditor value={draft.instructionsHtml} onChange={(html) => upd({ instructionsHtml: html })} placeholder="Explain how to do this job…" />
            : <textarea className="portal-input" value={draft.instructionsHtml} onChange={e => upd({ instructionsHtml: e.target.value })} style={{ minHeight: 120, width: '100%' }} />}
        </PField>
      </div>
    </PModal>
  );
};

// ------------------------------------------------------------------
// Shared: a From/To date-range picker pair + a range-fetch hook.
// ------------------------------------------------------------------
const SjRangePicker = ({ from, to, setFrom, setTo }) => (
  <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 18, flexWrap: 'wrap' }}>
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <span style={{ fontSize: 11.5, fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>From</span>
      <PDatePicker value={from} onChange={setFrom} />
    </div>
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <span style={{ fontSize: 11.5, fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>To</span>
      <PDatePicker value={to} onChange={setTo} />
    </div>
  </div>
);

const useSjCompletions = (from, to) => {
  const [rows, setRows] = useSjState([]);
  const [loading, setLoading] = useSjState(true);
  const [tick, setTick] = useSjState(0);
  useSjEffect(() => {
    if (!window.supa) { setLoading(false); return; }
    let cancelled = false;
    setLoading(true);
    window.supa.from('side_job_completions')
      .select('*')
      .eq('restaurant_id', window.RESTAURANT_ID)
      .gte('the_date', window.isoDate(from))
      .lte('the_date', window.isoDate(to))
      .order('done_at', { ascending: false })
      .then(({ data, error }) => {
        if (cancelled) return;
        if (error) console.error('side_job_completions range fetch failed', error);
        setRows(data || []);
        setLoading(false);
      });
    return () => { cancelled = true; };
  }, [window.isoDate(from), window.isoDate(to), tick]);
  return { rows, loading, reload: () => setTick(t => t + 1) };
};

// ------------------------------------------------------------------
// LOG TAB — completions grouped by day, KPI strip, admin row delete.
// ------------------------------------------------------------------
const SjLog = ({ date }) => {
  // Single selected day (the page header owns the date picker).
  const { rows } = useSjCompletions(date, date);
  const staffById = useSjMemo(sjStaffById, []);

  const [removed, setRemoved] = useSjState(new Set());
  const visible = useSjMemo(() => rows.filter(r => !removed.has(r.id)), [rows, removed]);

  const deleteRow = async (row) => {
    if (!window.confirm('Delete this completion? Points will be removed from the leaderboard.')) return;
    setRemoved(prev => { const n = new Set(prev); n.add(row.id); return n; });
    if (!window.supa) return;
    const { error } = await window.supa.from('side_job_completions').delete().eq('id', row.id);
    if (error) { console.error('side_job_completions delete failed', error); setRemoved(prev => { const n = new Set(prev); n.delete(row.id); return n; }); }
  };

  const stats = useSjMemo(() => {
    const crew = new Set(); let points = 0;
    visible.forEach(r => { crew.add(r.staff_id); points += (Number(r.points) || 0); });
    return { count: visible.length, points, crew: crew.size };
  }, [visible]);

  return (
    <div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12, marginBottom: 18 }}>
        <SjKpi label="Completions" value={stats.count} />
        <SjKpi label="Points" value={stats.points} />
        <SjKpi label="Distinct crew" value={stats.crew} />
      </div>
      {visible.length === 0 ? (
        <div className="portal-empty"><div style={{ fontSize: 14, color: 'var(--fg-2)' }}>No side jobs logged on this day.</div></div>
      ) : (
        <div style={{ background: '#FFFFFF', border: '1px solid var(--border-2)', borderRadius: 10, overflow: 'hidden' }}>
          <table style={{ borderCollapse: 'collapse', width: '100%' }}>
            <thead>
              <tr style={{ borderBottom: '1px solid var(--border-2)', background: '#FAFAF9' }}>
                {['Time', 'Person', 'Item', 'Points', ''].map((h, i) => (
                  <th key={i} style={{ textAlign: i === 3 ? 'right' : 'left', padding: '10px 14px', fontSize: 11, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.06em', whiteSpace: 'nowrap' }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {visible.map(r => {
                const s = staffById[r.staff_id];
                return (
                  <tr key={r.id} style={{ borderBottom: '1px solid var(--border-2)' }}>
                    <td style={{ padding: '10px 14px', fontSize: 13, color: 'var(--fg-2)', fontFamily: 'var(--font-num)', whiteSpace: 'nowrap' }}>{r.done_at ? new Date(r.done_at).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) : '—'}</td>
                    <td style={{ padding: '10px 14px' }}>
                      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                        {s ? <PAvatar staff={s} size={26} /> : null}
                        <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--fg-1)' }}>{s ? s.name : r.staff_id}</span>
                      </span>
                    </td>
                    <td style={{ padding: '10px 14px', fontSize: 13, color: 'var(--fg-1)', fontWeight: 500 }}>{r.task_label || 'Item'}</td>
                    <td style={{ padding: '10px 14px', textAlign: 'right', fontSize: 14, fontWeight: 600, fontFamily: 'var(--font-num)', color: 'var(--fg-1)' }}>{Number(r.points) || 0}</td>
                    <td style={{ padding: '10px 14px', textAlign: 'right', whiteSpace: 'nowrap' }}>
                      <button onClick={() => deleteRow(r)} title="Delete completion" style={sjIconBtn}><PIcon name="trash" size={14} color="var(--danger)" /></button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
};

const SjKpi = ({ label, value }) => (
  <div style={{ background: '#FFFFFF', border: '1px solid var(--border-2)', borderRadius: 10, padding: '12px 14px' }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>{label}</div>
    <div style={{ fontSize: 22, fontWeight: 600, color: 'var(--fg-1)', fontFamily: 'var(--font-num)', marginTop: 4, letterSpacing: '-0.02em' }}>{value}</div>
  </div>
);

// ------------------------------------------------------------------
// LEADERBOARD TAB — points summed per staff over a range, ranked rows.
// ------------------------------------------------------------------
const SjLeaderboard = () => {
  const [to, setTo] = useSjState(() => new Date());
  const [from, setFrom] = useSjState(() => { const d = new Date(); d.setDate(d.getDate() - 13); return d; });
  const { rows: completions, loading } = useSjCompletions(from, to);
  const staffById = useSjMemo(sjStaffById, []);

  const rows = useSjMemo(() => {
    const map = {};
    completions.forEach(c => {
      const m = (map[c.staff_id] = map[c.staff_id] || { id: c.staff_id, points: 0, jobs: 0 });
      m.points += (Number(c.points) || 0); m.jobs += 1;
    });
    return Object.values(map).sort((a, b) => (b.points - a.points) || (b.jobs - a.jobs));
  }, [completions]);

  const maxPts = rows.reduce((m, r) => Math.max(m, r.points), 0) || 1;
  const totalPts = rows.reduce((s, r) => s + r.points, 0);

  return (
    <div>
      <SjRangePicker from={from} to={to} setFrom={setFrom} setTo={setTo} />
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12, marginBottom: 18, maxWidth: 520 }}>
        <SjKpi label="Points awarded" value={totalPts} />
        <SjKpi label="Crew ranked" value={rows.length} />
      </div>
      {loading ? (
        <div className="portal-empty"><div style={{ fontSize: 14, color: 'var(--fg-2)' }}>Loading…</div></div>
      ) : rows.length === 0 ? (
        <div className="portal-empty"><div style={{ fontSize: 14, color: 'var(--fg-2)' }}>No points earned in this range.</div></div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {rows.map((r, i) => {
            const s = staffById[r.id];
            const name = s ? s.name : r.id;
            return (
              <div key={r.id} style={{ display: 'flex', alignItems: 'center', gap: 14, background: '#FFFFFF', border: '1px solid var(--border-2)', borderRadius: 10, padding: '12px 16px' }}>
                <div style={{ width: 26, textAlign: 'center', fontFamily: 'var(--font-num)', fontWeight: 700, fontSize: 15, color: SJ_RANK_COLORS[i] || 'var(--fg-3)' }}>{i + 1}</div>
                {s ? <PAvatar staff={s} size={36} /> : <div style={{ width: 36 }} />}
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-1)' }}>{name}{s && s.role === 'Admin' && <span style={{ fontSize: 11, color: 'var(--fg-3)', fontWeight: 500 }}> · Admin</span>}</div>
                  <div style={{ marginTop: 6, height: 7, background: 'var(--bg-sunken)', borderRadius: 999, overflow: 'hidden' }}>
                    <div style={{ width: Math.round((r.points / maxPts) * 100) + '%', height: '100%', background: i === 0 ? '#EAB308' : 'var(--fg-1)', borderRadius: 999 }} />
                  </div>
                  <div style={{ marginTop: 6, fontSize: 11.5, color: 'var(--fg-3)' }}>{r.jobs} job{r.jobs === 1 ? '' : 's'} done</div>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <div style={{ fontSize: 22, fontWeight: 700, fontFamily: 'var(--font-num)', color: 'var(--fg-1)', lineHeight: 1 }}>{r.points}</div>
                  <div style={{ fontSize: 10.5, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.05em', marginTop: 2 }}>points</div>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};

const sjNumInput = {
  width: 68, height: 34, padding: '0 8px',
  border: '1px solid var(--border-1)', borderRadius: 7,
  fontSize: 13.5, fontWeight: 600, fontFamily: 'var(--font-num)',
  textAlign: 'right', color: 'var(--fg-1)', background: '#FFFFFF',
};
const sjIconBtn = {
  width: 30, height: 30, borderRadius: 7,
  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
  border: '1px solid var(--border-2)', background: '#FFFFFF', cursor: 'pointer', flexShrink: 0,
};

window.SideJob = SideJob;
