// CrewLeadScorecard.jsx — Dashboard page that turns the owner's old bonus
// spreadsheet into a live report (v12.00).
//
// Scoring model (per Crew Lead, per 14-day pay period):
//   start at 100%
//   − for each DAILY ITEM (Prep / Checklist / Temp Log / Side Job / Store
//     Phone): (number of FAILED days that this lead WORKED) × item %.
//     Failed days come from `scorecard_daily` — the nightly snapshot written
//     by scorecard_snapshot_range() just after midnight Chicago, so later
//     edits can't rewrite bonus history. Shared: every lead who clocked in
//     that day takes the same hit.
//   − ATTENDANCE (individual): late days × %. A clock-in even 1 minute after
//     the scheduled start counts (owner's rule; data from `square-hours`).
//   ± CUSTOM items (shared): manually-entered count × signed % (e.g. 5-star
//     review +1, 4-star −5). Counts are stored per period.
//   raw can exceed 100% or go negative; FINAL is clamped to 0..100 (raw is
//   still displayed so overperformance is visible).
//   bonus = hours × $rate/hr × final%.  Hours = Square CLOSED timecards net
//   of unpaid breaks, attributed to the local day the shift started.
//
// Config lives in restaurants.scorecard_config (rate_per_hour + per-item %),
// edited in the gear panel here. Custom item definitions are
// scorecard_custom_items; per-period counts are scorecard_custom_counts.

const SC_DAILY_ITEMS = [
  { key: 'prep',        label: 'Prep',        hint: 'primary items not completed' },
  { key: 'checklist',   label: 'Checklist',   hint: 'any due item unchecked' },
  { key: 'temp_log',    label: 'Temp Log',    hint: 'a checkpoint not finalized' },
  { key: 'side_job',    label: 'Side Job',    hint: 'a due item not done' },
  { key: 'store_phone', label: 'Store Phone', hint: 'a missed call not returned in time' },
];
// Every line an override can be tied to (v20.03): the daily items plus the
// late-day line. `attendance` is the config key the late-day % already uses.
const SC_ITEM_LABEL = Object.fromEntries(SC_DAILY_ITEMS.map(it => [it.key, it.label]));
SC_ITEM_LABEL.attendance = 'Days late to work';

// An override tied to a deduction (v20.03). `myAdj` is one lead's override
// rows; `key` is the line ('prep' … 'attendance'); `entries` are that line's
// days for this lead (charged, waived, off-shift); `eachPct` is the deduction
// per day. Returns the entries annotated with the overrides that sit on
// their day, plus the line's adjusted result — original result + the tied
// overrides — and an effective record that drops a day whose overrides add
// up to the full deduction. The score math elsewhere is untouched: an
// override is still one signed number in the final sum. This only decides
// where it is SHOWN — struck through under the deduction it undoes — and
// keeps it out of the foot of the table, which now carries store-wide
// overrides only.
//
// An override tied to a day this lead was never charged for (the day was
// re-scored after it was written, or it was tied by hand to the wrong line)
// comes back as an `orphan`: still visible, still editable, never hidden.
const scTieOverrides = (myAdj, key, entries, record, result, eachPct) => {
  const tied = myAdj.filter(r => r.item_key === key);
  const byDay = {};
  tied.forEach(r => { (byDay[r.event_date] = byDay[r.event_date] || []).push(r); });
  const sum = (rows) => rows.reduce((t, r) => t + (Number(r.pct) || 0), 0);
  const out = entries.map(e => {
    const os = byDay[e.day] || [];
    if (!os.length) return e;
    const opct = sum(os);
    return { ...e, overrides: os, overridePct: opct, cancelled: !!e.charged && eachPct > 0 && opct >= eachPct };
  });
  const orphans = Object.keys(byDay).sort()
    .filter(day => !entries.some(e => e.day === day))
    .map(day => ({ day, label: scFmtDay(day), note: 'no deduction on this day', muted: true,
                   overrides: byDay[day], overridePct: sum(byDay[day]), orphan: true }));
  const tiedPct = sum(tied);
  return {
    entries: out, orphans, tied, tiedPct,
    adjusted: result + tiedPct,
    effRecord: record - out.filter(e => e.cancelled).length,
  };
};

