// SalesGoal.jsx — the "Sales vs. Labor" page (v16.00; own nav route v16.02).
// Sets what the iPad strip reports as CREW LOAD. Renamed twice: "Sales Goal"
// → "Efficiency Goal" (v16.06) → here (v16.08), once we pinned down that the
// number is demand against staffing, not an efficiency rate. The route hash
// stays `sales-goal` through all of it so bookmarks keep working.
//
// Three settings that together drive the iPad's pacing strip and the prep
// waiver on the Crew Lead Scorecard:
//
//   1. A labor % target per weekday. Scheduled labor ÷ that target = the day's
//      sales goal. Monday and Friday genuinely have different shapes, so
//      one number for the week would be wrong most days.
//   2. A hand-drawn hourly curve per weekday. Blocks are RELATIVE weights, not
//      dollars — 3 blocks at noon and 1 at 2pm just means noon is worth three
//      times as much. That's what turns raw progress into pace, which is the
//      difference between a strip the crew reads and one they learn to ignore.
//   3. Specific dates marked untrackable. On a catering day Ryan schedules
//      extra labor, so the ratio reads like a failure through nobody's fault;
//      the strip says so instead of lying. Untrackable does NOT waive prep —
//      he adjudicates those by hand.
//
// Everything lives in restaurants.sales_goal_config except the untrackable
// dates, which are rows in sales_untrackable_days.
//
// All top-level idents are sg-prefixed — Babel-standalone shares one global
// scope across every .jsx in the app (HANDBOOK §9).

const SG_DOW = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const SG_DOW_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const SG_MAX_BLOCKS = 10;

// '14' -> '2p'. Compact enough to sit under a narrow bar.
const sgHourLabel = (h) => {
  const n = Number(h);
  if (n === 12) return '12p';
  return n > 12 ? (n - 12) + 'p' : n + 'a';
};

const sgMoney = (cents) =>
  '$' + (Math.round(cents / 100)).toLocaleString();

// Date-only strings, handled as strings. `new Date('2026-08-05')` parses as
// UTC and lands on the 4th in Chicago — the trap this file already hit once.
const sgToday = () => {
  const n = new Date();
  return n.getFullYear() + '-' + String(n.getMonth() + 1).padStart(2, '0') + '-' + String(n.getDate()).padStart(2, '0');
};
const sgParse = (iso) => { const [y, m, d] = String(iso).split('-').map(Number); return new Date(y, m - 1, d); };
const sgISO = (d) =>
  d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
const sgAddDays = (iso, n) => { const d = sgParse(iso); d.setDate(d.getDate() + n); return sgISO(d); };
// Sunday of the week containing `iso` — the card grid runs Sun..Sat, so the
// week it shows has to start there too.
const sgWeekStart = (iso) => sgAddDays(iso, -sgParse(iso).getDay());
const sgShortDate = (iso) => {
  const d = sgParse(iso);
  return ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()] + ' ' + d.getDate();
};

// Round a goal to something a human argues about less. $2,119 invites
// "but it was $2,107" — $2,100 doesn't.
const sgRoundGoal = (dollars) => Math.round(dollars / 25) * 25;

// One label/value line inside a weekday card. Values are right-aligned in the
// numeric face so the seven cards line up as columns rather than as prose.
const SgRow = ({ label, value, color, strong }) => (
  <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 6, lineHeight: 1.6 }}>
    {/* nowrap: "Sched labor" wrapped to two lines in the narrow cards and
        only in some of them, so the seven cards stopped lining up row-for-row
        — the exact raggedness v16.03 set out to fix. */}
    <span style={{ fontSize: 10.5, color: 'var(--fg-3)', whiteSpace: 'nowrap' }}>{label}</span>
    <span style={{
      fontSize: 11.5, fontFamily: 'var(--font-num)', whiteSpace: 'nowrap',
      fontWeight: strong ? 600 : 500,
      color: color || (strong ? 'var(--fg-1)' : 'var(--fg-2)'),
    }}>{value}</span>
  </div>
);

// One roll-up figure beside the week picker. Label over value, so four of
// them read as a row of columns rather than a sentence.
const SgStat = ({ label, value, color, title }) => (
  <div title={title} style={{ textAlign: 'right', cursor: title ? 'help' : 'default' }}>
    <div style={{ fontSize: 9.5, fontWeight: 700, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.06em', lineHeight: 1.4 }}>
      {label}
    </div>
    <div style={{ fontSize: 13, fontWeight: 600, fontFamily: 'var(--font-num)', lineHeight: 1.3,
      color: color || 'var(--fg-1)', whiteSpace: 'nowrap' }}>{value}</div>
  </div>
);

const SgSection = ({ title, hint, children }) => (
  <div style={{ marginBottom: 22 }}>
    <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-1)', marginBottom: hint ? 4 : 10 }}>{title}</div>
    {hint && <div style={{ fontSize: 12, color: 'var(--fg-3)', lineHeight: 1.5, marginBottom: 10, maxWidth: 780 }}>{hint}</div>}
    {children}
  </div>
);

const SalesGoal = () => {
  const [cfg, setCfg] = useState(null);
  const [dow, setDow] = useState(() => new Date().getDay());
  const [saving, setSaving] = useState(false);
  const [toast, setToast] = useState('');
  const [untrackable, setUntrackable] = useState([]);
  const [newFrom, setNewFrom] = useState('');
  const [newTo, setNewTo] = useState('');
  const [newNote, setNewNote] = useState('');
  // Most recent FINISHED occurrence per weekday — the result block at the
  // bottom of each card. Read-only; never written back.
  const [laborByDow, setLaborByDow] = useState({});
  // THIS week's scheduled labor per date, which is what the goals are built
  // from (v16.09). Two sources, and the precedence matters: a labor_daily row
  // is the snapshot the iPad is actually judging the crew against, so it wins
  // wherever it exists. Days with no snapshot yet (the rest of the week) come
  // from labor-week-preview reading Square live. See §9.
  const [weekLabor, setWeekLabor] = useState(null);
  // Which week the cards show. 0 = the week containing today; Ryan publishes
  // one to two weeks out, so the point of this is looking forward at a week
  // he can still change.
  const [weekOffset, setWeekOffset] = useState(0);
  // Raw labor_daily rows, kept so changing weeks can re-derive without
  // re-reading the table.
  const [labRows, setLabRows] = useState(null);
  // The crew-load sales basis keyed by date (labor_sales_cents since
  // v19.09 — gross less third-party commission, discounts left in), for the
  // result block on a week being browsed.
  const [netByDate, setNetByDate] = useState({});
  // Hand-keyed training labor per date, in cents. Subtracted from scheduled
  // labor before the goal — and read by the iPad strip and the scorecard
  // waiver from the same table, so all three agree (v16.11).
  const [training, setTraining] = useState({});
  // Day editor (v16.15). Both numbers on a card are now edited in a popup
  // instead of live inputs on the grid. The old inline `%` box persisted on
  // every KEYSTROKE, so typing "25" wrote 2 and then 25 — and if focus left
  // in between, 2 stuck. It was also `type="number"`, which the mouse wheel
  // edits when focused. Between them Wednesday's target silently became 2%
  // (a $26,025 day goal) and Thu/Fri/Sat each drifted +1. Nothing on the page
  // commits until Save now.
  const [editDay, setEditDay] = useState(null);          // { iso, dow } | null
  const [editDraft, setEditDraft] = useState({ pct: '', training: '' });

  useEffect(() => {
    if (!window.supa) return;
    let cancelled = false;
    (async () => {
      const [{ data: rest }, { data: uts }, { data: lab }, { data: sal }, { data: trn }] = await Promise.all([
        window.supa.from('restaurants').select('sales_goal_config').eq('id', window.RESTAURANT_ID).maybeSingle(),
        window.supa.from('sales_untrackable_days').select('*').eq('restaurant_id', window.RESTAURANT_ID).order('the_date', { ascending: false }),
        window.supa.from('labor_daily').select('the_date, scheduled_cost_cents').eq('restaurant_id', window.RESTAURANT_ID).order('the_date', { ascending: false }).limit(60),
        window.supa.from('sales_daily').select('the_date, labor_sales_cents').eq('restaurant_id', window.RESTAURANT_ID).order('the_date', { ascending: false }).limit(60),
        window.supa.from('labor_training').select('the_date, amount_cents').eq('restaurant_id', window.RESTAURANT_ID).limit(200),
      ]);
      if (cancelled) return;
      const c = (rest && rest.sales_goal_config) || {};
      setCfg({
        open_hour: Number(c.open_hour) || 11,
        close_hour: Number(c.close_hour) || 22,
        targets: c.targets || {},
        curve: c.curve || {},
      });
      setUntrackable(uts || []);
      // Most recent occurrence of each weekday that has BOTH a labor snapshot
      // and a sales figure, so the % shown is a real pairing rather than two
      // numbers from different dates.
      // labor_sales_cents (v19.09) — gross less third-party commission, with
      // discounts left in. Same column the iPad strip and the prep waiver
      // read; all three have to share a denominator or the same day gets
      // different answers on different screens.
      const netByDate = Object.fromEntries((sal || []).map(r => [r.the_date, r.labor_sales_cents]));
      // Today is still running — its labor % is meaningless until close (at
      // 9am a full day's labor sits over an hour of sales and reads as 500%).
      // "Last Monday" means the most recent FINISHED Monday.
      const todayISO = (() => {
        const n = new Date();
        return n.getFullYear() + '-' + String(n.getMonth() + 1).padStart(2, '0') + '-' + String(n.getDate()).padStart(2, '0');
      })();
      const m = {};
      (lab || []).filter(r => String(r.the_date) < todayISO).forEach(r => {
        // Date-only string parsed as UTC would shift a day; split it instead.
        const [y, mo, d] = String(r.the_date).split('-').map(Number);
        const wd = new Date(y, mo - 1, d).getDay();
        if (m[wd] === undefined) {
          m[wd] = {
            date: r.the_date,
            laborCents: r.scheduled_cost_cents,
            netCents: netByDate[r.the_date],
          };
        }
      });
      setLaborByDow(m);
      setLabRows(lab || []);
      setNetByDate(netByDate);
      setTraining(Object.fromEntries((trn || []).map(r => [r.the_date, r.amount_cents])));
    })();
    return () => { cancelled = true; };
  }, []);

  // The shown week's scheduled labor, Sun..Sat. Snapshots first, then ask
  // Square for whatever is left. Re-runs when the week changes. A failed or
  // absent preview call leaves those days null, which renders as "—" rather
  // than as a wrong number.
  useEffect(() => {
    if (!labRows) return;
    let cancelled = false;
    const wkStart = sgWeekStart(sgAddDays(sgToday(), weekOffset * 7));
    const week = {};
    for (let i = 0; i < 7; i++) {
      const iso = sgAddDays(wkStart, i);
      const row = labRows.find(r => String(r.the_date) === iso);
      week[iso] = row
        ? { cents: row.scheduled_cost_cents, published: true, source: 'snapshot' }
        : null;
    }
    setWeekLabor({ ...week });
    if (!Object.keys(week).some(k => !week[k]) || !window.callEdge) return;
    (async () => {
      const { data } = await window.callEdge('labor-week-preview', {
        start: wkStart, end: sgAddDays(wkStart, 6),
      });
      if (cancelled || !data || !data.ok) return;
      (data.days || []).forEach(d => {
        if (week[d.the_date]) return;              // snapshot already won
        week[d.the_date] = d.published
          ? { cents: d.cost_cents, published: true, source: 'preview' }
          : { cents: null, published: false, source: 'preview' };
      });
      setWeekLabor({ ...week });
    })();
    return () => { cancelled = true; };
  }, [labRows, weekOffset]);

  if (!cfg) return <div className="portal-empty"><div style={{ fontSize: 14, color: 'var(--fg-3)' }}>Loading…</div></div>;

  const hours = [];
  for (let h = cfg.open_hour; h < cfg.close_hour; h++) hours.push(h);

  const curveFor = (d) => {
    const arr = cfg.curve[String(d)] || [];
    return hours.map((_, i) => Number(arr[i]) || 0);
  };
  const blocks = curveFor(dow);
  const blockTotal = blocks.reduce((s, b) => s + b, 0);

  const todayISO = sgToday();
  const weekStart = sgWeekStart(sgAddDays(todayISO, weekOffset * 7));
  const dateForDow = (d) => sgAddDays(weekStart, d);
  const laborForDow = (d) => (weekLabor || {})[dateForDow(d)] || null;
  const trainingFor = (iso) => Number(training[iso] || 0);
  // Scheduled labor less training — the divisor for every goal on this page.
  const netLaborFor = (iso, grossCents) =>
    grossCents == null ? null : Math.max(0, grossCents - trainingFor(iso));

  const openDayEditor = (iso, d) => {
    const pct = cfg.targets[String(d)] === undefined ? 25 : cfg.targets[String(d)];
    const t = trainingFor(iso);
    setEditDraft({ pct: String(pct), training: t ? String(t / 100) : '' });
    setEditDay({ iso, dow: d });
  };

  const saveDayEdit = async () => {
    if (!editDay) return;
    const { iso, dow: d } = editDay;
    const pct = Math.min(100, Math.max(1, Number(editDraft.pct)));
    if (!Number.isFinite(pct)) { setEditDay(null); return; }
    setEditDay(null);
    if (pct !== Number(cfg.targets[String(d)])) {
      await persist({ ...cfg, targets: { ...cfg.targets, [String(d)]: pct } });
    }
    await saveTraining(iso, editDraft.training);
  };

  // Week roll-up for the strip beside the picker (v16.14).
  //
  // The goal side is the whole week: every day's net labor and every day's
  // goal, so the blended target is what the week is actually aiming at.
  // The actual LABOR % counts FINISHED days only. Today has a whole day's
  // labor sitting against however much has been rung so far, so including it
  // reads high — measured 2026-08-08: 30.4% with today, 26.4% without, and
  // far worse earlier in a day. Same reason `laborByDow` skips today. Sales
  // so far DOES include today, because that's the question it answers; the
  // two figures cover different day sets on purpose and both labels say so.
  const weekStats = (() => {
    if (!weekLabor) return null;
    let goalSum = 0, laborSum = 0, actSalesCents = 0, actLaborCents = 0;
    let priced = 0, withSales = 0, finished = 0;
    for (let i = 0; i < 7; i++) {
      const iso = sgAddDays(weekStart, i);
      const wd = sgParse(iso).getDay();
      const pct = cfg.targets[String(wd)] === undefined ? 25 : Number(cfg.targets[String(wd)]);
      const wk = weekLabor[iso];
      const gross = wk && wk.cents;
      if (!gross || !(pct > 0)) continue;
      const net = netLaborFor(iso, gross);
      priced++;
      goalSum += sgRoundGoal((net / 100) / (pct / 100));
      laborSum += net;
      const sales = netByDate[iso];
      if (sales != null && sales > 0) {
        withSales++;
        actSalesCents += sales;                       // includes today
        if (iso < todayISO) { finished++; actLaborCents += net; }
      }
    }
    // Denominator for the labor % is the finished days only, to match its
    // numerator. Recomputed here rather than reusing actSalesCents.
    let finishedSalesCents = 0;
    for (let i = 0; i < 7; i++) {
      const iso = sgAddDays(weekStart, i);
      if (iso >= todayISO) continue;
      const sales = netByDate[iso];
      if (sales != null && sales > 0) finishedSalesCents += sales;
    }
    return {
      priced, withSales, finished,
      goalDollars: goalSum,
      targetPct: goalSum > 0 ? ((laborSum / 100) / goalSum) * 100 : null,
      actSalesDollars: actSalesCents / 100,
      actPct: finishedSalesCents > 0 ? (actLaborCents / finishedSalesCents) * 100 : null,
    };
  })();

  // Persist a training figure and re-score the day. The waiver lives in
  // scorecard_snapshot_range, and scorecard_daily is a STORED snapshot — the
  // number would otherwise be right on this page and stale on the Crew Lead
  // Scorecard until the next nightly run (§9).
  const saveTraining = async (iso, dollars) => {
    const cents = Math.max(0, Math.round((Number(dollars) || 0) * 100));
    const prev = Number(training[iso] || 0);
    if (cents === prev) return;
    setTraining(t => ({ ...t, [iso]: cents }));
    const { error } = await window.supa.from('labor_training').upsert({
      restaurant_id: window.RESTAURANT_ID,
      the_date: iso,
      amount_cents: cents,
      updated_at: new Date().toISOString(),
    }, { onConflict: 'restaurant_id,the_date' }).select();
    if (error) {
      setTraining(t => ({ ...t, [iso]: prev }));           // put it back
      setToast('Save failed: ' + error.message);
      return;
    }
    setToast(cents ? 'Training labor saved for ' + sgShortDate(iso) : 'Training labor cleared');
    // Best-effort re-score; the nightly run is the backstop if it's refused.
    if (window.supa.rpc) {
      window.supa.rpc('scorecard_snapshot_range', { rid: window.RESTAURANT_ID, d0: iso, d1: iso });
    }
  };

  // The result block under each card. When the day being shown has already
  // finished, show THAT day's result — browsing back a week and being told
  // about a date in a different week is the same "which week is this?"
  // confusion the dated headers exist to kill. Only fall back to the most
  // recent finished occurrence when the shown day hasn't happened yet.
  const resultForDow = (d) => {
    const iso = dateForDow(d);
    if (iso < todayISO && labRows) {
      const row = labRows.find(r => String(r.the_date) === iso);
      if (row) return { date: iso, laborCents: row.scheduled_cost_cents, netCents: netByDate[iso] };
    }
    return laborByDow[d] || null;
  };

  const targetPct = cfg.targets[String(dow)] === undefined ? 25 : Number(cfg.targets[String(dow)]);
  // The hourly shape prices off THIS week's scheduled labor for the selected
  // weekday, so the per-hour dollars match the goal on the card above it.
  const selLabor = laborForDow(dow);
  const labourCents = selLabor && netLaborFor(dateForDow(dow), selLabor.cents);
  const goalDollars = (labourCents && targetPct > 0)
    ? sgRoundGoal((labourCents / 100) / (targetPct / 100)) : null;

  const persist = async (next) => {
    setCfg(next);
    setSaving(true);
    const { error } = await window.supa.from('restaurants')
      .update({ sales_goal_config: next }).eq('id', window.RESTAURANT_ID);
    setSaving(false);
    if (error) setToast('Save failed: ' + error.message);
  };

  const setBlock = (hourIdx, value) => {
    const arr = curveFor(dow).slice();
    arr[hourIdx] = value;
    persist({ ...cfg, curve: { ...cfg.curve, [String(dow)]: arr } });
  };
  const addUntrackable = async () => {
    const from = newFrom, to = newTo || newFrom;
    if (!from) return;
    // A range is stored as one row per day — simpler to query than a range
    // type, and the list stays explicit about exactly which days are excluded.
    const rows = [];
    for (let d = new Date(from + 'T12:00:00'); d <= new Date(to + 'T12:00:00'); d.setDate(d.getDate() + 1)) {
      rows.push({
        restaurant_id: window.RESTAURANT_ID,
        the_date: d.toISOString().slice(0, 10),
        note: newNote.trim() || null,
      });
      if (rows.length > 60) break;  // fat-finger guard on the end date
    }
    const { error } = await window.supa.from('sales_untrackable_days')
      .upsert(rows, { onConflict: 'restaurant_id,the_date' });
    if (error) { setToast('Save failed: ' + error.message); return; }
    const { data } = await window.supa.from('sales_untrackable_days')
      .select('*').eq('restaurant_id', window.RESTAURANT_ID).order('the_date', { ascending: false });
    setUntrackable(data || []);
    setNewFrom(''); setNewTo(''); setNewNote('');
    setToast(rows.length === 1 ? 'Day marked untrackable' : rows.length + ' days marked untrackable');
  };

  const removeUntrackable = async (date) => {
    await window.supa.from('sales_untrackable_days').delete()
      .eq('restaurant_id', window.RESTAURANT_ID).eq('the_date', date);
    setUntrackable(u => u.filter(x => x.the_date !== date));
  };

  return (
    <div className="portal-page-wide" style={{ maxWidth: 1100 }}>
      <PPageHeader
        title="Sales vs. Labor"
        subtitle={<span>
          Scheduled labor ÷ the weekday’s target = the sales that day is staffed for. The
          hourly shape below spreads it across the day, so the iPad can report{' '}
          <b>crew load</b> — how busy the day is running against the crew on it — rather
          than just how far they’ve got. Over 100% is what waives incomplete prep.{' '}
          {saving ? <b>Saving…</b> : null}
        </span>}
      />

      <PToast message={toast} open={!!toast} onClose={() => setToast('')} />

      {/* Day editor. Shows the resulting goal live as you type — the whole
          point: a target of 2 instead of 25 turned a $2,075 day into $26,025
          and nothing on screen said so until the weekly total looked absurd. */}
      {editDay && (() => {
        const { iso, dow: d } = editDay;
        const wk = (weekLabor || {})[iso];
        const gross = wk && wk.cents;
        const pctNum = Number(editDraft.pct);
        const trainCentsDraft = Math.max(0, Math.round((Number(editDraft.training) || 0) * 100));
        const netPreview = gross == null ? null : Math.max(0, gross - trainCentsDraft);
        const goalPreview = (netPreview && pctNum > 0)
          ? sgRoundGoal((netPreview / 100) / (pctNum / 100)) : null;
        const valid = Number.isFinite(pctNum) && pctNum >= 1 && pctNum <= 100;
        return (
          <PModal open onClose={() => setEditDay(null)}
            title={SG_DOW[d] + ' · ' + sgShortDate(iso)} width={420}
            footer={<>
              <PBtn variant="secondary" onClick={() => setEditDay(null)}>Cancel</PBtn>
              <PBtn variant="primary" disabled={!valid} onClick={saveDayEdit}>Save</PBtn>
            </>}>
            <div style={{ padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 16 }}>
              <PField label="Labor target" hint={'Scheduled labor ÷ this = the day\u2019s sales goal. Applies to every ' + SG_DOW[d] + ', not just this date.'}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <input type="number" min="1" max="100" step="0.5" autoFocus
                    value={editDraft.pct}
                    onChange={e => setEditDraft(v => ({ ...v, pct: e.target.value }))}
                    onWheel={e => e.currentTarget.blur()}
                    className="portal-input"
                    style={{ width: 90, height: 34, textAlign: 'right', fontFamily: 'var(--font-num)', fontWeight: 600 }} />
                  <span style={{ fontSize: 13, color: 'var(--fg-3)' }}>%</span>
                </div>
              </PField>

              <PField label="Training labor" hint="Dollars spent on someone learning rather than producing, on this date only. Comes off before the goal.">
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <span style={{ fontSize: 13, color: 'var(--fg-3)' }}>−$</span>
                  <input type="number" min="0" step="1" placeholder="0"
                    value={editDraft.training}
                    onChange={e => setEditDraft(v => ({ ...v, training: e.target.value }))}
                    onWheel={e => e.currentTarget.blur()}
                    className="portal-input"
                    style={{ width: 90, height: 34, textAlign: 'right', fontFamily: 'var(--font-num)', fontWeight: 600 }} />
                </div>
              </PField>

              <div style={{ borderTop: '1px solid var(--border-2)', paddingTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
                <SgRow label="Sched labor" value={gross ? sgMoney(gross) : '—'} />
                <SgRow label="Training" value={'−' + sgMoney(trainCentsDraft)} />
                <SgRow label="Net labor" value={netPreview == null ? '—' : sgMoney(netPreview)} />
                <SgRow label="Goal" value={goalPreview == null ? '—' : '$' + goalPreview.toLocaleString()} strong />
              </div>
            </div>
          </PModal>
        );
      })()}

      {/* ---- Weekday targets ---- */}
      <SgSection title="Labor target by day"
        hint="If the day’s scheduled labor lands at or under this share of sales (gross, less third-party commission — discounts are not taken off), incomplete prep doesn’t deduct from the Crew Lead Scorecard. Checklist, Temp Log and Side Job still count.">
        {/* Week picker. The targets themselves are per-WEEKDAY settings and
            don't move with this — only the schedule, goals and results shown
            underneath them do. Ryan publishes one to two weeks ahead, so the
            forward direction is the point; back is for reference. */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
          {/* Icon-only, so it needs a name of its own — it read as a bare
              "button" in the accessibility tree otherwise. */}
          <PBtn variant="secondary" size="sm" title="Previous week" ariaLabel="Previous week"
            onClick={() => setWeekOffset(weekOffset - 1)}>
            <i className="ri-arrow-left-s-line" />
          </PBtn>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-1)', minWidth: 150, textAlign: 'center' }}>
            {sgShortDate(weekStart)} – {sgShortDate(sgAddDays(weekStart, 6))}
          </div>
          <PBtn variant="secondary" size="sm" title="Next week" ariaLabel="Next week"
            onClick={() => setWeekOffset(weekOffset + 1)}>
            <i className="ri-arrow-right-s-line" />
          </PBtn>
          <span style={{ fontSize: 11.5, color: 'var(--fg-3)' }}>
            {weekOffset === 0 ? 'This week'
              : weekOffset === 1 ? 'Next week'
              : weekOffset === -1 ? 'Last week'
              : (weekOffset > 0 ? 'In ' + weekOffset + ' weeks' : Math.abs(weekOffset) + ' weeks ago')}
          </span>
          {weekOffset !== 0 && (
            <PBtn variant="secondary" size="sm" onClick={() => setWeekOffset(0)}>This week</PBtn>
          )}

          <span style={{ flex: 1 }} />

          {weekStats && weekStats.priced > 0 && (
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 18 }}>
              <SgStat label="Labor % goal"
                value={weekStats.targetPct === null ? '—' : weekStats.targetPct.toFixed(1) + '%'}
                title={'Blended target across the ' + weekStats.priced + ' scheduled day(s) — total net labor ÷ total sales goal.'} />
              <SgStat label="Sales goal" value={'$' + weekStats.goalDollars.toLocaleString()}
                title={'Sum of every scheduled day\u2019s goal this week.'} />
              <SgStat label={'Actual labor' + (weekStats.finished ? ' · ' + weekStats.finished + 'd' : '')}
                value={weekStats.actPct === null ? '—' : weekStats.actPct.toFixed(1) + '%'}
                color={weekStats.actPct === null || weekStats.targetPct === null ? null
                  : (weekStats.actPct <= weekStats.targetPct ? '#047857' : 'var(--danger)')}
                title={'Net labor ÷ sales across the ' + weekStats.finished + ' finished day(s). Sales here is gross less third-party commission; discounts are not taken off. Today is left out — a whole day of labor against a part-day of sales reads high.'} />
              <SgStat label="Sales so far"
                value={'$' + Math.round(weekStats.actSalesDollars).toLocaleString()}
                title={'Sales across ' + weekStats.withSales + ' of 7 days, including today so far. Gross less third-party commission; discounts are not taken off.'} />
            </div>
          )}
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 8 }}>
          {SG_DOW.map((name, d) => {
            const pct = cfg.targets[String(d)] === undefined ? 25 : cfg.targets[String(d)];
            // Top half: THIS week's occurrence of this weekday. Before v16.09
            // the goal came off the last FINISHED occurrence instead, so on
            // 2026-08-05 this card read $2,775 (last Wednesday's $693.99 labor)
            // while the iPad was judging the crew against $2,300 from today's
            // own $577.53 — a $475 disagreement about the day's own number.
            const iso = dateForDow(d);
            const wk = laborForDow(d);
            const wkCents = wk && wk.cents;
            const trainCents = trainingFor(iso);
            const netCents = netLaborFor(iso, wkCents);
            const goal = (netCents && pct > 0) ? sgRoundGoal((netCents / 100) / (pct / 100)) : null;
            // Bottom half: the most recent FINISHED occurrence. For a weekday
            // already past this week that IS the same date — plan on top,
            // result underneath — so both blocks carry their date and the
            // reader never has to guess which week they're looking at.
            const last = resultForDow(d);
            const lc = last && last.laborCents;
            const nc = last && last.netCents;
            const actualPct = (lc && nc) ? (100 * lc / nc) : null;
            const cleared = actualPct !== null && actualPct <= pct;
            return (
              <div key={d} style={{
                border: '1px solid ' + (d === dow ? 'var(--fg-1)' : 'var(--border-2)'),
                borderRadius: 10, padding: '10px 11px', background: 'var(--bg-surface)',
                display: 'flex', flexDirection: 'column',
              }}>
                <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
                  {SG_DOW_SHORT[d]}
                </div>
                <div style={{ fontSize: 10, color: iso === todayISO ? 'var(--fg-1)' : 'var(--fg-3)',
                  fontWeight: iso === todayISO ? 600 : 400, marginTop: 1 }}>
                  {sgShortDate(iso)}{iso === todayISO ? ' · today' : ''}
                </div>
                {/* Click to edit — see the editDay comment. A live input here
                    is what let a stray keystroke or scroll wheel rewrite the
                    number that sets the crew's goal. */}
                <button type="button" onClick={() => openDayEditor(iso, d)}
                  title={'Edit ' + SG_DOW[d] + ' target and training labor'}
                  style={{
                    display: 'flex', alignItems: 'baseline', gap: 3, marginTop: 6,
                    background: 'transparent', border: '1px solid var(--border-1)',
                    borderRadius: 7, padding: '4px 8px', cursor: 'pointer', width: 'fit-content',
                  }}>
                  <span style={{ fontSize: 15, fontWeight: 600, fontFamily: 'var(--font-num)', color: 'var(--fg-1)' }}>{pct}</span>
                  <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>%</span>
                  <i className="ri-pencil-line" style={{ fontSize: 11, color: 'var(--fg-3)', marginLeft: 2 }} />
                </button>

                {/* Label left, number right, one fact per row. The old version
                    ran all of this together as a sentence, so it wrapped at a
                    different word in every card and nothing lined up. Fixed rows
                    mean the seven cards read as columns of a table. */}
                <div style={{ marginTop: 10, paddingTop: 8, borderTop: '1px solid var(--border-2)' }}>
                  <SgRow label="Sched labor"
                    value={wkCents ? sgMoney(wkCents) : (weekLabor ? '—' : '…')} />
                  {/* Always shown, even at zero — a row that appeared only on
                      training days would leave the seven cards different
                      heights. Click opens the same editor as the % box. */}
                  <div onClick={() => openDayEditor(iso, d)}
                    style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
                      gap: 6, lineHeight: 1.6, cursor: 'pointer' }}>
                    <span style={{ fontSize: 10.5, color: 'var(--fg-3)', whiteSpace: 'nowrap' }}>Training</span>
                    <span style={{
                      fontSize: 11.5, fontFamily: 'var(--font-num)', fontWeight: 500, whiteSpace: 'nowrap',
                      color: trainCents ? 'var(--fg-2)' : 'var(--fg-3)',
                      borderBottom: '1px dashed var(--border-1)',
                    }}>{trainCents ? '−' + sgMoney(trainCents) : '−$0'}</span>
                  </div>
                  <SgRow label="Net labor"
                    value={netCents != null && wkCents ? sgMoney(netCents) : (weekLabor ? '—' : '…')} />
                  <SgRow label="Goal" value={goal !== null ? '$' + goal.toLocaleString() : '—'} strong />
                  {weekLabor && wk && !wk.published && (
                    <div style={{ fontSize: 9.5, color: 'var(--fg-3)', fontStyle: 'italic', marginTop: 2, lineHeight: 1.35 }}>
                      Schedule not published
                    </div>
                  )}
                  {weekLabor && !wk && (
                    <div style={{ fontSize: 9.5, color: 'var(--fg-3)', fontStyle: 'italic', marginTop: 2, lineHeight: 1.35 }}>
                      No schedule found
                    </div>
                  )}
                </div>

                {last ? (
                  <div style={{ marginTop: 8, paddingTop: 7, borderTop: '1px solid var(--border-2)' }}>
                    <div style={{ fontSize: 9.5, fontWeight: 700, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 3 }}>
                      {last.date === iso ? 'Result' : 'Last ' + SG_DOW_SHORT[d]} · {sgShortDate(last.date)}
                    </div>
                    <SgRow label="Labor" value={sgMoney(lc)} />
                    <SgRow label="Sales" value={nc != null ? sgMoney(nc) : '—'} />
                    <SgRow label="Actual"
                      value={actualPct !== null ? actualPct.toFixed(1) + '%' : '—'}
                      color={actualPct === null ? null : (cleared ? '#047857' : 'var(--danger)')}
                      strong />
                  </div>
                ) : (
                  <div style={{ marginTop: 8, paddingTop: 7, borderTop: '1px solid var(--border-2)',
                    fontSize: 10.5, color: 'var(--fg-3)', fontStyle: 'italic' }}>
                    No finished {SG_DOW_SHORT[d]} yet
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </SgSection>

      {/* ---- Hourly shape ---- */}
      <SgSection title="Hourly shape"
        hint="Click a bar to set how busy that hour usually is. These are relative weights, not dollars — three blocks at noon and one at 2pm just means noon is worth three times as much.">
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 16 }}>
          {SG_DOW.map((name, d) => {
            const on = d === dow;
            const drawn = (cfg.curve[String(d)] || []).some(b => Number(b) > 0);
            return (
              <button key={d} onClick={() => setDow(d)} style={{
                padding: '6px 12px', borderRadius: 8, cursor: 'pointer', fontSize: 12.5,
                fontWeight: on ? 600 : 500,
                background: on ? 'var(--fg-1)' : 'var(--bg-surface)',
                color: on ? '#fff' : 'var(--fg-2)',
                border: '1px solid ' + (on ? 'var(--fg-1)' : 'var(--border-1)'),
              }}>
                {SG_DOW_SHORT[d]}
                {!drawn && <span style={{ opacity: 0.55, marginLeft: 6, fontSize: 11 }}>—</span>}
              </button>
            );
          })}
          <div style={{ flex: 1 }} />
          <PBtn variant="secondary" size="sm"
            onClick={() => persist({ ...cfg, curve: { ...cfg.curve, [String(dow)]: hours.map(() => 0) } })}>
            Clear {SG_DOW_SHORT[dow]}
          </PBtn>
          <PBtn variant="secondary" size="sm"
            onClick={() => {
              // Copying beats redrawing: most weekdays look like each other.
              const src = curveFor(dow);
              const nextCurve = { ...cfg.curve };
              for (let d = 0; d < 7; d++) nextCurve[String(d)] = src.slice();
              persist({ ...cfg, curve: nextCurve });
            }}>
            Copy to all days
          </PBtn>
        </div>

        <div style={{
          display: 'flex', alignItems: 'flex-end', gap: 6,
          padding: '14px 12px 10px', border: '1px solid var(--border-2)',
          borderRadius: 10, background: 'var(--bg-surface)', overflowX: 'auto',
        }}>
          {hours.map((h, i) => {
            const val = blocks[i];
            const share = blockTotal > 0 ? val / blockTotal : 0;
            return (
              <div key={h} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, minWidth: 46, flex: 1 }}>
                <div style={{ fontSize: 10, color: 'var(--fg-3)', fontFamily: 'var(--font-num)', height: 13 }}>
                  {blockTotal > 0 && val > 0 ? Math.round(share * 100) + '%' : ''}
                </div>
                {/* Stacked top-down so index 0 is the tallest block. */}
                <div style={{ display: 'flex', flexDirection: 'column-reverse', gap: 2 }}>
                  {Array.from({ length: SG_MAX_BLOCKS }, (_, b) => {
                    const filled = b < val;
                    return (
                      <button
                        key={b}
                        title={`${sgHourLabel(h)} — ${b + 1} block${b ? 's' : ''}`}
                        onClick={() => setBlock(i, val === b + 1 ? b : b + 1)}
                        style={{
                          width: '100%', height: 14, minWidth: 34, borderRadius: 3, cursor: 'pointer',
                          background: filled ? 'var(--fg-1)' : 'var(--bg-sunken)',
                          border: '1px solid ' + (filled ? 'var(--fg-1)' : 'var(--border-2)'),
                          padding: 0,
                        }}
                      />
                    );
                  })}
                </div>
                <div style={{ fontSize: 10.5, color: 'var(--fg-3)', fontFamily: 'var(--font-num)' }}>
                  {sgHourLabel(h)}
                </div>
                <div style={{ fontSize: 10, color: 'var(--fg-3)', fontFamily: 'var(--font-num)', height: 13 }}>
                  {goalDollars !== null && blockTotal > 0 && val > 0
                    ? '$' + Math.round(goalDollars * share) : ''}
                </div>
              </div>
            );
          })}
        </div>
        <div style={{ fontSize: 11.5, color: 'var(--fg-3)', marginTop: 8, lineHeight: 1.5 }}>
          {blockTotal === 0
            ? <>No shape drawn for {SG_DOW[dow]} — the strip will spread the goal evenly across the day.</>
            : <>Top row is each hour’s share; bottom row is the dollars that lands on, using this
              week’s {SG_DOW[dow]} scheduled labor. Change the target above and these move.</>}
        </div>
      </SgSection>

      {/* ---- Open hours ---- */}
      <SgSection title="Trading hours" hint="Sets the columns above. Leave headroom if you plan to extend closing.">
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <input type="number" min="0" max="23" className="portal-input"
            value={cfg.open_hour}
            onChange={e => persist({ ...cfg, open_hour: Math.min(23, Math.max(0, Number(e.target.value) || 0)) })}
            style={{ width: 68, height: 32, textAlign: 'right', fontFamily: 'var(--font-num)' }} />
          <span style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>to</span>
          <input type="number" min="1" max="24" className="portal-input"
            value={cfg.close_hour}
            onChange={e => persist({ ...cfg, close_hour: Math.min(24, Math.max(1, Number(e.target.value) || 0)) })}
            style={{ width: 68, height: 32, textAlign: 'right', fontFamily: 'var(--font-num)' }} />
          <span style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>
            ({sgHourLabel(cfg.open_hour)} – {sgHourLabel(cfg.close_hour)}, {hours.length} hours)
          </span>
        </div>
      </SgSection>

      {/* ---- Untrackable days ---- */}
      <SgSection title="Untrackable days"
        hint="Special events where you’ve scheduled extra labor. The iPad shows “Untrackable sales today” instead of a pace, and prep is NOT waived — adjust those by hand on the scorecard if you want to.">
        {/* One row. The date inputs need an explicit width — left to flex they
            stretch to fill and push everything onto its own line. */}
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 12 }}>
          <input type="date" className="portal-input" value={newFrom}
            onChange={e => setNewFrom(e.target.value)}
            style={{ height: 32, width: 150, flex: '0 0 auto' }} />
          <span style={{ fontSize: 12.5, color: 'var(--fg-3)', flex: '0 0 auto' }}>to</span>
          <input type="date" className="portal-input" value={newTo}
            onChange={e => setNewTo(e.target.value)}
            style={{ height: 32, width: 150, flex: '0 0 auto' }} />
          <input className="portal-input" placeholder="Why? e.g. school catering"
            value={newNote} onChange={e => setNewNote(e.target.value)}
            style={{ height: 32, flex: '1 1 auto', minWidth: 120 }} />
          <PBtn variant="primary" size="sm" onClick={addUntrackable} disabled={!newFrom}>Add</PBtn>
        </div>
        {untrackable.length === 0 ? (
          <div style={{ fontSize: 12.5, color: 'var(--fg-3)', fontStyle: 'italic' }}>None — every day is tracked.</div>
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {untrackable.map(u => (
              <div key={u.the_date} style={{
                display: 'flex', alignItems: 'center', gap: 10, padding: '8px 11px',
                border: '1px solid var(--border-2)', borderRadius: 8, background: 'var(--bg-surface)',
              }}>
                <span style={{ fontSize: 13, fontWeight: 500, fontFamily: 'var(--font-num)' }}>{u.the_date}</span>
                <span style={{ fontSize: 12, color: 'var(--fg-3)', flex: 1 }}>{u.note || '—'}</span>
                <button onClick={() => removeUntrackable(u.the_date)} title="Remove" style={{ cursor: 'pointer', lineHeight: 0 }}>
                  <PIcon name="trash" size={14} color="var(--danger)" />
                </button>
              </div>
            ))}
          </div>
        )}
      </SgSection>
    </div>
  );
};

window.SalesGoal = SalesGoal;