// One crew lead's report, laid out for a single sheet of US Letter (v16.16).
//
// Printed via the browser rather than a PDF library: the portal has no build
// step, native print gives selectable text and real page breaks, and "Save as
// PDF" is in every print dialog. Sizing is `@page { size: letter }` plus
// physical units here — pt/in, not rem — because the screen's root font size
// has nothing to do with a sheet of paper.
//
// Colour is kept to the two verdict tones. These get printed on whatever is in
// the office printer, so the layout has to survive greyscale: every red/green
// figure also carries its sign, and no meaning rests on colour alone.
const ScReport = ({ lead, cfg, activeCustom, rateFor, periodLabel, generatedOn }) => {
  const P = { fontSize: 9.5, padding: '4pt 6pt', borderBottom: '0.5pt solid #D4D4D8' };
  const num = { ...P, textAlign: 'right', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' };
  // U+2212, matching the −N% in the Each column. JS's own '-' is a
  // hyphen and reads visibly shorter next to it in print.
  const sign = (n) => (n > 0 ? '+' : n < 0 ? '−' : '')
    + Math.abs(Math.round(n * 100) / 100) + '%';
  const tone = (n) => (n > 0 ? '#166534' : n < 0 ? '#B91C1C' : '#3F3F46');
  const struck = { textDecoration: 'line-through', color: '#A1A1AA', fontWeight: 400 };

  // The days behind a deduction sit directly under the line they explain
  // (v19.14), so "Checklist −20%" is followed by the two days that made it.
  //
  // Three kinds of day, and the sheet has to keep them apart at a glance
  // (v20.03): a CHARGED day (plain), an OVERRIDDEN day (struck through, with
  // the manager's reason and the amount given back on the next line), and a
  // day that was never charged — off-shift, waived — which is pulled out to
  // "Not counted" at the foot so it can't be mistaken for a deduction.
  const chargedFor = (row) => (row.entries || []).filter(e => !e.muted);
  const notCounted = [];
  SC_DAILY_ITEMS.forEach((it, i) => {
    const muted = ((lead.items[i] || {}).entries || []).filter(e => e.muted);
    if (muted.length) notCounted.push({ label: it.label, entries: muted });
  });
  const overrideTotal = lead.adjPct || 0;
  const tiedCount = lead.tiedCount || 0;
  const general = (lead.adjList || []).filter(a => !a.item_key);

  // A day behind a deduction, indented under the line it explains. An
  // overridden day is struck through, and the override that undid it follows
  // on its own line so the reason reads as the reason for THAT day.
  const subRows = (key, e) => {
    const rows = [
      <tr key={key}>
        <td colSpan={3} style={{
          ...P, paddingLeft: '20pt', paddingTop: '2pt', paddingBottom: '2pt',
          fontSize: 8.5, color: '#52525B', borderBottom: e.overrides ? 'none' : '0.5pt solid #E4E4E7',
        }}>
          <span style={{ fontVariantNumeric: 'tabular-nums', color: '#3F3F46', fontWeight: 600, ...(e.cancelled ? struck : {}) }}>{e.label}</span>
          {e.note ? <span style={e.cancelled ? struck : {}}>{'  ·  ' + e.note}</span> : null}
        </td>
        <td style={{ ...num, paddingTop: '2pt', paddingBottom: '2pt', fontSize: 8.5, borderBottom: e.overrides ? 'none' : '0.5pt solid #E4E4E7',
          color: '#71717A', ...(e.cancelled ? struck : {}) }}>
          {e.charged && e.eachPct ? sign(-e.eachPct) : ''}
        </td>
      </tr>,
    ];
    (e.overrides || []).forEach((o, k) => rows.push(
      <tr key={key + '-o' + k}>
        <td colSpan={3} style={{
          ...P, paddingLeft: '30pt', paddingTop: '1pt', paddingBottom: '3pt',
          fontSize: 8.5, color: '#166534', borderBottom: k === e.overrides.length - 1 ? '0.5pt solid #E4E4E7' : 'none',
        }}>
          <span style={{ fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', fontSize: 7.5 }}>Overridden</span>
          <span style={{ color: '#3F3F46' }}>{'  ' + (o.reason ? o.reason : 'no reason recorded')}</span>
        </td>
        <td style={{ ...num, paddingTop: '1pt', paddingBottom: '3pt', fontSize: 8.5, color: tone(o.pct), fontWeight: 700,
          borderBottom: k === e.overrides.length - 1 ? '0.5pt solid #E4E4E7' : 'none' }}>
          {sign(o.pct)}
        </td>
      </tr>,
    ));
    return rows;
  };

  // One scored line: original figure struck through when overrides moved it,
  // the figure that actually counted beside it.
  const resultCell = (row, extra) => {
    const moved = (row.tiedPct || 0) !== 0;
    const shown = moved ? row.adjusted : row.result;
    return (
      <td style={{ ...num, ...extra, color: tone(shown), fontWeight: shown !== 0 ? 700 : 400 }}>
        {moved && <span style={{ ...struck, marginRight: '4pt' }}>{row.result === 0 ? '0%' : sign(row.result)}</span>}
        {shown === 0 ? '0%' : sign(shown)}
      </td>
    );
  };
  const countCell = (row, extra) => (
    <td style={{ ...num, ...extra }}>
      {row.effRecord != null && row.effRecord !== row.record
        ? <><span style={{ ...struck, marginRight: '4pt' }}>{row.record}</span>{row.effRecord}</>
        : row.record}
    </td>
  );
  const dayRows = (row, prefix) => {
    const days = chargedFor(row).map(e => ({ ...e, eachPct: row.eachPct }));
    const all = days.concat(row.orphans || []);
    return all.flatMap((e, k) => subRows(prefix + k, e));
  };

  return (
    <section className="sc-sheet" style={{
      fontFamily: 'Helvetica, Arial, sans-serif', color: '#18181B',
      width: '100%', boxSizing: 'border-box',
    }}>
      <header style={{ borderBottom: '1.5pt solid #18181B', paddingBottom: '6pt', marginBottom: '10pt' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
          <div>
            <div style={{ fontSize: 17, fontWeight: 700, letterSpacing: '-0.01em' }}>{lead.name}</div>
            <div style={{ fontSize: 10, color: '#52525B', marginTop: '2pt' }}>
              Crew Lead bonus report · {periodLabel}
            </div>
          </div>
          <div style={{ textAlign: 'right' }}>
            <div style={{ fontSize: 26, fontWeight: 700, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>
              {Math.round(lead.finalPct)}%
            </div>
            <div style={{ fontSize: 8.5, color: '#52525B', textTransform: 'uppercase', letterSpacing: '0.06em', marginTop: '2pt' }}>
              Final result
            </div>
          </div>
        </div>
      </header>

      <div style={{ display: 'flex', gap: '10pt', marginBottom: '12pt' }}>
        {[
          ['Hours', lead.effHours.toFixed(2) + (lead.fixedWeekly != null ? ' (fixed)' : '')],
          ['Rate', scMoney(rateFor(lead.staff_id)) + '/hr'],
          ['Potential', scMoney(lead.potential)],
          ['Bonus earned', scMoney(lead.bonus)],
        ].map(([k, v], i) => (
          <div key={k} style={{
            flex: 1, border: '0.5pt solid #D4D4D8', borderRadius: '3pt', padding: '5pt 7pt',
            background: i === 3 ? '#F0FDF4' : '#FAFAFA',
          }}>
            <div style={{ fontSize: 7.5, color: '#52525B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>{k}</div>
            <div style={{ fontSize: 12.5, fontWeight: 700, marginTop: '1pt', fontVariantNumeric: 'tabular-nums',
              color: i === 3 ? '#166534' : '#18181B' }}>{v}</div>
          </div>
        ))}
      </div>

      {/* The one sentence that makes the sheet self-explanatory (v20.03):
          what struck-through means, before the reader meets one. */}
      {overrideTotal !== 0 && (
        <div style={{
          fontSize: 8.5, lineHeight: 1.5, color: '#3F3F46', marginBottom: '8pt',
          padding: '5pt 7pt', border: '0.5pt solid #D4D4D8', borderRadius: '3pt', background: '#FAFAFA',
        }}>
          Without any override this period would have scored{' '}
          <b style={{ fontVariantNumeric: 'tabular-nums' }}>{Math.round(Math.max(0, Math.min(100, lead.beforeOverrides)))}%</b>.
          {' '}A manager overrode <b>{sign(overrideTotal)}</b>
          {tiedCount ? <> — {tiedCount === 1 ? 'one deduction' : tiedCount + ' deductions'} below {tiedCount === 1 ? 'is' : 'are'} <span style={{ textDecoration: 'line-through' }}>struck through</span> with the reason given back to you</> : null}
          {general.length ? <>{tiedCount ? ', and ' : ' — '}{general.length === 1 ? 'one store-wide adjustment' : general.length + ' store-wide adjustments'} {general.length === 1 ? 'is' : 'are'} listed near the foot of the table</> : null}.
        </div>
      )}

      <div style={{ fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em',
        color: '#52525B', marginBottom: '3pt' }}>How the score was worked out</div>
      <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '12pt' }}>
        <thead>
          <tr>
            <th style={{ ...P, textAlign: 'left', fontSize: 8, color: '#52525B', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Item</th>
            <th style={{ ...num, fontSize: 8, color: '#52525B', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Each</th>
            <th style={{ ...num, fontSize: 8, color: '#52525B', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Count</th>
            <th style={{ ...num, fontSize: 8, color: '#52525B', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Result</th>
          </tr>
        </thead>
        <tbody>
          <tr><td style={P}>Starting score</td><td style={num}>—</td><td style={num}>—</td>
            <td style={{ ...num, fontWeight: 700 }}>100%</td></tr>
          {SC_DAILY_ITEMS.map((it, i) => {
            const row = { ...lead.items[i], eachPct: Number(cfg.items[it.key]) || 0 };
            const days = chargedFor(row).length + (row.orphans || []).length;
            const nb = { borderBottom: days ? 'none' : P.borderBottom };
            return (
              <React.Fragment key={it.key}>
                <tr>
                  <td style={{ ...P, ...nb }}>
                    {it.label}<span style={{ color: '#71717A' }}> — {it.hint}</span>
                  </td>
                  <td style={{ ...num, ...nb }}>−{row.eachPct}%</td>
                  {countCell(row, nb)}
                  {resultCell(row, nb)}
                </tr>
                {dayRows(row, it.key)}
              </React.Fragment>
            );
          })}
          {(() => {
            const row = { ...lead.att, eachPct: Number(cfg.items.attendance) || 0 };
            const days = chargedFor(row).length + (row.orphans || []).length;
            const nb = { borderBottom: days ? 'none' : P.borderBottom };
            return (
              <React.Fragment>
                <tr>
                  <td style={{ ...P, ...nb }}>
                    Days late to work<span style={{ color: '#71717A' }}> — clock-in after scheduled start</span>
                  </td>
                  <td style={{ ...num, ...nb }}>−{row.eachPct}%</td>
                  {countCell(row, nb)}
                  {resultCell(row, nb)}
                </tr>
                {dayRows(row, 'late')}
              </React.Fragment>
            );
          })()}
          {activeCustom.map((ci, i) => {
            const c = lead.custom[i];
            return (
              <tr key={ci.id}>
                <td style={P}>{ci.label}</td>
                <td style={num}>{sign(ci.pct)}</td>
                <td style={num}>{c.record}</td>
                <td style={{ ...num, color: tone(c.result), fontWeight: c.result !== 0 ? 700 : 400 }}>
                  {c.result === 0 ? '0%' : sign(c.result)}
                </td>
              </tr>
            );
          })}
          {/* Store-wide adjustments only (v20.03): an override tied to a
              deduction already appears struck through under that line, so
              listing it again here would count it twice on the page. */}
          {general.map(a => (
            <tr key={a.id}>
              <td style={P}>
                Store-wide adjustment
                {a.event_date && <span style={{ color: '#18181B' }}>{' · ' + scDayLabel(a.event_date)}</span>}
                <span style={{ color: '#52525B' }}>
                  {' — '}{a.reason ? a.reason : 'no reason recorded'}
                </span>
              </td>
              <td style={num}>—</td>
              <td style={num}>—</td>
              <td style={{ ...num, color: tone(a.pct), fontWeight: 700 }}>{sign(a.pct)}</td>
            </tr>
          ))}
          <tr>
            <td style={{ ...P, fontWeight: 700, borderBottom: 'none', borderTop: '1pt solid #18181B' }}>Final result</td>
            <td style={{ ...num, borderBottom: 'none', borderTop: '1pt solid #18181B' }} />
            <td style={{ ...num, borderBottom: 'none', borderTop: '1pt solid #18181B' }} />
            <td style={{ ...num, fontWeight: 700, fontSize: 11, borderBottom: 'none', borderTop: '1pt solid #18181B' }}>
              {Math.round(lead.finalPct)}%
            </td>
          </tr>
        </tbody>
      </table>

      {/* Everything that did NOT affect the score, at the very bottom (v19.14).
          It has to be here — the report must answer "why isn't this counted
          against me?" — but above the deductions it competed with them for
          attention, and reading a day you were forgiven for in the middle of
          the days you were charged for is exactly backwards. */}
      {notCounted.length > 0 && (
        <>
          <div style={{ fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em',
            color: '#52525B', marginBottom: '3pt' }}>
            Not counted
            <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0, color: '#71717A' }}>
              {' — never charged: off shift, or waived by the labor rule; none of this changed the score'}
            </span>
          </div>
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <tbody>
              {notCounted.map(sec => (
                <React.Fragment key={sec.label}>
                  <tr><td colSpan={2} style={{ ...P, fontWeight: 700, background: '#F4F4F5', fontSize: 9 }}>
                    {sec.label}
                  </td></tr>
                  {sec.entries.map((e, i) => (
                    <tr key={sec.label + i}>
                      <td style={{ ...P, width: '28%', color: '#71717A', fontVariantNumeric: 'tabular-nums' }}>{e.label}</td>
                      <td style={{ ...P, color: '#71717A', fontStyle: 'italic' }}>{e.note}</td>
                    </tr>
                  ))}
                </React.Fragment>
              ))}
            </tbody>
          </table>
        </>
      )}

      <footer style={{ marginTop: '12pt', paddingTop: '5pt', borderTop: '0.5pt solid #D4D4D8',
        fontSize: 7.5, color: '#71717A', lineHeight: 1.45 }}>
        Daily items are shared across everyone who worked that day, and only count on days you were clocked in.
        The days under each line are the ones that made up that deduction. A struck-through day was charged and
        then overridden by a manager; the line beneath it is the reason and the amount given back. Checklist,
        Temp Log and Store Phone charge only for checkpoints that fell inside your shift — anything under
        “Not counted” was looked at and never charged. Generated {generatedOn}.
      </footer>
    </section>
  );
};

const SC_DEFAULT_CONFIG = {
  rate_per_hour: 2,
  hide_inactive: false,
  // Prep waiver: a failed Prep day is forgiven when that day's labor cost was
  // under this % of sales (busy day, short-handed). Sales = gross less
  // third-party commission, discounts NOT deducted (v19.09). 0 = off.
  labor_waiver_pct: 22,
  items: { prep: 10, checklist: 20, temp_log: 10, side_job: 10, store_phone: 10, attendance: 20 },
};

// The PDF's default filename. Browsers take it from document.title, so the
// print effect swaps the title for the duration and puts it back after
// (v16.17). "07-26-26 Omar Bohorquez 89" — period start, full name, score.
// No '%': some file pickers are funny about it, and it reads fine without.
const scPdfName = (periodStart, fullName, finalPct) => {
  const mm = String(periodStart.getMonth() + 1).padStart(2, '0');
  const dd = String(periodStart.getDate()).padStart(2, '0');
  const yy = String(periodStart.getFullYear()).slice(-2);
  // Strip anything a filesystem would object to; keep it otherwise verbatim.
  const safe = String(fullName || '').replace(/[\\/:*?"<>|]/g, '').trim();
  return `${mm}-${dd}-${yy} ${safe} ${Math.round(finalPct)}`;
};

const scMoney = (n) => '$' + (Math.round(n * 100) / 100).toFixed(2);
const scPct = (n) => (Math.round(n * 100) / 100) + '%';
// 'YYYY-MM-DD' → 'Aug 14'. Split field-by-field rather than `new Date(str)`,
// which parses a bare date as UTC and prints the day before anywhere west of
// it — the same bug that put the wrong incident date on discipline memos.
const scDayLabel = (ymd) => {
  const [y, m, d] = String(ymd || '').split('-').map(Number);
  if (!y || !m || !d) return '';
  return new Date(y, m - 1, d).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
};

// 'YYYY-MM-DD' -> "Mon, Jul 13". Parsed by hand — new Date('YYYY-MM-DD') is
// UTC and drifts a day in Central time (HANDBOOK §9).
const scFmtDay = (iso) => {
  const [y, m, d] = iso.split('-').map(Number);
  return new Date(y, m - 1, d).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
};
const scFmtTime = (iso) => {
  try {
    return new Date(iso).toLocaleTimeString('en-US', {
      timeZone: 'America/Chicago', hour: 'numeric', minute: '2-digit',
    }).replace(' ', '').toLowerCase();
  } catch (e) { return ''; }
};

// Instant -> Chicago wall-clock 'YYYY-MM-DD HH:MM' for lexical comparison
// against a checkpoint's day + time. Comparing in wall-clock space is DST-safe
// and handles overnight shifts (the interval simply spans two dates).
const scChiWallFmt = new Intl.DateTimeFormat('en-CA', {
  timeZone: 'America/Chicago',
  year: 'numeric', month: '2-digit', day: '2-digit',
  hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
});
const scChiWall = (iso) => {
  try {
    const parts = scChiWallFmt.formatToParts(new Date(iso));
    const g = (t) => (parts.find(p => p.type === t) || {}).value || '00';
    return `${g('year')}-${g('month')}-${g('day')} ${g('hour')}:${g('minute')}`;
  } catch (e) { return ''; }
};
// 'HH:MM' (24h) -> '9:00pm'
const scFmt12 = (hm) => {
  if (!hm) return '';
  const [h, m] = hm.split(':').map(Number);
  return (((h + 11) % 12) + 1) + ':' + String(m || 0).padStart(2, '0') + (h >= 12 ? 'pm' : 'am');
};

// Did this lead work on `day`, and when? Reads the same `worked_days` /
// `intervals` the daily items are judged from, so the override picker offers
// exactly the people the scorecard would hold responsible for that date —
// and shows their clock times, which is what decides responsibility for the
// time-of-day items (Checklist checkpoints, a missed call).
const scWorkedOn = (lead, day) => {
  if (!day) return null;
  if (!(lead.worked_days || []).includes(day)) return null;
  const spans = (lead.intervals || [])
    .map(iv => [scChiWall(iv.start_at), iv.end_at ? scChiWall(iv.end_at) : ''])
    .filter(([a, b]) => a && (a.slice(0, 10) === day || (b && b.slice(0, 10) === day)))
    // No end_at means they are still on the clock — say so rather than
    // dropping the row and leaving the chip looking like it has no shift.
    .map(([a, b]) => b ? scFmt12(a.slice(11)) + '–' + scFmt12(b.slice(11))
                       : 'from ' + scFmt12(a.slice(11)));
  return { spans };
};

// ⓘ + hover card listing the days behind a Record number. Entries:
// { label, note?, muted?, charged?, day?, overrides?, cancelled? } — muted
// renders the row grey (waived / off shift); an entry carrying `overrides`
// is struck through when they cancel the whole deduction and shows each
// override, with its reason, beneath it (v20.03). `extra` are orphans:
// overrides tied to a day with no deduction, kept visible so they can be
// fixed.
const ScInfoTip = ({ entries, extra, onOverride, onEditOverride, onDeleteOverride }) => {
  const [open, setOpen] = useState(false);
  // The card holds a button now, so reaching it with the mouse has to be
  // reliable (v19.13). Two things were fighting that:
  //
  //   1. The card sat at `top: 18` under a 13px icon, leaving ~5px of dead
  //      space. mouseleave fires the instant the pointer is over neither, so
  //      moving toward the card closed it. The gap is now PADDING on a
  //      transparent positioning wrapper, which makes it part of the
  //      hoverable area instead of a hole in it.
  //   2. A diagonal exit can still clip a corner for one frame. Closing is
  //      deferred, and re-entering cancels it.
  const closeTimer = useRef(null);
  const cancelClose = () => { if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; } };
  const openNow = () => { cancelClose(); setOpen(true); };
  const closeSoon = () => { cancelClose(); closeTimer.current = setTimeout(() => setOpen(false), 140); };
  useEffect(() => cancelClose, []);
  const all = (entries || []).concat(extra || []);
  if (all.length === 0) return null;
  const hasOverride = all.some(e => e.overrides && e.overrides.length);
  const iconBtn = (name, title, onClick) => (
    <button onClick={(ev) => { ev.stopPropagation(); onClick(); }} title={title}
      style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-3)', padding: 2, display: 'inline-flex' }}>
      <PIcon name={name} size={12} />
    </button>
  );
  return (
    <span
      style={{ position: 'relative', display: 'inline-flex', marginLeft: 6, verticalAlign: 'middle' }}
      onMouseEnter={openNow}
      onMouseLeave={closeSoon}
    >
      <PIcon name="info" size={13} color={hasOverride ? '#2D6A2A' : 'var(--fg-3)'} style={{ cursor: 'help' }} />
      {open && (
        <div
          onMouseEnter={openNow}
          onMouseLeave={closeSoon}
          style={{
            position: 'absolute', top: '100%', right: -6, zIndex: 500,
            paddingTop: 6, // the bridge — transparent, but hoverable
          }}
        >
        <div style={{
          width: 340, background: '#FFFFFF',
          border: '1px solid var(--border-1)', borderRadius: 10,
          boxShadow: '0 10px 30px rgba(0,0,0,0.16)',
          padding: '8px 12px', textAlign: 'left',
        }}>
          {/* One day per block (v20.04): the date and the Override button on
              the first line, the note on its own line beneath so a long
              checkpoint list wraps instead of running out of the card. */}
          {all.map((e, i) => {
            const cancelled = !!e.cancelled;
            const muted = !!e.muted;
            const strike = cancelled ? { textDecoration: 'line-through', color: 'var(--fg-3)' } : {};
            return (
              <div key={i} style={{
                fontSize: 12, lineHeight: 1.4, padding: '6px 0',
                fontFamily: 'var(--font-ui)', fontWeight: 400, textTransform: 'none', letterSpacing: 0,
                color: muted ? 'var(--fg-3)' : 'var(--fg-1)',
                borderBottom: i < all.length - 1 ? '1px solid var(--border-2)' : 'none',
              }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
                  <span style={{ fontWeight: 600, whiteSpace: 'nowrap', ...strike }}>{e.label}</span>
                  {/* An override starts from the deduction it undoes (v19.12).
                      Only on rows that actually cost something and aren't
                      already fully overridden — a waived, off-shift or
                      cancelled row has nothing left to override. */}
                  {onOverride && e.charged && e.day && !cancelled ? (
                    <button
                      onClick={(ev) => { ev.stopPropagation(); onOverride(e); }}
                      title={'Add an override for ' + e.label}
                      style={{
                        border: '1px solid var(--border-2)', background: 'var(--bg-sunken)',
                        borderRadius: 999, padding: '1px 8px', cursor: 'pointer', flexShrink: 0,
                        fontSize: 11, fontWeight: 600, color: 'var(--fg-2)', whiteSpace: 'nowrap',
                      }}>Override</button>
                  ) : (cancelled ? (
                    <span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#2D6A2A', flexShrink: 0 }}>Overridden</span>
                  ) : null)}
                </div>
                {e.note && (
                  <div style={{ marginTop: 2, whiteSpace: 'normal', overflowWrap: 'anywhere',
                    color: muted ? (e.orphan ? '#9A3412' : '#2D6A2A') : 'var(--fg-2)', ...strike }}>{e.note}</div>
                )}
                {/* The overrides that sit on this day, each with its reason and
                    its own edit/remove — this card is where a tied override
                    lives now, not the foot of the table (v20.03). */}
                {(e.overrides || []).map(o => (
                  <div key={o.batch_id} style={{
                    display: 'flex', alignItems: 'flex-start', gap: 8, marginTop: 4,
                    padding: '5px 8px', borderRadius: 7, background: '#EEF3EF',
                    fontSize: 11.5, lineHeight: 1.4, whiteSpace: 'normal',
                  }}>
                    <span style={{ fontWeight: 700, color: '#2D6A2A', fontFamily: 'var(--font-num)', whiteSpace: 'nowrap' }}>
                      {(Number(o.pct) || 0) > 0 ? '+' : ''}{scPct(Number(o.pct) || 0)}
                    </span>
                    <span style={{ flex: 1, minWidth: 0, color: 'var(--fg-1)' }}>
                      {o.reason || <span style={{ color: 'var(--fg-3)', fontStyle: 'italic' }}>No reason recorded</span>}
                    </span>
                    {onEditOverride && iconBtn('edit', 'Edit this override', () => onEditOverride(o))}
                    {onDeleteOverride && iconBtn('trash', 'Remove this override', () => onDeleteOverride(o))}
                  </div>
                ))}
              </div>
            );
          })}
        </div>
        </div>
      )}
    </span>
  );
};

// Custom-item mappers (useSupaList)
const scCustomItemFromRow = (r) => ({
  id: r.id, label: r.label, pct: Number(r.pct) || 0,
  sortOrder: Number(r.sort_order) || 0, archived: !!r.archived,
});
const scCustomItemToRow = (o) => ({
  id: o.id, label: o.label, pct: Number(o.pct) || 0,
  sort_order: Number(o.sortOrder) || 0, archived: !!o.archived,
});

const CrewLeadScorecard = ({ payAnchor }) => {
  const [periodOffset, setPeriodOffset] = useState(0);
  const [daily, setDaily] = useState(null);      // scorecard_daily rows for period
  const [square, setSquare] = useState(null);    // square-hours response
  const [labor, setLabor] = useState({});        // day -> {labor_cents, sales_cents, pct} for failed-prep days
  // 'YYYY-MM-DD HH:MM' (Chicago) -> caller number, for every missed call in
  // the period (v21.01). The snapshot only stores a miss's TIME; the number
  // is what lets someone find the call in Quo, so it is looked up here.
  const [missedByWall, setMissedByWall] = useState({});
  const [config, setConfig] = useState(null);    // restaurants.scorecard_config
  const [counts, setCounts] = useState({});      // custom counts: itemId -> count
  // Every override row for this period (v19.06). One row per (override ×
  // person); rows sharing a batch_id are one override the manager created.
  const [adjRows, setAdjRows] = useState([]);
  const [adjEditing, setAdjEditing] = useState(null); // draft override, or null
  // staff_id -> { bonus_per_hour } (v16.00). The bonus is per person now,
  // set on Crew, so the scorecard has to look each lead up rather than
  // multiplying everyone by one global rate.
  const [staffById, setStaffById] = useState({});
  // staff_id of the report being printed, or null. Set it, let React paint,
  // then window.print(); cleared on afterprint (v16.16).
  const [printLead, setPrintLead] = useState(null);
  // Captured at click time rather than derived in the effect: the effect has
  // to sit above this component's `if (!period) return` guard, which is well
  // above where `leads` (and so finalPct) is computed.
  const [printName, setPrintName] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [toast, setToast] = useState('');
  const refreshedFor = useRef(null); // period startISO we already auto-snapshotted

  const [customItems, setCustomItems] = window.useSupaList('scorecard_custom_items', {
    fromRow: scCustomItemFromRow, toRow: scCustomItemToRow, initial: () => [],
  });

  // Period derivation must tolerate a not-yet-loaded anchor WITHOUT an early
  // return — every hook below has to run on every render or React throws
  // "Rendered more hooks than during the previous render" the moment the
  // anchor arrives (App loads it async). The early return lives after the
  // hooks, guarded by `period` being null.
  const refDate = new Date();
  refDate.setDate(refDate.getDate() + periodOffset * 14);
  const period = (payAnchor && window.getPayPeriod) ? window.getPayPeriod(payAnchor, refDate) : null;
  const startISO = period ? isoDate(period.start) : null;
  const endISO = period ? isoDate(period.end) : null;
  const todayISO = isoDate(new Date());

  const load = async () => {
    if (!window.supa || !startISO) return;
    setLoading(true); setError('');
    try {
      const fetchDaily = () => window.supa.from('scorecard_daily').select('the_date, item_key, passed, detail')
        .eq('restaurant_id', window.RESTAURANT_ID)
        .gte('the_date', startISO).lte('the_date', endISO);

      const [dRes, cfgRes, cntRes, adjRes] = await Promise.all([
        fetchDaily(),
        window.supa.from('restaurants').select('scorecard_config').eq('id', window.RESTAURANT_ID).single(),
        window.supa.from('scorecard_custom_counts').select('item_id, count')
          .eq('restaurant_id', window.RESTAURANT_ID).eq('period_start', startISO),
        window.supa.from('scorecard_adjustments').select('id, staff_id, pct, reason, batch_id, created_at, event_date, item_key')
          .eq('restaurant_id', window.RESTAURANT_ID).eq('period_start', startISO)
          .order('created_at', { ascending: true }),
      ]);
      if (dRes.error) throw dRes.error;
      const cfg = { ...SC_DEFAULT_CONFIG, ...(cfgRes.data?.scorecard_config || {}) };
      cfg.items = { ...SC_DEFAULT_CONFIG.items, ...(cfg.items || {}) };
      setConfig(cfg);
      setCounts(Object.fromEntries((cntRes.data || []).map(r => [r.item_id, r.count])));
      setAdjRows((adjRes.data || []).map(r => ({ ...r, pct: Number(r.pct) || 0 })));
      const { data: staffRows } = await window.supa.from('staff')
        .select('id, name, bonus_per_hour, fixed_hours_per_week').eq('restaurant_id', window.RESTAURANT_ID);
      setStaffById(Object.fromEntries((staffRows || []).map(r => [r.id, r])));

      // Missed calls for the period, keyed by Chicago day + HH:MM — the same
      // wall-clock the snapshot's `calls[].time` is written in. A day's UTC
      // window is widened by a day either side; the key does the precise cut.
      const { data: missedRows } = await window.supa.from('phone_calls')
        .select('started_at, external_number')
        .eq('restaurant_id', window.RESTAURANT_ID)
        .eq('direction', 'incoming').eq('answered', false)
        .gte('started_at', startISO + 'T00:00:00Z')
        .lte('started_at', endISO + 'T23:59:59Z')
        .limit(1000);
      const byWall = {};
      (missedRows || []).forEach(r => {
        const w = scChiWall(r.started_at);
        if (w && r.external_number && !byWall[w]) byWall[w] = r.external_number;
      });
      setMissedByWall(byWall);

      // Auto-heal: if any fully-elapsed day is missing snapshot rows (e.g. the
      // page is viewed on a period predating the nightly job), backfill once.
      let dRows = dRes.data || [];
      const elapsed = period.days.map(isoDate).filter(d => d < todayISO);
      const expected = elapsed.length * SC_DAILY_ITEMS.length;
      if (dRows.length < expected && refreshedFor.current !== startISO) {
        refreshedFor.current = startISO;
        const { error: rpcErr } = await window.supa.rpc('scorecard_refresh', { d0: startISO, d1: endISO });
        if (!rpcErr) {
          const again = await fetchDaily();
          if (again.data) dRows = again.data;
        }
      }
      setDaily(dRows);

      // Square runs AFTER the snapshots so we can ask for labor % on exactly
      // the failed-Prep days (the waiver rule only needs those).
      const failedPrep = dRows.filter(r => r.item_key === 'prep' && !r.passed).map(r => r.the_date).sort();
      const sqRes = await window.supa.functions.invoke('square-hours', {
        body: { start_date: startISO, end_date: endISO, labor_days: failedPrep.slice(0, 16) },
      });
      if (sqRes.error || sqRes.data?.error) {
        setSquare({ crew_leads: [], failed: true });
        setLabor({});
      } else {
        setSquare(sqRes.data);
        setLabor(sqRes.data.labor || {});
      }
    } catch (e) {
      setError(e.message || String(e));
    }
    setLoading(false);
  };
  useEffect(() => { load(); }, [startISO]);

  // Print has to happen AFTER the report is in the DOM, so it hangs off the
  // state change rather than the click. afterprint fires on both Print and
  // Cancel, which is what clears the sheet either way.
  useEffect(() => {
    if (!printLead) return;
    const prevTitle = document.title;
    if (printName) document.title = printName;
    const done = () => setPrintLead(null);
    window.addEventListener('afterprint', done);
    const t = setTimeout(() => window.print(), 150);
    return () => {
      clearTimeout(t);
      window.removeEventListener('afterprint', done);
      document.title = prevTitle;
    };
  }, [printLead, printName]);


  if (!period) {
    return (
      <div className="portal-page-wide" style={{ maxWidth: 1280 }}>
        <div style={{ padding: 40, color: 'var(--fg-3)', fontSize: 13 }}>Loading pay-period settings…</div>
      </div>
    );
  }

  const saveConfig = async (next) => {
    setConfig(next);
    const { error: err } = await window.supa.from('restaurants')
      .update({ scorecard_config: next }).eq('id', window.RESTAURANT_ID);
    setToast(err ? 'Save failed — ' + err.message : 'Scorecard settings saved');
  };

  // One override, applied to one or more people, with the reason it exists.
  // Written as a row per person sharing a batch_id — see the migration note.
  // A reason is required by the UI, not by the column: the seven rows that
  // pre-date this have none, and back-filling an invented reason onto
  // someone's bonus record would be worse than showing it blank.
  const saveOverride = async (draft) => {
    const pct = Math.max(-100, Math.min(100, Number(draft.pct) || 0));
    const reason = (draft.reason || '').trim();
    const ids = draft.staffIds || [];
    if (!ids.length || !reason) return false;
    const batch = draft.batchId || crypto.randomUUID();
    const now = new Date().toISOString();
    const rows = ids.map(sid => ({
      id: batch + ':' + sid,
      restaurant_id: window.RESTAURANT_ID,
      staff_id: sid, period_start: startISO, pct, reason,
      event_date: draft.eventDate || null,
      // Tied to a deduction (v20.03): the line + the day above name exactly
      // one charged entry in that person's column. NULL = store-wide.
      item_key: draft.itemKey || null,
      batch_id: batch,
      created_by_email: window.currentAdminEmail || null,
      updated_at: now,
    }));
    // An edit can drop people, so the batch is cleared before it is rewritten
    // rather than upserted over — otherwise someone removed from an override
    // keeps their row and silently keeps the adjustment.
    if (draft.batchId) {
      const { error: delErr } = await window.supa.from('scorecard_adjustments').delete().eq('batch_id', batch);
      if (delErr) { setToast('Override save failed — ' + delErr.message); return false; }
    }
    const { error: err } = await window.supa.from('scorecard_adjustments').insert(rows);
    if (err) { setToast('Override save failed — ' + err.message); return false; }
    setAdjRows(prev => prev.filter(r => r.batch_id !== batch)
      .concat(rows.map(r => ({ id: r.id, staff_id: r.staff_id, pct, reason, batch_id: batch,
                               event_date: draft.eventDate || null, item_key: draft.itemKey || null, created_at: now }))));
    setToast(draft.batchId ? 'Override updated' : 'Override added');
    return true;
  };

  const deleteOverride = async (batchId, reason) => {
    if (!window.confirm('Remove this override?' + (reason ? '\n\n“' + reason + '”' : '') +
      '\n\nIt stops counting toward the bonus and disappears from everyone\u2019s report.')) return;
    const { error: err } = await window.supa.from('scorecard_adjustments').delete().eq('batch_id', batchId);
    if (err) { setToast('Delete failed — ' + err.message); return; }
    setAdjRows(prev => prev.filter(r => r.batch_id !== batchId));
    setToast('Override removed');
  };

  const saveCount = async (itemId, count) => {
    const n = Math.max(0, Math.round(Number(count) || 0));
    setCounts(c => ({ ...c, [itemId]: n }));
    const { error: err } = await window.supa.from('scorecard_custom_counts').upsert({
      id: itemId + ':' + startISO,
      restaurant_id: window.RESTAURANT_ID,
      item_id: itemId, period_start: startISO, count: n,
      updated_at: new Date().toISOString(),
    }, { onConflict: 'restaurant_id,item_id,period_start' });
    if (err) setToast('Count save failed — ' + err.message);
  };

  // Persist a lead's fixed weekly hours. Empty clears it back to clocked
  // Square hours, which is the default and what everyone else uses.
  const saveFixedHours = async (staffId, raw) => {
    const txt = String(raw == null ? '' : raw).trim();
    const val = txt === '' ? null : Math.min(168, Math.max(0.25, Number(txt)));
    if (txt !== '' && !Number.isFinite(val)) return;
    const prev = staffById[staffId];
    setStaffById(m => ({ ...m, [staffId]: { ...(m[staffId] || {}), fixed_hours_per_week: val } }));
    // .select() so an RLS refusal surfaces as an error rather than a zero-row
    // success that looks like it saved (§9, v16.05).
    const { error } = await window.supa.from('staff')
      .update({ fixed_hours_per_week: val }).eq('id', staffId).select();
    if (error) {
      setStaffById(m => ({ ...m, [staffId]: prev }));
      setToast('Save failed: ' + error.message);
      return;
    }
    setToast(val == null ? 'Back to clocked hours' : 'Fixed at ' + val + ' hrs/week');
  };

  // ---------- scoring ----------
  const cfg = config || SC_DEFAULT_CONFIG;
  // v16.00 — the bonus rate is per person (staff.bonus_per_hour, edited on
  // Crew), not one global number. cfg.rate_per_hour survives only as the
  // fallback for a staff row that predates the column.
  const fallbackRate = Number(cfg.rate_per_hour) || 0;
  const rateFor = (staffId) => {
    const row = (staffById && staffById[staffId]) || null;
    return row && row.bonus_per_hour != null ? Number(row.bonus_per_hour) : fallbackRate;
  };
  // v16.13 — salaried leads can have their hours STATED instead of measured.
  // Omar clocks only his scheduled shifts and does admin outside them without
  // punching, so his closed timecards understate the week he's paid for.
  // fixed_hours_per_week null (everyone else) keeps the Square figure.
  //
  // Scaled by the period length rather than assumed fortnightly: the pay
  // period comes from payAnchor and a caller could hand this a different
  // span. 50/wk over a 14-day period is 100, and it does NOT pro-rate down
  // mid-period — a fixed figure that crept up day by day would be neither
  // fixed nor the clocked truth, just a third number nobody could check.
  const periodWeeks = (period && period.days && period.days.length)
    ? period.days.length / 7 : 2;
  const fixedWeeklyFor = (staffId) => {
    const row = (staffById && staffById[staffId]) || null;
    const v = row && row.fixed_hours_per_week;
    return v == null || v === '' ? null : Number(v);
  };
  const hoursFor = (lead) => {
    const fw = fixedWeeklyFor(lead.staff_id);
    return fw == null ? (lead.hours || 0) : fw * periodWeeks;
  };
  const failedByItem = {}; // item_key -> Set('YYYY-MM-DD')
  // Per-checkpoint breakdowns. store_phone joined in v14.06 (detail.calls =
  // the local times of each unreturned miss, for the on-shift test below).
  const detailByDay = { checklist: {}, temp_log: {}, store_phone: {} };
  (daily || []).forEach(r => {
    if (!r.passed) (failedByItem[r.item_key] = failedByItem[r.item_key] || new Set()).add(r.the_date);
    if (detailByDay[r.item_key]) {
      detailByDay[r.item_key][r.the_date] = r.detail || {};
    }
  });

  // Prep labor waiver (v16.00). The decision is made in SQL, inside
  // scorecard_snapshot_range, against the stored sales_daily / labor_daily
  // figures and the per-weekday target on Sales vs. Labor. This used to be
  // recomputed here from a separate live Square call against one global %,
  // which meant the portal and the snapshot could disagree about the same day.
  // Now the snapshot is the single answer and this only reads it: a waived day
  // is passed=true carrying detail.waived, so it never reaches failedByItem —
  // we pull it back out purely to keep showing it, greyed, in the tooltip.
  const prepDetail = {};   // 'YYYY-MM-DD' -> detail blob for prep
  (daily || []).forEach(r => { if (r.item_key === 'prep') prepDetail[r.the_date] = r.detail || {}; });
  // Only days the waiver actually RESCUED. detail.waived is true whenever the
  // day hit its labor target, including days where prep was finished anyway —
  // listing those as "waived" would imply a save that never happened.
  const prepWaivedDays = Object.keys(prepDetail).filter(d => {
    const x = prepDetail[d];
    return x.waived && Number(x.done) < Number(x.due);
  });
  const activeCustom = customItems.filter(ci => !ci.archived)
    .sort((a, b) => (a.sortOrder - b.sortOrder) || a.label.localeCompare(b.label));

  const leads = ((square && square.crew_leads) || [])
    // Optional display filter — a deactivated lead who worked the period still
    // exists in the data (and their bonus math is unaffected); this just hides
    // their column when the owner doesn't want to see departed staff.
    .filter(l => !(cfg.hide_inactive && l.active === false))
    .map(l => {
    const worked = new Set(l.worked_days || []);
    // Shift windows in Chicago wall-clock space, for the checkpoint rule.
    const wallIntervals = (l.intervals || []).map(iv => [scChiWall(iv.start_at), scChiWall(iv.end_at)]);
    const onShift = (day, hm) => {
      if (!hm) return true; // checkpoint with no time — fall back to day-based charge
      const t = day + ' ' + hm;
      return wallIntervals.some(([a, b]) => a && b && a <= t && t <= b);
    };

    const items = SC_DAILY_ITEMS.map(it => {
      const pct = Number(cfg.items[it.key]) || 0;

      // Checklist + Temp Log charge per CHECKPOINT: a lead is only responsible
      // for checkpoints whose time-of-day fell inside their clock-in/out. A day
      // still counts at most once; off-shift misses show greyed in the tooltip.
      if (it.key === 'checklist' || it.key === 'temp_log') {
        const entries = [];
        let record = 0;
        for (const d of [...(failedByItem[it.key] || [])].sort()) {
          if (!worked.has(d)) continue;
          const det = detailByDay[it.key][d] || {};
          const cps = (it.key === 'checklist' ? (det.groups || []) : (det.checkpoints || []))
            .filter(cp => cp.failed)
            .map(cp => ({ name: cp.name || cp.label, time: cp.time }));
          const mine = cps.filter(cp => onShift(d, cp.time));
          const off = cps.filter(cp => !onShift(d, cp.time));
          if (mine.length) {
            record += 1;
            entries.push({
              day: d, charged: true,
              label: scFmtDay(d),
              note: mine.map(cp => `${cp.name} (${scFmt12(cp.time)})`).join(', ') + ' missed',
              // Which checkpoints charged this lead (v20.04): a tied override
              // reaches only the leads that share one, because two leads
              // can both fail Checklist on the same day for different hours.
              cps: mine.map(cp => cp.name + '@' + cp.time),
            });
          } else if (off.length) {
            entries.push({
              day: d,
              label: scFmtDay(d),
              note: 'off shift — ' + off.map(cp => `${cp.name} (${scFmt12(cp.time)})`).join(', '),
              muted: true,
            });
          }
        }
        return { key: it.key, record, entries, result: -(record * pct) };
      }

      // Store Phone charges per unreturned CALL on the same shift rule: a
      // lead only owns a miss that rang while they were clocked in (v14.06).
      // `detail.calls` carries the local HH:MM of each still-unreturned miss;
      // rows written before v14.06 have none, and those fall back to the
      // day-based charge rather than being silently forgiven.
      if (it.key === 'store_phone') {
        const entries = [];
        let record = 0;
        for (const d of [...(failedByItem[it.key] || [])].sort()) {
          if (!worked.has(d)) continue;
          const times = ((detailByDay[it.key][d] || {}).calls || [])
            .map(c => c && c.time).filter(Boolean);
          if (times.length === 0) {
            record += 1;
            entries.push({ day: d, charged: true, label: scFmtDay(d), note: 'missed call not returned' });
            continue;
          }
          const mine = times.filter(t => onShift(d, t));
          const off = times.filter(t => !onShift(d, t));
          // "12:05pm · (229) 485-8436" — the number is what identifies the
          // call in Quo; the time alone was ambiguous on a day with two.
          const callText = (t) => {
            const num = missedByWall[d + ' ' + t];
            return scFmt12(t) + (num ? ' · ' + (window.fmtPhone ? window.fmtPhone(num) : num) : '');
          };
          if (mine.length) {
            record += 1;
            entries.push({
              day: d, charged: true,
              label: scFmtDay(d),
              note: mine.map(callText).join(', ') + ' — not returned',
              cps: mine.map(t => 'call@' + t),
              // shown under the count in the table cell (v21.01)
              cellLine: scDayLabel(d) + ' ' + mine.map(t => scFmt12(t)).join(', '),
            });
          } else if (off.length) {
            entries.push({
              day: d,
              label: scFmtDay(d),
              note: 'off shift — ' + off.map(callText).join(', '),
              muted: true,
            });
          }
        }
        return { key: it.key, record, entries, result: -(record * pct) };
      }

      // Day-based items. Only Prep has the labor waiver; waived days stay in
      // the tooltip but drop out of the counted record.
      const counted = [...(failedByItem[it.key] || [])].filter(d => worked.has(d)).sort();
      // Waived prep days passed, so they aren't in failedByItem — add them back
      // for display only. They never count toward the record.
      const shown = it.key === 'prep'
        ? [...new Set([...counted, ...prepWaivedDays.filter(d => worked.has(d))])].sort()
        : counted;
      const entries = shown.map(d => {
        const det = it.key === 'prep' ? (prepDetail[d] || {}) : {};
        const waived = !!det.waived;
        return {
          day: d, charged: !waived,
          label: scFmtDay(d),
          note: waived
            ? `waived — labor ${det.labor_pct != null ? det.labor_pct : '?'}% (target ${det.labor_target_pct != null ? det.labor_target_pct : '?'}%)`
            : 'missed',
          muted: waived,
        };
      });
      return { key: it.key, record: counted.length, entries, result: -(counted.length * pct) };
    });
    const attPct = Number(cfg.items.attendance) || 0;
    const lateEntries = (l.late_days || []).map(x => ({
      day: x.date, charged: true,
      label: scFmtDay(x.date),
      note: `in ${scFmtTime(x.clocked_in)} vs ${scFmtTime(x.scheduled_start)}`,
    }));
    const att = { key: 'attendance', record: lateEntries.length, entries: lateEntries, result: -(lateEntries.length * attPct) };
    const custom = activeCustom.map(ci => {
      const count = counts[ci.id] || 0;
      return { id: ci.id, record: count, result: count * ci.pct };
    });
    const myAdj = adjRows.filter(r => r.staff_id === l.staff_id);
    // Overrides tied to a deduction are folded into that line for DISPLAY
    // (v20.03): the day is struck through, the line shows the figure that
    // counted. The score below still sums every override once, exactly as
    // before — `adjPct` is unchanged; `generalPct` is the store-wide part
    // the foot of the table shows.
    items.forEach(row => Object.assign(row, scTieOverrides(myAdj, row.key, row.entries, row.record, row.result, Number(cfg.items[row.key]) || 0)));
    Object.assign(att, scTieOverrides(myAdj, 'attendance', att.entries, att.record, att.result, attPct));
    const generalAdj = myAdj.filter(r => !r.item_key);
    const generalPct = generalAdj.reduce((sum, r) => sum + (Number(r.pct) || 0), 0);
    const tiedCount = myAdj.length - generalAdj.length;
    const adjPct = myAdj.reduce((sum, r) => sum + (Number(r.pct) || 0), 0);
    const raw = 100
      + items.reduce((s, x) => s + x.result, 0)
      + att.result
      + custom.reduce((s, x) => s + x.result, 0)
      + adjPct;
    const finalPct = Math.max(0, Math.min(100, raw));
    const effHours = hoursFor(l);
    const potential = effHours * rateFor(l.staff_id);
    const bonus = potential * finalPct / 100;
    return { ...l, items, att, custom, adjPct, generalPct, tiedCount, adjList: myAdj, generalAdj,
             beforeOverrides: raw - adjPct, raw, finalPct, potential, bonus,
             effHours, fixedWeekly: fixedWeeklyFor(l.staff_id) };
  });

  // ---------- render ----------
  const cellR = { padding: '8px 10px', textAlign: 'right', fontFamily: 'var(--font-num)', whiteSpace: 'nowrap' };
  const cellHead = {
    padding: '8px 10px', fontSize: 10.5, fontWeight: 700, letterSpacing: '0.05em',
    textTransform: 'uppercase', color: 'var(--fg-3)', textAlign: 'right', whiteSpace: 'nowrap',
  };
  const sectionRow = (label) => (
    <tr key={'sec-' + label} style={{ background: 'var(--bg-sunken)' }}>
      <td colSpan={2 + leads.length * 2} style={{ padding: '6px 10px', fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-2)' }}>
        {label}
      </td>
    </tr>
  );
  const resColor = (n) => n > 0 ? '#2D6A2A' : (n < 0 ? '#DC2626' : 'var(--fg-2)');
  // A line an override moved shows both figures (v20.03): the original
  // struck through in grey, then the one that counted. The record does the
  // same when a whole day was cancelled.
  const struckStyle = { textDecoration: 'line-through', color: 'var(--fg-3)', fontWeight: 400, marginRight: 5 };
  const recordCell = (row) => (
    row.effRecord != null && row.effRecord !== row.record
      ? <><span style={struckStyle}>{row.record}</span>{row.effRecord}</>
      : row.record
  );
  const resultCell = (row) => {
    const moved = (row.tiedPct || 0) !== 0;
    const shown = moved ? row.adjusted : row.result;
    return (
      <td style={{ ...cellR, color: resColor(shown), fontWeight: shown !== 0 ? 600 : 400 }}>
        {moved && <span style={struckStyle}>{scPct(row.result)}</span>}
        {shown > 0 ? '+' : ''}{scPct(shown)}
      </td>
    );
  };

  // Open the override composer from a specific deduction (v19.12). Date and
  // person come from the row you clicked, and the percentage defaults to the
  // exact amount that deduction cost — overriding a −10% Checklist day almost
  // always means giving the 10% back. The REASON is deliberately left empty:
  // prefilling it with the miss would put a description of what happened where
  // the justification belongs, and it would get accepted unread.
  const openOverrideFor = ({ lead, itemKey, itemLabel, entry, pct }) => setAdjEditing({
    batchId: null,
    pct: pct ? String(Math.abs(pct)) : '',
    reason: '',
    staffIds: [lead.staff_id],
    eventDate: entry.day,
    // The tie is what gets stored (v20.03); the context string is display only.
    itemKey: itemKey || null,
    // The exact miss (v20.04): the picker offers only leads charged for one
    // of these checkpoints / calls, not everyone charged the line that day.
    cps: entry.cps || null,
    context: itemLabel + ' · ' + entry.label + (entry.note ? ' · ' + entry.note : ''),
  });
  // Edit an existing override from wherever it is shown — the ⓘ card of the
  // deduction it is tied to, or the store-wide list at the foot.
  const openEditBatch = (batchId) => {
    const b = adjBatches.find(x => x.batchId === batchId);
    if (!b) return;
    setAdjEditing({
      batchId: b.batchId, pct: String(b.pct), reason: b.reason, staffIds: b.staffIds,
      eventDate: b.eventDate || '', itemKey: b.itemKey || null,
      context: b.itemKey ? (SC_ITEM_LABEL[b.itemKey] || b.itemKey) + (b.eventDate ? ' · ' + scFmtDay(b.eventDate) : '') : '',
    });
  };

  // Flat rows → the overrides as created. Names resolve through the period's
  // leads first so the column header and this line agree; staffById covers
  // anyone who has since stopped being a lead but still carries the row.
  //
  // Plain expression, NOT useMemo: this sits below the loading/error early
  // returns, so a hook here runs on some renders and not others — React
  // counts hooks by position and throws "rendered more hooks than during the
  // previous render". There are a handful of rows; memoising buys nothing.
  const adjBatches = (() => {
    const by = {};
    adjRows.forEach(r => {
      const b = by[r.batch_id] || (by[r.batch_id] = {
        batchId: r.batch_id, pct: Number(r.pct) || 0, reason: r.reason || '',
        eventDate: r.event_date || null, itemKey: r.item_key || null,
        staffIds: [], names: [], createdAt: r.created_at || '',
      });
      b.staffIds.push(r.staff_id);
      const lead = (leads || []).find(l => l.staff_id === r.staff_id);
      b.names.push(lead ? lead.first_name : ((staffById[r.staff_id] || {}).name || r.staff_id));
    });
    return Object.values(by).sort((a, b2) => String(a.createdAt).localeCompare(String(b2.createdAt)));
  })();

  // Coverage note (v14.12). Daily items are only judged once a day has
  // CLOSED — scorecard_snapshot_range clamps to yesterday (Chicago), because
  // scoring a day still in progress would fail every checklist group and temp
  // checkpoint whose time hasn't arrived. Without saying so, today's misses
  // look like they were forgiven. Only shown while the period is still open.
  const scoredThrough = (daily || []).reduce((m, r) => (r.the_date > m ? r.the_date : m), '');
  const showCoverage = !loading && !error && endISO >= todayISO && scoredThrough && scoredThrough < todayISO;

  return (
    <div className="portal-page-wide" style={{ maxWidth: 1280 }}>
      <PPageHeader
        title="Crew Lead Scorecard"
        subtitle="Bonus report per pay period — daily items are judged automatically; hours and lateness come from Square."
        right={
          <>
            <ScPeriodNav period={period} offset={periodOffset} onOffsetChange={setPeriodOffset} />
            <PBtn variant="secondary" size="sm" icon="refresh" title="Re-check this period's days" onClick={async () => {
              const { error: err, data } = await window.supa.rpc('scorecard_refresh', { d0: startISO, d1: endISO });
              setToast(err ? 'Refresh failed — ' + err.message : 'Re-checked ' + (data / SC_DAILY_ITEMS.length) + ' days');
              if (!err) load();
            }} />
            <PBtn variant="secondary" size="sm" icon="settings" onClick={() => setSettingsOpen(true)}>Settings</PBtn>
          </>
        }
      />

      {showCoverage && (
        <div style={{
          padding: '10px 14px', marginBottom: 12, borderRadius: 10,
          background: 'var(--bg-sunken)', color: 'var(--fg-2)', fontSize: 12.5, lineHeight: 1.5,
          display: 'flex', alignItems: 'center', gap: 8,
        }}>
          <PIcon name="clock" size={14} color="var(--fg-3)" />
          <span>
            Daily items scored through <b style={{ color: 'var(--fg-1)' }}>{scFmtDay(scoredThrough)}</b>.
            {' '}Today is judged after it closes, so anything that happened today isn't counted here yet.
          </span>
        </div>
      )}

      {error && <div style={{ padding: 14, color: '#DC2626', fontSize: 13 }}>Couldn't load: {error}</div>}
      {loading && !error && <div style={{ padding: 40, color: 'var(--fg-3)', fontSize: 13 }}>Crunching the period…</div>}
      {!loading && !error && square && square.failed && (
        <div style={{ padding: 14, marginBottom: 12, borderRadius: 10, background: '#FEF3C7', color: '#92400E', fontSize: 13 }}>
          Square is unreachable — hours, lateness and the crew list can't be shown right now. The daily-item snapshots below are unaffected.
        </div>
      )}
      {!loading && !error && leads.length === 0 && !(square && square.failed) && (
        <div className="portal-card" style={{ padding: 40, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
          No Crew Lead worked in this period.
        </div>
      )}

      {!loading && !error && leads.length > 0 && (
        <div className="portal-card" style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
            <thead>
              <tr style={{ borderBottom: '1px solid var(--border-1)' }}>
                <th style={{ ...cellHead, textAlign: 'left' }}>Object</th>
                <th style={cellHead}>Math</th>
                {leads.map(l => (
                  <th key={l.staff_id} colSpan={2} style={{ ...cellHead, textAlign: 'center', borderLeft: '1px solid var(--border-2)' }}>
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                      <PAvatar staff={{ name: l.name, initials: l.initials, color: l.color || '#64748B', avatarUrl: l.avatar_url }} size={22} />
                      <span style={{ fontSize: 12, color: 'var(--fg-1)', textTransform: 'none', letterSpacing: 0 }}>{l.first_name}</span>
                      {l.active === false && <span style={{ fontSize: 9, color: 'var(--fg-3)', textTransform: 'none' }}>(inactive)</span>}
                      <PBtn size="xs" variant="secondary" icon="download"
                        title={'Export ' + l.first_name + '\u2019s report as PDF'}
                        ariaLabel={'Export ' + l.first_name + '\u2019s report as PDF'}
                        onClick={() => {
                          setPrintName(scPdfName(period.start, l.name, l.finalPct));
                          setPrintLead(l.staff_id);
                        }} />
                    </span>
                  </th>
                ))}
              </tr>
              <tr style={{ borderBottom: '1px solid var(--border-1)' }}>
                <th /><th />
                {leads.map(l => (
                  <React.Fragment key={l.staff_id}>
                    <th style={{ ...cellHead, borderLeft: '1px solid var(--border-2)' }}>Record</th>
                    <th style={cellHead}>Result</th>
                  </React.Fragment>
                ))}
              </tr>
            </thead>
            <tbody>
              {sectionRow('Daily items (shared — days you worked)')}
              {SC_DAILY_ITEMS.map((it, i) => (
                <tr key={it.key} style={{ borderBottom: '1px solid var(--border-2)' }}>
                  <td style={{ padding: '8px 10px' }}>
                    <div>{it.label}</div>
                    <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 2 }}>{it.hint}</div>
                  </td>
                  <td style={{ ...cellR, color: 'var(--fg-2)' }}>−{Number(cfg.items[it.key]) || 0}%</td>
                  {leads.map(l => {
                    const row = l.items[i];
                    return (
                      <React.Fragment key={l.staff_id}>
                        <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', background: '#FEF9EC' }}>
                          {recordCell(row)}
                          <ScInfoTip
                            entries={row.entries}
                            extra={row.orphans}
                            onOverride={(entry) => openOverrideFor({
                              lead: l, itemKey: it.key, itemLabel: it.label, entry,
                              pct: Number(cfg.items[it.key]) || 0,
                            })}
                            onEditOverride={(o) => openEditBatch(o.batch_id)}
                            onDeleteOverride={(o) => deleteOverride(o.batch_id, o.reason)}
                          />
                          {/* Which calls, right in the cell (v21.01): the count
                              alone can't tell a 12:05 miss from a 3:44 one. */}
                          {it.key === 'store_phone' && row.entries.some(e => e.cellLine) && (
                            <div style={{ fontSize: 10, lineHeight: 1.35, color: 'var(--fg-3)', marginTop: 2, fontWeight: 500, whiteSpace: 'normal', maxWidth: 150, textAlign: 'right', marginLeft: 'auto' }}>
                              {row.entries.filter(e => e.cellLine).map(e => (
                                <div key={e.day} style={e.cancelled ? { textDecoration: 'line-through' } : undefined}>{e.cellLine}</div>
                              ))}
                            </div>
                          )}
                        </td>
                        {resultCell(row)}
                      </React.Fragment>
                    );
                  })}
                </tr>
              ))}

              {sectionRow('Attendance (individual)')}
              <tr style={{ borderBottom: '1px solid var(--border-2)' }}>
                <td style={{ padding: '8px 10px' }}>
                  <div>Days late to work</div>
                  <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 2 }}>clock-in after scheduled start</div>
                </td>
                <td style={{ ...cellR, color: 'var(--fg-2)' }}>−{Number(cfg.items.attendance) || 0}%</td>
                {leads.map(l => (
                  <React.Fragment key={l.staff_id}>
                    <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', background: '#FEF9EC' }}>
                      {recordCell(l.att)}
                      <ScInfoTip
                        entries={l.att.entries}
                        extra={l.att.orphans}
                        onOverride={(entry) => openOverrideFor({
                          lead: l, itemKey: 'attendance', itemLabel: 'Days late to work', entry,
                          pct: Number(cfg.items.attendance) || 0,
                        })}
                        onEditOverride={(o) => openEditBatch(o.batch_id)}
                        onDeleteOverride={(o) => deleteOverride(o.batch_id, o.reason)}
                      />
                    </td>
                    {resultCell(l.att)}
                  </React.Fragment>
                ))}
              </tr>

              {activeCustom.length > 0 && sectionRow('Custom (shared — whole period)')}
              {activeCustom.map((ci, i) => (
                <tr key={ci.id} style={{ borderBottom: '1px solid var(--border-2)' }}>
                  <td style={{ padding: '6px 10px' }}>
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
                      {ci.label}
                      <input
                        type="number" min={0}
                        className="portal-input"
                        value={counts[ci.id] ?? ''}
                        placeholder="0"
                        onChange={e => saveCount(ci.id, e.target.value)}
                        style={{ width: 64, height: 28, textAlign: 'right', fontFamily: 'var(--font-num)' }}
                        title="How many this period"
                      />
                    </span>
                  </td>
                  <td style={{ ...cellR, color: 'var(--fg-2)' }}>{ci.pct > 0 ? '+' : ''}{ci.pct}%</td>
                  {leads.map(l => (
                    <React.Fragment key={l.staff_id}>
                      <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', background: '#FEF9EC' }}>{l.custom[i].record}</td>
                      <td style={{ ...cellR, color: resColor(l.custom[i].result), fontWeight: l.custom[i].result !== 0 ? 600 : 400 }}>
                        {l.custom[i].result > 0 ? '+' : ''}{scPct(l.custom[i].result)}
                      </td>
                    </React.Fragment>
                  ))}
                </tr>
              ))}

              {/* OVERRIDES (v20.03). An override tied to a deduction lives in
                  that deduction's ⓘ card and is already reflected in the
                  line's struck-through figure, so it is NOT listed again
                  here — the foot carries only store-wide overrides (no
                  deduction behind them) and their total, and the column
                  reads straight down to the final result. */}
              <tr style={{ background: 'var(--bg-sunken)' }}>
                <td colSpan={2 + leads.length * 2} style={{ padding: '5px 10px' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                    <span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-2)' }}>
                      Store-wide override (manual)
                    </span>
                    <PBtn size="xs" variant="secondary" icon="plus"
                      onClick={() => setAdjEditing({ batchId: null, pct: '', reason: '', staffIds: [], eventDate: '', itemKey: null, context: '' })}>
                      Add override
                    </PBtn>
                    {adjBatches.some(b => b.itemKey) && (
                      <span style={{ fontSize: 11, color: 'var(--fg-3)', fontWeight: 500, letterSpacing: 0, textTransform: 'none' }}>
                        {adjBatches.filter(b => b.itemKey).length} tied to a deduction above — hover its ⓘ
                      </span>
                    )}
                  </div>
                </td>
              </tr>

              {adjBatches.filter(b => !b.itemKey).map(b => (
                <tr key={b.batchId} style={{ borderBottom: '1px solid var(--border-2)' }}>
                  <td style={{ padding: '8px 10px' }}>
                    {/* The OBJECT column is sized for short labels like "Temp Log";
                        a sentence in it wraps to a ten-line sliver. minWidth gives
                        the reason a readable measure and widens the column for
                        every row, which costs nothing — the table already scrolls. */}
                    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, minWidth: 240 }}>
                      <div style={{ flex: 1, minWidth: 0 }} title={b.reason || undefined}>
                        <div style={{ whiteSpace: 'normal', lineHeight: 1.45 }}>
                          {b.reason || <span style={{ color: 'var(--fg-3)', fontStyle: 'italic' }}>No reason recorded</span>}
                        </div>
                        <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 2 }}>
                          {b.eventDate ? scDayLabel(b.eventDate) + ' · ' : ''}{b.names.join(', ')}
                        </div>
                      </div>
                      <div style={{ display: 'flex', gap: 2, flexShrink: 0 }}>
                        <button onClick={() => openEditBatch(b.batchId)}
                          title="Edit this override"
                          style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-3)', padding: 2 }}>
                          <PIcon name="edit" size={13} />
                        </button>
                        <button onClick={() => deleteOverride(b.batchId, b.reason)}
                          title="Remove this override"
                          style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-3)', padding: 2 }}>
                          <PIcon name="trash" size={13} />
                        </button>
                      </div>
                    </div>
                  </td>
                  <td style={{ ...cellR, color: resColor(b.pct) }}>{b.pct > 0 ? '+' : ''}{scPct(b.pct)}</td>
                  {leads.map(l => {
                    const applies = b.staffIds.includes(l.staff_id);
                    return (
                      <React.Fragment key={l.staff_id}>
                        <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', background: '#FEF9EC', color: 'var(--fg-3)' }}>
                          {applies ? <PIcon name="check" size={13} color="#2D6A2A" /> : '—'}
                        </td>
                        <td style={{ ...cellR, color: applies ? resColor(b.pct) : 'var(--fg-3)', fontWeight: applies && b.pct !== 0 ? 600 : 400 }}>
                          {applies ? (b.pct > 0 ? '+' : '') + scPct(b.pct) : '—'}
                        </td>
                      </React.Fragment>
                    );
                  })}
                </tr>
              ))}

              <tr style={{ borderBottom: '1px solid var(--border-2)' }}>
                <td style={{ padding: '8px 10px' }}>
                  <div>Store-wide override total</div>
                  <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 2 }}>
                    {adjBatches.filter(b => !b.itemKey).length === 0
                      ? 'none this period'
                      : 'total of ' + adjBatches.filter(b => !b.itemKey).length + ' store-wide override' + (adjBatches.filter(b => !b.itemKey).length === 1 ? '' : 's')}
                  </div>
                </td>
                <td style={{ ...cellR, color: 'var(--fg-2)' }}>±%</td>
                {leads.map(l => (
                  <React.Fragment key={l.staff_id}>
                    <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', background: '#FEF9EC', color: 'var(--fg-3)' }}>
                      {l.generalAdj.length || '—'}
                    </td>
                    <td style={{ ...cellR, color: resColor(l.generalPct), fontWeight: l.generalPct !== 0 ? 600 : 400 }}>
                      {l.generalPct > 0 ? '+' : ''}{scPct(l.generalPct)}
                    </td>
                  </React.Fragment>
                ))}
              </tr>

              <tr style={{ background: 'var(--fg-1)', color: '#FFFFFF' }}>
                <td style={{ padding: '9px 10px', fontWeight: 700, letterSpacing: '0.03em' }}>FINAL RESULT</td>
                <td />
                {leads.map(l => (
                  <React.Fragment key={l.staff_id}>
                    <td style={{ borderLeft: '1px solid rgba(255,255,255,0.2)' }} />
                    <td style={{ ...cellR, color: '#FFFFFF', fontWeight: 700 }}>
                      {scPct(l.finalPct)}
                      {l.raw !== l.finalPct && (
                        <span style={{ marginLeft: 6, fontSize: 10, color: 'rgba(255,255,255,0.6)', fontWeight: 500 }}>
                          raw {scPct(l.raw)}
                        </span>
                      )}
                    </td>
                  </React.Fragment>
                ))}
              </tr>

              <tr style={{ background: 'var(--bg-sunken)', borderBottom: '1px solid var(--border-2)' }}>
                <td colSpan={2} style={{ padding: '8px 10px', fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--fg-3)' }}>
                  Hours · Bonus <span style={{ fontWeight: 500, textTransform: 'none', letterSpacing: 0 }}>(rate set on Crew)</span>
                </td>
                {leads.map(l => (
                  <React.Fragment key={l.staff_id}>
                    <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', background: '#FEF9EC' }}>
                      {l.effHours.toFixed(2)}
                      {/* A stated figure must never look like a measured one.
                          The dotted underline + tooltip keep the real clocked
                          hours one hover away, so nobody has to wonder why the
                          number doesn't match Square. */}
                      {l.fixedWeekly != null && (
                        <span
                          title={'Fixed at ' + l.fixedWeekly + ' hrs/week × ' + periodWeeks + ' weeks. '
                            + 'Square timecards for this period: ' + (l.hours || 0).toFixed(2) + ' hrs.'}
                          style={{
                            marginLeft: 4, fontSize: 10, fontWeight: 600, color: 'var(--fg-3)',
                            borderBottom: '1px dotted var(--fg-3)', cursor: 'help',
                          }}
                        >fixed</span>
                      )}
                      {/* The rate is per person now, so the row header can't state it
                          once for everyone — it has to sit with the hours it multiplies. */}
                      <span style={{ color: 'var(--fg-3)', fontWeight: 500, marginLeft: 4 }}>
                        ({scMoney(rateFor(l.staff_id))}/hr)
                      </span>
                    </td>
                    <td style={{ ...cellR, color: '#2D6A2A', fontWeight: 700 }}>{scMoney(l.bonus)}</td>
                  </React.Fragment>
                ))}
              </tr>
              <tr>
                <td colSpan={2} style={{ padding: '8px 10px', fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--fg-3)' }}>
                  Potential · Missed
                </td>
                {leads.map(l => (
                  <React.Fragment key={l.staff_id}>
                    <td style={{ ...cellR, borderLeft: '1px solid var(--border-2)', color: 'var(--fg-2)' }}>{scMoney(l.potential)}</td>
                    <td style={{ ...cellR, color: l.potential - l.bonus > 0 ? '#DC2626' : 'var(--fg-2)' }}>{scMoney(l.potential - l.bonus)}</td>
                  </React.Fragment>
                ))}
              </tr>
            </tbody>
          </table>
        </div>
      )}

      {!loading && !error && (
        <div style={{ marginTop: 10, fontSize: 11.5, color: 'var(--fg-3)', lineHeight: 1.5 }}>
          Daily items are frozen nightly just after midnight (Chicago) — a failed day charges every Crew Lead who clocked in that day.
          Today ({fmtDate(new Date())}) isn't judged until it's over. Hours count closed Square shifts only, so today's open shift appears after clock-out.
          <> A failed Prep day is waived when that day's scheduled labor ran at or under the weekday's target share of sales — set those on <b>Sales vs. Labor</b>. Hover the ⓘ to see which days.</>
          {' '}Checklist and Temp Log only charge a lead for checkpoints that fell inside their clock-in/out — an off-shift miss shows greyed in the ⓘ and doesn't count.
          {' '}An override tied to a deduction is struck through inside that deduction's ⓘ, and the line shows the figure that counted; only store-wide overrides are listed at the foot.
        </div>
      )}

      {/* Print sheet. Kept out of the DOM until asked for, and out of the
          screen layout entirely — `position: fixed; left: -10000px` rather
          than display:none, because a display:none subtree doesn't lay out
          and prints blank in Safari. In print we hide everything by
          VISIBILITY (display:none on an ancestor would take the report with
          it) and float the sheet to the top-left of the page box. */}
      {printLead && (() => {
        const lead = leads.find(l => l.staff_id === printLead);
        if (!lead) return null;
        return ReactDOM.createPortal(
          <>
            <style>{`
              @page { size: letter portrait; margin: 0.5in; }
              #sc-print { position: fixed; left: -10000px; top: 0; width: 7.5in; }
              @media print {
                html, body { background: #FFFFFF !important; }
                /* display:none, not visibility:hidden. Hidden content still
                   takes up layout height, and the scorecard page is several
                   screens tall — that printed the report on page 1 and a
                   blank page 2. This only works because the sheet is
                   portalled to <body>, so it isn't inside anything we hide. */
                body > *:not(#sc-print) { display: none !important; }
                #sc-print {
                  position: static !important; left: auto !important; top: auto !important;
                  width: 100% !important;
                }
                /* Keep a day's misses with their heading. */
                #sc-print tr, #sc-print section { break-inside: avoid; page-break-inside: avoid; }
                #sc-print thead { display: table-header-group; }
              }
            `}</style>
            <div id="sc-print">
              <ScReport
                lead={lead}
                cfg={cfg}
                activeCustom={activeCustom}
                rateFor={rateFor}
                periodLabel={period ? (fmtDate(period.start) + ' – ' + fmtDate(period.end)) : ''}
                generatedOn={new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
              />
            </div>
          </>,
          document.body,
        );
      })()}

      {settingsOpen && config && (
        <ScSettingsModal
          config={config}
          customItems={customItems}
          setCustomItems={setCustomItems}
          leads={leads}
          staffById={staffById}
          periodWeeks={periodWeeks}
          onSaveFixedHours={saveFixedHours}
          onSave={saveConfig}
          onClose={() => setSettingsOpen(false)}
        />
      )}

      {adjEditing && (
        <ScOverrideModal
          draft={adjEditing}
          leads={leads}
          cfgItemsPct={(k) => cfg.items[k]}
          periodLabel={period ? (fmtDate(period.start) + ' – ' + fmtDate(period.end)) : 'this period'}
          periodStart={startISO}
          periodEnd={endISO}
          todayISO={todayISO}
          onSave={async (d) => { if (await saveOverride(d)) setAdjEditing(null); }}
          onClose={() => setAdjEditing(null)}
        />
      )}

      {toast && <ScToast msg={toast} onDone={() => setToast('')} />}
    </div>
  );
};

// Period navigator — same interaction as DailyReport's, named apart to avoid
// the top-level-const collision gotcha (HANDBOOK §9).
const ScPeriodNav = ({ period, offset, onOffsetChange }) => (
  <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
    <PBtn variant="secondary" size="sm" icon="chevronL" onClick={() => onOffsetChange(offset - 1)} title="Previous period" />
    <div style={{
      padding: '6px 14px', background: 'var(--bg-surface)', border: '1px solid var(--border-1)',
      borderRadius: 8, fontSize: 12.5, fontWeight: 500, minWidth: 200, textAlign: 'center', whiteSpace: 'nowrap',
    }}>
      {fmtDate(period.start)} – {fmtDate(period.end)}
    </div>
    <PBtn variant="secondary" size="sm" icon="chevronR" onClick={() => onOffsetChange(offset + 1)} title="Next period" />
    {offset !== 0 && <PBtn variant="ghost" size="sm" onClick={() => onOffsetChange(0)}>Current</PBtn>}
  </div>
);

const ScToast = ({ msg, onDone }) => {
  useEffect(() => { const t = setTimeout(onDone, 2600); return () => clearTimeout(t); }, [msg]);
  return (
    <div style={{
      position: 'fixed', bottom: 26, left: '50%', transform: 'translateX(-50%)', zIndex: 3000,
      background: '#181818', color: '#fff', borderRadius: 10, padding: '10px 16px', fontSize: 13,
    }}>{msg}</div>
  );
};

// Settings: $/hr rate + per-item deduction %s + custom item manager.
// One override: how much, why, and who it lands on (v19.06).
//
// The reason is REQUIRED. This is the only number on the page a manager can
// move by hand, it goes straight onto someone's pay, and it is the one the
// person receiving it has the most reason to question — an unexplained ±30%
// is exactly what a written record should not contain. The text lands on
// their printed report verbatim, so the field says so.
const ScOverrideModal = ({ draft, leads, cfgItemsPct, periodLabel, periodStart, periodEnd, todayISO, onSave, onClose }) => {
  const [pct, setPct] = useState(draft.pct ?? '');
  const [reason, setReason] = useState(draft.reason || '');
  // The leads a line actually charged on a day (v20.04). For the checkpoint
  // items that is not "who worked" — Checklist, Temp Log and Store Phone
  // charge by who was clocked in at that hour — so a tied override offers
  // exactly the people carrying that deduction, and offers all of them
  // pre-selected: a shared miss is nearly always forgiven for everyone it hit.
  const lineEntries = (l, key) => key === 'attendance'
    ? (l.att && l.att.entries) || []
    : ((l.items || []).find(row => row.key === key) || {}).entries || [];
  // `cps` narrows a checkpoint line to the exact miss the override started
  // from (v20.04): a lead charged Checklist that day for a DIFFERENT hour
  // was not affected by this one. A lead whose entry carries no checkpoints
  // (pre-v14.06 day-based charge) is kept — nothing says it wasn't the same.
  const chargedOn = (key, day, cps) => (!key || !day) ? []
    : leads.filter(l => lineEntries(l, key).some(e => e.day === day && e.charged
        && (!cps || !cps.length || !e.cps || e.cps.some(c => cps.includes(c)))));
  const originCps = (key) => (key && key === draft.itemKey ? draft.cps : null) || null;
  const [ids, setIds] = useState(() => (!draft.batchId && draft.itemKey)
    ? chargedOn(draft.itemKey, draft.eventDate, originCps(draft.itemKey)).map(l => l.staff_id)
    : (draft.staffIds || []));
  // Defaults to today when the period is still running, otherwise its last
  // day — an override is nearly always about something inside the period
  // being scored, so an empty date field would just be typing for its own sake.
  const [eventDate, setEventDate] = useState(
    draft.eventDate || (todayISO >= periodStart && todayISO <= periodEnd ? todayISO : periodEnd) || '');
  const outsidePeriod = eventDate && (eventDate < periodStart || eventDate > periodEnd);
  // Escape hatch: an override can legitimately be about something with no
  // shift attached (a rota someone never published, say), so the full list
  // stays reachable — just not the default.
  const [showAll, setShowAll] = useState(false);
  // What this override is tied to (v20.03): a scorecard line + the date
  // above. Opened from a deduction's ⓘ it arrives set; opened from the foot
  // it is picked here from the deductions the selected people were charged
  // on that date — or left untied, which is a store-wide adjustment.
  const [itemKey, setItemKey] = useState(draft.itemKey || null);
  const whoWorked = leads.filter(l => scWorkedOn(l, eventDate));
  const whoCharged = chargedOn(itemKey, eventDate, originCps(itemKey));
  // Shift data is only loaded for the period on screen. For a date outside
  // it we know nothing, which is not the same as knowing nobody worked —
  // so offer everyone rather than assert an absence we can't see.
  const unknownShifts = !!outsidePeriod;
  // Tied: the people that deduction charged. Untied: the people on shift.
  const narrow = itemKey ? whoCharged : whoWorked;
  const choices = (showAll || unknownShifts) ? leads : narrow;
  // Changing the date (or the tie) can strip someone of their reason for
  // being selected. Silently leaving them ticked is how an override lands
  // on a person who wasn't there, which is the whole thing this is meant
  // to prevent. A NEW tied override re-selects everyone the deduction hit.
  useEffect(() => {
    setIds(prev => {
      const kept = prev.filter(id => choices.some(l => l.staff_id === id));
      if (itemKey && !draft.batchId && !showAll) {
        const hit = chargedOn(itemKey, eventDate, originCps(itemKey)).map(l => l.staff_id);
        return hit.length ? hit : kept;
      }
      return kept;
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [eventDate, showAll, itemKey]);
  const [saving, setSaving] = useState(false);
  const tieChoices = (() => {
    if (!eventDate) return [];
    const seen = {};
    leads.filter(l => ids.includes(l.staff_id)).forEach(l => {
      const lines = l.items.map(row => ({ key: row.key, label: SC_ITEM_LABEL[row.key], entries: row.entries, each: Number(cfgItemsPct(row.key)) || 0 }))
        .concat([{ key: 'attendance', label: SC_ITEM_LABEL.attendance, entries: l.att.entries, each: Number(cfgItemsPct('attendance')) || 0 }]);
      lines.forEach(line => {
        const hit = (line.entries || []).find(e => e.day === eventDate && e.charged);
        if (hit && !seen[line.key]) seen[line.key] = { key: line.key, label: line.label, each: line.each, note: hit.note };
      });
    });
    return Object.values(seen);
  })();
  const n = Number(pct);
  const pctValid = pct !== '' && !Number.isNaN(n) && n >= -100 && n <= 100 && n !== 0;
  const ok = pctValid && reason.trim().length > 0 && ids.length > 0 && !!eventDate;
  const toggle = (id) => setIds(v => v.includes(id) ? v.filter(x => x !== id) : v.concat([id]));

  return (
    <PModal open onClose={onClose} width={520} title={draft.batchId ? 'Edit override' : 'Add override'} footer={
      <>
        <div style={{ flex: 1 }} />
        <PBtn variant="ghost" onClick={onClose}>Cancel</PBtn>
        <PBtn variant="primary" disabled={!ok || saving}
          onClick={async () => { setSaving(true); await onSave({ ...draft, pct: n, reason, staffIds: ids, eventDate, itemKey }); setSaving(false); }}>
          {saving ? 'Saving…' : draft.batchId ? 'Save override' : 'Add override'}
        </PBtn>
      </>
    }>
      <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 15 }}>
        {itemKey && (
          <div style={{
            fontSize: 12.5, lineHeight: 1.5, color: 'var(--fg-2)',
            background: '#EEF3EF', borderRadius: 8, padding: '10px 12px',
            display: 'flex', alignItems: 'center', gap: 10,
          }}>
            <span style={{ flex: 1 }}>
              <span style={{ color: 'var(--fg-3)' }}>Tied to</span>{' '}
              <strong style={{ color: 'var(--fg-1)' }}>
                {draft.context && draft.itemKey === itemKey ? draft.context : (SC_ITEM_LABEL[itemKey] || itemKey) + (eventDate ? ' · ' + scFmtDay(eventDate) : '')}
              </strong>
              <span style={{ display: 'block', fontSize: 11.5, color: 'var(--fg-3)', marginTop: 2 }}>
                Shows struck through under that deduction, with this reason.
              </span>
            </span>
            <button onClick={() => setItemKey(null)} title="Make this a store-wide adjustment instead"
              style={{ background: 'none', border: '1px solid var(--border-2)', borderRadius: 999, padding: '2px 9px', cursor: 'pointer', fontSize: 11, color: 'var(--fg-2)', whiteSpace: 'nowrap' }}>
              Untie
            </button>
          </div>
        )}
        <PField label="Adjustment" hint={'Signed percent, added to the final score for ' + periodLabel + '. Use a minus sign to take away.'}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <input type="number" step={0.5} min={-100} max={100} className="portal-input"
              value={pct} onChange={e => setPct(e.target.value)} placeholder="e.g. 10 or -15"
              style={{ width: 130, textAlign: 'right', fontFamily: 'var(--font-num)' }} autoFocus />
            <span style={{ fontSize: 14, color: 'var(--fg-2)' }}>%</span>
            {pctValid && (
              <span style={{ fontSize: 12.5, marginLeft: 4, fontWeight: 600, color: n > 0 ? '#2D6A2A' : '#DC2626' }}>
                {n > 0 ? 'adds' : 'takes away'} {Math.abs(n)}% {n > 0 ? 'to' : 'from'} {ids.length || 'no'}
                {' '}{ids.length === 1 ? 'person' : 'people'}
              </span>
            )}
          </div>
        </PField>

        <PField label="When it happened" hint="The date of the incident, not the day you're recording it. Printed on their report beside the reason.">
          <input type="date" className="portal-input" value={eventDate}
            onChange={e => setEventDate(e.target.value)} style={{ width: 190 }} />
          {outsidePeriod && (
            <div style={{ fontSize: 11.5, color: '#9A3412', marginTop: 6 }}>
              That's outside {periodLabel}. It still applies to this period's score — just make sure that's what you meant.
            </div>
          )}
        </PField>

        {/* Only offered once there's a date, and only the leads who actually
            worked it (v19.11). Picking someone who wasn't there is not a
            thing a manager means to do, and the old list made it a
            single mis-click. Their clock times ride along, because a
            checkpoint or a missed call is charged by whether they were on
            shift at that hour, not merely present that day. */}
        {!eventDate ? (
          <PField label="Who it applies to">
            <div style={{ fontSize: 12.5, color: 'var(--fg-3)', padding: '9px 12px', background: 'var(--bg-sunken)', borderRadius: 8 }}>
              Pick a date first — then this lists who was on shift.
            </div>
          </PField>
        ) : (narrow.length === 0 && !unknownShifts) ? (
          <PField label="Who it applies to">
            <div style={{ fontSize: 12.5, color: '#9A3412', padding: '9px 12px', background: 'var(--bg-sunken)', borderRadius: 8, lineHeight: 1.55 }}>
              {itemKey
                ? <>Nobody was charged {SC_ITEM_LABEL[itemKey] || itemKey} on {scDayLabel(eventDate)}. Untie it, pick another date, or use</>
                : <>No Crew Lead was clocked in on {scDayLabel(eventDate)}. Pick another date, or use</>}
              {' '}<button onClick={() => setShowAll(v => !v)} style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', color: 'var(--fg-2)', fontSize: 12.5, textDecoration: 'underline' }}>
                {showAll ? (itemKey ? 'only who was charged' : 'only who worked') : 'anyone in this period'}
              </button>.
            </div>
          </PField>
        ) : null}

        {eventDate && choices.length > 0 && (
          <PField label="Who it applies to"
            hint={itemKey && !showAll && !unknownShifts
              ? 'Only the people this ' + (originCps(itemKey) ? 'miss' : 'deduction') + ' charged on ' + scDayLabel(eventDate) + ' — all of them are selected, untick anyone it shouldn\u2019t reach.'
              : undefined}>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
              {choices.map(l => {
                const on = ids.includes(l.staff_id);
                const w = scWorkedOn(l, eventDate);
                const hit = itemKey ? lineEntries(l, itemKey).find(e => e.day === eventDate && e.charged) : null;
                // eslint-disable-next-line no-unused-vars
                return (
                  <button key={l.staff_id} onClick={() => toggle(l.staff_id)} title={hit && hit.note ? hit.note : undefined} style={{
                    display: 'inline-flex', alignItems: 'center', gap: 8,
                    padding: '6px 12px 6px 6px', borderRadius: 999, cursor: 'pointer',
                    background: on ? 'var(--fg-1)' : 'var(--bg-sunken)',
                    color: on ? '#FFFFFF' : 'var(--fg-1)',
                    border: '1px solid ' + (on ? 'var(--fg-1)' : 'var(--border-2)'),
                    fontSize: 13, fontWeight: on ? 600 : 500,
                  }}>
                    <PAvatar staff={{ name: l.name || l.first_name, avatarUrl: l.avatar_url, initials: (l.first_name || '?')[0] }} size={20} />
                    {l.first_name}
                    {/* Silent when shifts aren't loaded — "not on shift" would
                        be asserting an absence we cannot see. */}
                    {!unknownShifts && (
                      <span style={{ fontSize: 11, opacity: on ? 0.75 : 0.6, fontFamily: 'var(--font-num)' }}>
                        {w && w.spans.length ? w.spans.join(', ') : (w ? 'on shift' : 'not on shift')}
                      </span>
                    )}
                  </button>
                );
              })}
            </div>
            <div style={{ marginTop: 9, display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap' }}>
              {choices.length > 1 && (
                <button onClick={() => setIds(ids.length === choices.length ? [] : choices.map(l => l.staff_id))}
                  style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                           color: 'var(--fg-2)', fontSize: 12.5, textDecoration: 'underline' }}>
                  {ids.length === choices.length ? 'Clear all' : (unknownShifts ? 'Select everyone' : (itemKey ? 'Everyone this deduction charged' : 'Everyone on shift that day'))}
                </button>
              )}
              {unknownShifts && (
                <span style={{ fontSize: 12, color: '#9A3412' }}>
                  Shift times aren’t loaded for dates outside {periodLabel}, so everyone is listed.
                </span>
              )}
              {!unknownShifts && narrow.length > 0 && narrow.length < leads.length && (
                <button onClick={() => setShowAll(v => !v)}
                  style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                           color: 'var(--fg-3)', fontSize: 12.5, textDecoration: 'underline' }}>
                  {showAll ? (itemKey ? 'Only who was charged' : 'Only who worked that day') : 'Show everyone in this period'}
                </button>
              )}
            </div>
          </PField>
        )}

        {/* Which deduction this undoes (v20.03). Only offered once there is a
            date and someone selected, and only the lines they were actually
            charged on that date. Untied = store-wide, listed at the foot. */}
        {!itemKey && eventDate && ids.length > 0 && (
          <PField label="Tied to a deduction"
            hint={tieChoices.length
              ? 'Pick the deduction this override undoes; it will show struck through under it. Leave it untied for a store-wide adjustment.'
              : 'Nobody selected was charged anything on ' + scDayLabel(eventDate) + ', so this can only be a store-wide adjustment.'}>
            {tieChoices.length > 0 && (
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                {tieChoices.map(c => (
                  <button key={c.key}
                    onClick={() => { setItemKey(c.key); if (pct === '' && c.each) setPct(String(c.each)); }}
                    /* the effect above re-selects everyone that line charged on this date */
                    title={c.note || undefined}
                    style={{
                      display: 'inline-flex', alignItems: 'center', gap: 8,
                      padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
                      background: 'var(--bg-sunken)', color: 'var(--fg-1)',
                      border: '1px solid var(--border-2)', fontSize: 13, fontWeight: 500,
                    }}>
                    {c.label}
                    <span style={{ fontSize: 11, color: '#DC2626', fontFamily: 'var(--font-num)', fontWeight: 600 }}>−{c.each}%</span>
                  </button>
                ))}
              </div>
            )}
          </PField>
        )}

        <PField label="Reason" hint="Printed on their report, word for word. Write it so it still makes sense to them in a month.">
          {window.AiTextField
            ? <window.AiTextField value={reason} onChange={setReason} multiline rows={3}
                placeholder="What happened, and why it moves the score." contextLabel="Score override reason" />
            : <textarea className="portal-input" rows={3} value={reason} onChange={e => setReason(e.target.value)}
                placeholder="What happened, and why it moves the score." style={{ height: 'auto', padding: 10 }} />}
        </PField>

      </div>
    </PModal>
  );
};

const ScSettingsModal = ({ config, customItems, setCustomItems, leads, staffById, periodWeeks, onSaveFixedHours, onSave, onClose }) => {
  const [draft, setDraft] = useState(() => JSON.parse(JSON.stringify(config)));
  const [newLabel, setNewLabel] = useState('');
  const [newPct, setNewPct] = useState('');

  const setItem = (k, v) => setDraft(d => ({ ...d, items: { ...d.items, [k]: Math.max(0, Number(v) || 0) } }));
  const rows = [...SC_DAILY_ITEMS.map(it => ({ key: it.key, label: it.label })), { key: 'attendance', label: 'Attendance (late day)' }];

  const addCustom = () => {
    const label = newLabel.trim();
    const pct = Number(newPct);
    if (!label || !isFinite(pct) || pct === 0) return;
    const id = 'sci-' + Date.now().toString(36);
    const maxSort = customItems.reduce((m, c) => Math.max(m, c.sortOrder || 0), 0);
    setCustomItems(list => [...list, { id, label, pct, sortOrder: maxSort + 10, archived: false }]);
    setNewLabel(''); setNewPct('');
  };

  return (
    <PModal open onClose={onClose} title="Scorecard settings" width={560} footer={
      <>
        <PBtn variant="secondary" onClick={onClose}>Close</PBtn>
        <PBtn variant="primary" onClick={() => { onSave(draft); onClose(); }}>Save</PBtn>
      </>
    }>
      <div style={{ padding: '14px 18px', display: 'flex', flexDirection: 'column', gap: 18 }}>
        <PField label="Bonus rate" hint="Now per person, on Crew. Each staff member has their own $/hr bonus; leave someone at $0 and they simply have no bonus opportunity.">
          <div style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>
            Set per person on <b style={{ color: 'var(--fg-2)' }}>Crew</b>.
          </div>
        </PField>
        <PField label="Prep labor waiver" hint="Now per weekday, on Sales vs. Labor. A failed Prep day is waived when that day's scheduled labor (wages + bonus) lands at or under that weekday's target share of sales \u2014 gross less third-party commission, with discounts left in.">
          <div style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>
            Set per weekday on <b style={{ color: 'var(--fg-2)' }}>Sales vs. Labor</b>.
          </div>
        </PField>

        <PField label="Fixed hours"
          hint={'For salaried crew whose timecards aren\u2019t the whole story \u2014 someone who clocks only their scheduled shifts but also works outside them. Leave blank to use clocked Square hours, which is the default. Multiplied by the ' + periodWeeks + '-week pay period.'}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 132px', gap: 8, alignItems: 'center' }}>
            {(leads || []).map(l => {
              const cur = (staffById && staffById[l.staff_id] && staffById[l.staff_id].fixed_hours_per_week);
              return (
                <React.Fragment key={l.staff_id}>
                  <div style={{ fontSize: 13, color: 'var(--fg-1)' }}>
                    {l.name || (staffById && staffById[l.staff_id] && staffById[l.staff_id].name) || l.staff_id}
                    <span style={{ fontSize: 11.5, color: 'var(--fg-3)', marginLeft: 6 }}>
                      {cur == null || cur === ''
                        ? 'clocked ' + (l.hours || 0).toFixed(2) + ' hrs'
                        : '= ' + (Number(cur) * periodWeeks).toFixed(2) + ' hrs this period'}
                    </span>
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                    <input
                      type="number" min="0.25" max="168" step="0.25" placeholder="clocked"
                      defaultValue={cur == null ? '' : cur}
                      onBlur={e => onSaveFixedHours(l.staff_id, e.target.value)}
                      onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur(); }}
                      className="portal-input"
                      style={{ width: 78, height: 30, textAlign: 'right', fontFamily: 'var(--font-num)' }}
                    />
                    <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>hrs/wk</span>
                  </div>
                </React.Fragment>
              );
            })}
            {(!leads || leads.length === 0) && (
              <div style={{ gridColumn: '1 / -1', fontSize: 12.5, color: 'var(--fg-3)' }}>
                No crew leads loaded for this period.
              </div>
            )}
          </div>
        </PField>

        <PField label="Display" hint="Inactive staff who worked the period still earn their bonus — this only hides their column.">
          <button
            type="button"
            onClick={() => setDraft(d => ({ ...d, hide_inactive: !d.hide_inactive }))}
            style={{ display: 'inline-flex', alignItems: 'center', gap: 10, background: 'transparent', border: 'none', cursor: 'pointer', padding: 0 }}
          >
            <span style={{
              width: 38, height: 22, borderRadius: 999, padding: 2,
              background: draft.hide_inactive ? 'var(--fg-1)' : 'var(--border-1)',
              display: 'inline-flex', justifyContent: draft.hide_inactive ? 'flex-end' : 'flex-start',
              transition: 'background 120ms',
            }}>
              <span style={{ width: 18, height: 18, borderRadius: 999, background: '#FFFFFF' }} />
            </span>
            <span style={{ fontSize: 13, color: 'var(--fg-1)' }}>Hide inactive staff</span>
          </button>
        </PField>

        <PField label="Deductions" hint="Percent taken off the period score per failed day (or late day).">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 110px', gap: 8, alignItems: 'center' }}>
            {rows.map(r => (
              <React.Fragment key={r.key}>
                <span style={{ fontSize: 13 }}>{r.label}</span>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'flex-end' }}>
                  <span style={{ fontSize: 13, color: 'var(--fg-3)' }}>−</span>
                  <input type="number" min={0} max={100} className="portal-input"
                    value={draft.items[r.key] ?? 0}
                    onChange={e => setItem(r.key, e.target.value)}
                    style={{ width: 64, height: 30, textAlign: 'right', fontFamily: 'var(--font-num)' }} />
                  <span style={{ fontSize: 13, color: 'var(--fg-3)' }}>%</span>
                </div>
              </React.Fragment>
            ))}
          </div>
        </PField>

        <PField label="Custom items" hint="Manual counts per period, positive or negative % each (e.g. 5-star review +1, 4-star −5).">
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {customItems.filter(c => !c.archived).map(c => (
              <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <input className="portal-input" value={c.label}
                  onChange={e => setCustomItems(list => list.map(x => x.id === c.id ? { ...x, label: e.target.value } : x))}
                  style={{ flex: 1, height: 30 }} />
                <input type="number" step={0.5} className="portal-input" value={c.pct}
                  onChange={e => setCustomItems(list => list.map(x => x.id === c.id ? { ...x, pct: Number(e.target.value) || 0 } : x))}
                  style={{ width: 72, height: 30, textAlign: 'right', fontFamily: 'var(--font-num)' }} />
                <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>%</span>
                <button title="Remove" onClick={() => setCustomItems(list => list.map(x => x.id === c.id ? { ...x, archived: true } : x))}
                  style={{ color: 'var(--fg-3)' }}><PIcon name="trash" size={13} /></button>
              </div>
            ))}
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
              <input className="portal-input" placeholder="e.g. 5-star Google review" value={newLabel}
                onChange={e => setNewLabel(e.target.value)} style={{ flex: 1, height: 30 }} />
              <input type="number" step={0.5} className="portal-input" placeholder="+1 / −5" value={newPct}
                onChange={e => setNewPct(e.target.value)} style={{ width: 72, height: 30, textAlign: 'right' }} />
              <PBtn variant="secondary" size="sm" icon="plus" onClick={addCustom}>Add</PBtn>
            </div>
          </div>
        </PField>
      </div>
    </PModal>
  );
};

window.CrewLeadScorecard = CrewLeadScorecard;
