// TemperatureLog.jsx — Admin Portal page for the Temperature Log feature.
//
// Three tabs (one nav entry in the sidebar):
//   • Log       — historical log viewer (date picker → all checkpoints that
//                 day, with summary stats + drill-down to per-item readings)
//   • Catalog   — items that need temperature checks (CRUD)
//   • Schedule  — daily checkpoint times the iPad surfaces (e.g. 11:00, 15:00)
//
// Data model (v5.43):
//   temperature_items     — id, name, sort_order, archived
//   temperature_schedule  — id, time_of_day, label, sort_order, archived
//   temperature_logs      — id, the_date, schedule_id, finalized_at, ...
//   temperature_readings  — id, log_id, item_id, band, value, created_at
//
// Bands are the buckets staff can quick-pick on the iPad. When they type a
// custom number on the keypad, we store the exact value AND derive the band
// from it (under_140 / between_140_165 / over_165) so summaries roll up
// cleanly across mixed quick-pick + custom readings.

const { useState: useT, useMemo: useMemoT, useEffect: useEffectT } = React;

// ------------------------------------------------------------------
// Constants — band metadata, formatters
// ------------------------------------------------------------------
// Color semantics chosen for food-safety legibility:
//   <140    → red   (out-of-spec hot-holding — flag it)
//   140-165 → gray  (the normal in-spec hot-hold range — nothing to flag)
//   >165    → amber (running hot — heads-up for quality / over-cook risk)
const TEMP_BANDS = [
  { id: 'under_140',       label: '<140°F',      shortLabel: '<140',    color: '#B91C1C', bg: '#FEE2E2', border: '#FCA5A5' },
  { id: 'between_140_165', label: '140–165°F',   shortLabel: '140-165', color: '#52525B', bg: '#F4F4F5', border: '#D4D4D8' },
  { id: 'over_165',        label: '>165°F',      shortLabel: '>165',    color: '#92400E', bg: '#FEF3C7', border: '#F0CB7D' },
];
const BAND_BY_ID = Object.fromEntries(TEMP_BANDS.map(b => [b.id, b]));

window.TEMP_BANDS = TEMP_BANDS;
window.TEMP_BAND_BY_ID = BAND_BY_ID;

// Derive band from a raw numeric value. Mirrors the iPad logic so a custom
// keypad entry always slots into the same bucket.
window.deriveTemperatureBand = function (value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return null;
  if (n < 140) return 'under_140';
  if (n <= 165) return 'between_140_165';
  return 'over_165';
};

// "HH:MM:SS" or "HH:MM" → "11:00 AM"
const fmtTime12 = (timeStr) => {
  if (!timeStr) return '';
  const [hStr, mStr] = String(timeStr).split(':');
  const h = Number(hStr); const m = Number(mStr) || 0;
  if (!Number.isFinite(h)) return timeStr;
  const ap = h >= 12 ? 'PM' : 'AM';
  const h12 = ((h + 11) % 12) + 1;
  return `${h12}:${String(m).padStart(2, '0')} ${ap}`;
};

// Date → "YYYY-MM-DD" (restaurant local). Matches Daily Prep convention.
const isoKey = (d) =>
  `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;

// ------------------------------------------------------------------
// Mappers
// ------------------------------------------------------------------
const tempItemFromRow = (r) => ({
  id: r.id, name: r.name,
  sortOrder: Number(r.sort_order) || 0,
  archived: !!r.archived,
  // "Basic" items are always-on staples (hot rice, white rice, soups) —
  // auto-included on every checkpoint. Non-basic items are added by
  // crew on the iPad as they spot them on the line that day.
  isBasic: !!r.is_basic,
});
const tempItemToRow = (o) => ({
  id: o.id, name: o.name,
  sort_order: Number(o.sortOrder) || 0,
  archived: !!o.archived,
  is_basic: !!o.isBasic,
});
const tempSchedFromRow = (r) => ({
  id: r.id,
  // Postgres `time` returns "HH:MM:SS". The form edits "HH:MM"; we
  // tolerate both here.
  timeOfDay: (r.time_of_day || '').slice(0, 5),
  label: r.label || '',
  sortOrder: Number(r.sort_order) || 0,
  archived: !!r.archived,
});
const tempSchedToRow = (o) => ({
  id: o.id,
  time_of_day: o.timeOfDay,
  label: o.label || null,
  sort_order: Number(o.sortOrder) || 0,
  archived: !!o.archived,
});

// ------------------------------------------------------------------
// Top-level page
// ------------------------------------------------------------------
const TemperatureLog = () => {
  const [tab, setTab] = useT('log');
  const [toast, setToast] = useT('');
  // Log-tab date lifted here so the unified page header can host the date
  // picker (and the date-aware Crew Lead strip) on its right end.
  const [logDate, setLogDate] = useT(() => new Date());
  const logDateKey = isoKey(logDate);

  const [items, setItems] = window.useSupaList('temperature_items', {
    fromRow: tempItemFromRow,
    toRow: tempItemToRow,
    initial: () => window.SAMPLE_TEMPERATURE_ITEMS || [],
  });
  const [schedule, setSchedule] = window.useSupaList('temperature_schedule', {
    fromRow: tempSchedFromRow,
    toRow: tempSchedToRow,
    initial: () => window.SAMPLE_TEMPERATURE_SCHEDULE || [],
  });

  return (
    <div className="portal-page-wide" style={{ maxWidth: 1280 }}>
      <PPageHeader
        title="Temperature Log"
        subtitle="Scheduled temperature checks. Crew records on the iPad; you set the catalog and times here."
        right={tab === 'log' ? <PDatePicker value={logDate} onChange={setLogDate} /> : null}
        crewLead={tab === 'log'}
        crewLeadDate={logDateKey}
      />

      <div style={{ display: 'flex', gap: 4, marginBottom: 18, borderBottom: '1px solid var(--border-2)' }}>
        {[
          { id: 'log',      label: 'Log' },
          { id: 'catalog',  label: 'Catalog' },
          { id: 'schedule', label: 'Schedule' },
        ].map(t => (
          <button
            key={t.id}
            onClick={() => setTab(t.id)}
            style={{
              padding: '8px 14px',
              fontSize: 13.5, fontWeight: 500,
              color: tab === t.id ? 'var(--fg-1)' : 'var(--fg-3)',
              background: 'transparent',
              border: 'none',
              borderBottom: '2px solid ' + (tab === t.id ? 'var(--fg-1)' : 'transparent'),
              marginBottom: -1,
              cursor: 'pointer',
            }}
          >{t.label}</button>
        ))}
      </div>

      {tab === 'log' && (
        <LogTab items={items} schedule={schedule} date={logDate} setDate={setLogDate} />
      )}
      {tab === 'catalog' && (
        <CatalogTab items={items} setItems={setItems} setToast={setToast} />
      )}
      {tab === 'schedule' && (
        <ScheduleTab schedule={schedule} setSchedule={setSchedule} setToast={setToast} />
      )}

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

// ------------------------------------------------------------------
// Tab 1 — Log (historical viewer)
// ------------------------------------------------------------------
const LogTab = ({ items, schedule, date, setDate }) => {
  // Date is owned by the page header (PPageHeader) now; we derive the ISO
  // key for queries. `setDate` kept in the signature for future in-tab use.
  const dateKey = isoKey(date);
  const [logs, setLogs] = useT([]);             // [{ id, schedule_id, finalized_at, finalized_by_staff_id }]
  const [readings, setReadings] = useT({});     // { logId: [{ item_id, band, value, created_at }] }
  const [expanded, setExpanded] = useT(null);   // logId currently expanded

  // Fetch logs + readings for the selected date.
  useEffectT(() => {
    if (!window.supa) return;
    let cancelled = false;
    const refetch = async () => {
      const { data: logRows } = await window.supa
        .from('temperature_logs')
        .select('*')
        .eq('restaurant_id', window.RESTAURANT_ID)
        .eq('the_date', dateKey);
      if (cancelled || !logRows) { setLogs([]); setReadings({}); return; }
      setLogs(logRows);
      if (logRows.length === 0) { setReadings({}); return; }
      const ids = logRows.map(l => l.id);
      const { data: readingRows } = await window.supa
        .from('temperature_readings')
        .select('*')
        .in('log_id', ids);
      if (cancelled) return;
      const byLog = {};
      (readingRows || []).forEach(r => {
        (byLog[r.log_id] = byLog[r.log_id] || []).push(r);
      });
      setReadings(byLog);
    };
    refetch();
    const ch = window.supa
      .channel('portal-temp-logs:' + window.RESTAURANT_ID + ':' + dateKey)
      .on('postgres_changes', {
        event: '*', schema: 'public', table: 'temperature_logs',
        filter: 'restaurant_id=eq.' + window.RESTAURANT_ID,
      }, () => refetch())
      .on('postgres_changes', {
        event: '*', schema: 'public', table: 'temperature_readings',
      }, () => refetch())
      .subscribe();
    return () => { cancelled = true; window.supa.removeChannel(ch); };
  }, [dateKey]);

  const itemById = useMemoT(() => Object.fromEntries(items.map(i => [i.id, i])), [items]);
  const schedById = useMemoT(() => Object.fromEntries(schedule.map(s => [s.id, s])), [schedule]);

  // Build display rows: every non-archived scheduled checkpoint for the day,
  // even if no log was started yet (so the manager sees "11:00 AM — not yet
  // recorded" alongside finalized entries).
  const rows = useMemoT(() => {
    const logsBySched = Object.fromEntries(logs.map(l => [l.schedule_id, l]));
    return schedule
      .filter(s => !s.archived)
      .slice()
      .sort((a, b) => (a.timeOfDay || '').localeCompare(b.timeOfDay || ''))
      .map(s => ({
        schedule: s,
        log: logsBySched[s.id] || null,
        rs: logsBySched[s.id] ? (readings[logsBySched[s.id].id] || []) : [],
      }));
  }, [schedule, logs, readings]);

  return (
    <>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', marginBottom: 16 }}>
        <div style={{ fontSize: 12, color: 'var(--fg-3)' }}>
          {schedule.filter(s => !s.archived).length} scheduled checkpoint{schedule.filter(s => !s.archived).length === 1 ? '' : 's'} · {items.filter(i => !i.archived).length} catalog item{items.filter(i => !i.archived).length === 1 ? '' : 's'}
        </div>
      </div>

      {schedule.filter(s => !s.archived).length === 0 && (
        <div className="portal-card" style={{ padding: 40, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
          No scheduled checkpoints yet. Add one in the <b>Schedule</b> tab to start logging.
        </div>
      )}

      {rows.map(row => {
        const summary = readingsSummary(row.rs);
        const finalized = !!row.log?.finalized_at;
        const staff = row.log?.finalized_by_staff_id
          ? (window.SAMPLE_STAFF || []).find(s => s.id === row.log.finalized_by_staff_id)
          : null;
        const isOpen = expanded === row.schedule.id;
        return (
          <div key={row.schedule.id} className="portal-card" style={{ marginBottom: 12, padding: 0 }}>
            <div
              onClick={() => setExpanded(isOpen ? null : row.schedule.id)}
              style={{
                display: 'flex', alignItems: 'center', gap: 16,
                padding: '14px 18px',
                cursor: 'pointer',
                borderBottom: isOpen ? '1px solid var(--border-2)' : 'none',
              }}
            >
              <div style={{ minWidth: 110 }}>
                <div style={{ fontSize: 17, fontWeight: 600, letterSpacing: '-0.015em' }}>
                  {fmtTime12(row.schedule.timeOfDay)}
                </div>
                {row.schedule.label && (
                  <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 2 }}>{row.schedule.label}</div>
                )}
              </div>

              {!row.log ? (
                <>
                  <div style={{ flex: 1, fontSize: 13, color: 'var(--fg-3)', fontStyle: 'italic' }}>
                    Not yet recorded · expand to log on behalf
                  </div>
                  <PIcon name={isOpen ? 'chevronD' : 'chevronR'} size={14} color="var(--fg-3)" />
                </>
              ) : (
                <>
                  <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
                    <SummaryPill count={summary.total} label="tested" tone="solid" />
                    {TEMP_BANDS.map(b => (
                      <SummaryPill key={b.id} count={summary.byBand[b.id] || 0} label={b.shortLabel}
                        bandColor={b.color} bandBg={b.bg} bandBorder={b.border} />
                    ))}
                  </div>
                  <div style={{ fontSize: 12, color: 'var(--fg-3)', textAlign: 'right', minWidth: 180 }}>
                    {finalized ? (
                      <>
                        Finalized {fmtRelOrTime(row.log.finalized_at)}
                        {staff && <div>by {staff.name}</div>}
                      </>
                    ) : (
                      <span style={{
                        display: 'inline-flex', alignItems: 'center',
                        padding: '2px 9px', borderRadius: 999,
                        background: '#DBEAFE', color: '#1E40AF',
                        fontSize: 10.5, fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase',
                      }}>In progress</span>
                    )}
                  </div>
                  <PIcon name={isOpen ? 'chevronD' : 'chevronR'} size={14} color="var(--fg-3)" />
                </>
              )}
            </div>

            {isOpen && (
              <LogReadingsEditor
                schedule={row.schedule}
                log={row.log}
                readings={row.rs}
                items={items}
                itemById={itemById}
                dateKey={dateKey}
              />
            )}
          </div>
        );
      })}
    </>
  );
};

// Sum readings into { total, byBand: { under_140, between_140_165, over_165 } }
const readingsSummary = (readings) => {
  const out = { total: readings.length, byBand: {} };
  readings.forEach(r => {
    out.byBand[r.band] = (out.byBand[r.band] || 0) + 1;
  });
  return out;
};

// Stacked metric tile: big count on top, small label below. Replaces the
// inline "1 <140" pill where the count + range read as a math expression
// rather than "1 reading in the <140 band". The two-line layout removes
// that ambiguity and gives the count its own visual weight.
const SummaryPill = ({ count, label, tone, bandColor, bandBg, bandBorder }) => {
  const isSolid = tone === 'solid';
  return (
    <div style={{
      display: 'inline-flex', flexDirection: 'column', alignItems: 'center',
      justifyContent: 'center',
      minWidth: 70, padding: '5px 12px 6px',
      borderRadius: 10,
      background: isSolid ? 'var(--fg-1)' : (bandBg || 'var(--bg-sunken)'),
      border: '1px solid ' + (isSolid ? 'var(--fg-1)' : (bandBorder || 'transparent')),
      lineHeight: 1,
    }}>
      <span style={{
        fontSize: 17, fontWeight: 700,
        fontFamily: 'var(--font-num)',
        color: isSolid ? '#fff' : (bandColor || 'var(--fg-2)'),
        letterSpacing: '-0.01em',
      }}>{count}</span>
      <span style={{
        marginTop: 3,
        fontSize: 9.5, fontWeight: 700,
        letterSpacing: '0.06em', textTransform: 'uppercase',
        color: isSolid ? 'rgba(255,255,255,0.78)' : (bandColor || 'var(--fg-2)'),
        opacity: isSolid ? 1 : 0.85,
      }}>{label}</span>
    </div>
  );
};

// Editable per-item readings for a single checkpoint. Shows ALL
// non-archived catalog items (so admin can add missing readings), plus
// any readings against archived/removed items so historical data isn't
// hidden. Each row exposes the three band buttons, a Type °F custom
// entry, and a Clear link.
//
// Behavior:
//   - If no log row exists yet, the first edit upserts a placeholder
//     log row (matches the iPad ensureLog pattern).
//   - Finalized logs are still editable from here — the user explicitly
//     wants always-on edit access from the portal. We don't unfinalize.
//   - Admin edits set `recorded_by_staff_id = null` (no PIN flow on the
//     portal). The historical viewer just shows the timestamp.
const LogReadingsEditor = ({ schedule, log, readings, items, itemById, dateKey }) => {
  const [localLogId, setLocalLogId] = useT(log?.id || null);
  // Keypad modal for the "Type °F" path: { itemId, value }
  const [keypad, setKeypad] = useT(null);
  // Optimistic overlay so taps render instantly without waiting on the
  // server roundtrip. Map: item_id → reading row (or null = cleared).
  // Reset to {} whenever the parent's readings prop refreshes — the
  // server has caught up, no need to keep the overlay.
  const [overrides, setOverrides] = useT({});
  useEffectT(() => { setOverrides({}); }, [readings]);

  const ensureLog = async () => {
    if (localLogId) return localLogId;
    if (log?.id) { setLocalLogId(log.id); return log.id; }
    if (!window.supa) return null;
    const newId = 'tl-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
    const { data, error } = await window.supa
      .from('temperature_logs')
      .upsert({
        id: newId,
        restaurant_id: window.RESTAURANT_ID,
        the_date: dateKey,
        schedule_id: schedule.id,
      }, { onConflict: 'restaurant_id,the_date,schedule_id' })
      .select('id')
      .single();
    if (error) { console.error('temperature_logs ensure failed', error); return null; }
    setLocalLogId(data.id);
    return data.id;
  };

  // Effective reading map = props.readings overlaid with optimistic
  // overrides. Used by all renderers below.
  const readingByItem = useMemoT(() => {
    const m = {};
    readings.forEach(r => { m[r.item_id] = r; });
    Object.entries(overrides).forEach(([itemId, r]) => {
      if (r === null) delete m[itemId];
      else m[itemId] = r;
    });
    return m;
  }, [readings, overrides]);

  const setReading = async (itemId, band, value) => {
    const lid = await ensureLog();
    if (!lid || !window.supa) return;
    const existing = readingByItem[itemId];
    const row = {
      id: existing?.id || ('tr-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6)),
      log_id: lid,
      item_id: itemId,
      band,
      value: value === null || value === undefined || value === '' ? null : Number(value),
      // Admin-edited from the portal. recorded_by_staff_id stays null
      // (no PIN flow on the portal side); the audit trail relies on
      // the row's existence + created_at timestamp.
      recorded_by_staff_id: null,
      // Stamp a client-side created_at so the optimistic row renders a
      // time immediately. Server will overwrite on insert; on update
      // we just pass it through (the row is keyed by id).
      created_at: existing?.created_at || new Date().toISOString(),
    };
    // Optimistic UI: render the new reading immediately.
    setOverrides(o => ({ ...o, [itemId]: row }));
    const { error } = await window.supa
      .from('temperature_readings')
      .upsert(row, { onConflict: 'log_id,item_id' });
    if (error) {
      console.error('temperature_readings upsert failed', error);
      // Roll back the optimistic update on failure.
      setOverrides(o => { const n = { ...o }; delete n[itemId]; return n; });
    }
  };

  const clearReading = async (itemId) => {
    const existing = readingByItem[itemId];
    if (!existing || !window.supa) return;
    // Optimistic clear.
    setOverrides(o => ({ ...o, [itemId]: null }));
    const { error } = await window.supa
      .from('temperature_readings').delete().eq('id', existing.id);
    if (error) {
      console.error('temperature_readings delete failed', error);
      // Roll back.
      setOverrides(o => ({ ...o, [itemId]: existing }));
    }
  };

  // Display rows: all non-archived catalog items (in catalog order), then
  // any readings against archived/removed items so we don't hide history.
  const activeItems = items.filter(i => !i.archived);
  const activeIds = new Set(activeItems.map(i => i.id));
  const orphanReadings = readings.filter(r => !activeIds.has(r.item_id));

  return (
    <div style={{ padding: '6px 0' }}>
      <table className="portal-table">
        <thead>
          <tr>
            <th>Item</th>
            <th style={{ width: 420 }}>Reading</th>
            <th style={{ width: 110 }}>Recorded</th>
          </tr>
        </thead>
        <tbody>
          {activeItems.map(item => (
            <EditableReadingRow
              key={item.id}
              item={item}
              reading={readingByItem[item.id]}
              onPickBand={(band) => setReading(item.id, band, null)}
              onOpenKeypad={() => setKeypad({ itemId: item.id, value: readingByItem[item.id]?.value ?? '' })}
              onClear={() => clearReading(item.id)}
            />
          ))}
          {orphanReadings.map(r => {
            const item = itemById[r.item_id] || { id: r.item_id, name: '(removed)', _orphan: true };
            return (
              <EditableReadingRow
                key={r.item_id}
                item={item}
                reading={r}
                isOrphan
                onPickBand={(band) => setReading(item.id, band, null)}
                onOpenKeypad={() => setKeypad({ itemId: item.id, value: r.value ?? '' })}
                onClear={() => clearReading(item.id)}
              />
            );
          })}
          {activeItems.length === 0 && orphanReadings.length === 0 && (
            <tr>
              <td colSpan={3} style={{ padding: 28, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
                No catalog items yet. Add some on the Catalog tab.
              </td>
            </tr>
          )}
        </tbody>
      </table>

      {keypad && (
        <CustomTempModal
          itemName={(itemById[keypad.itemId]?.name) || items.find(i => i.id === keypad.itemId)?.name || 'item'}
          initial={keypad.value}
          onCancel={() => setKeypad(null)}
          onCommit={(v) => {
            const band = window.deriveTemperatureBand(v);
            if (band) setReading(keypad.itemId, band, v);
            setKeypad(null);
          }}
        />
      )}
    </div>
  );
};

// One editable row in the readings table. The three band buttons act
// like the iPad's quick-picks. Type °F opens a numeric modal that
// derives the band from the value. Clear deletes the row.
const EditableReadingRow = ({ item, reading, isOrphan, onPickBand, onOpenKeypad, onClear }) => {
  const hasCustom = reading && reading.value !== null && reading.value !== undefined;
  const activeBand = reading ? reading.band : null;
  return (
    <tr style={{ background: reading ? '#FDFCFA' : 'transparent' }}>
      <td style={{ fontSize: 13, color: 'var(--fg-1)', fontWeight: 500 }}>
        {item.name}
        {isOrphan && (
          <span style={{
            marginLeft: 8, fontSize: 10, fontWeight: 700, letterSpacing: '0.06em',
            padding: '1px 6px', borderRadius: 999,
            background: 'var(--bg-sunken)', color: 'var(--fg-3)',
            textTransform: 'uppercase',
          }}>Removed</span>
        )}
      </td>
      <td>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
          {TEMP_BANDS.map(b => {
            const isActive = activeBand === b.id && !hasCustom;
            return (
              <button
                key={b.id}
                onClick={() => onPickBand(b.id)}
                style={{
                  height: 28, padding: '0 10px',
                  borderRadius: 7,
                  fontSize: 12, fontWeight: 600,
                  fontFamily: 'var(--font-num)',
                  background: isActive ? b.bg : '#FFFFFF',
                  color: isActive ? b.color : 'var(--fg-2)',
                  border: '1px solid ' + (isActive ? b.border : 'var(--border-2)'),
                  cursor: 'pointer',
                  whiteSpace: 'nowrap',
                }}
              >{b.shortLabel}</button>
            );
          })}
          <button
            onClick={onOpenKeypad}
            style={{
              height: 28, padding: '0 10px',
              borderRadius: 7,
              fontSize: 12, fontWeight: 600,
              background: hasCustom ? 'var(--fg-1)' : '#FFFFFF',
              color: hasCustom ? '#FFFFFF' : 'var(--fg-2)',
              border: '1px solid ' + (hasCustom ? 'var(--fg-1)' : 'var(--border-2)'),
              cursor: 'pointer',
              display: 'inline-flex', alignItems: 'center', gap: 5,
            }}
          >
            <PIcon name="edit" size={11} />
            {hasCustom ? `${Number(reading.value)}°F` : 'Type'}
          </button>
          {reading && (
            <button
              onClick={onClear}
              style={{
                fontSize: 11.5, color: 'var(--fg-3)',
                background: 'transparent', border: 'none',
                cursor: 'pointer', padding: '4px 8px',
                marginLeft: 'auto',
              }}
            >Clear</button>
          )}
        </div>
      </td>
      <td style={{ fontSize: 12, color: 'var(--fg-3)', fontFamily: 'var(--font-num)' }}>
        {reading?.created_at ? fmtRelOrTime(reading.created_at) : <span style={{ color: 'var(--border-1)' }}>—</span>}
      </td>
    </tr>
  );
};

// Small numeric-entry modal for custom temperature input from the
// portal. Browser <input type="number">; no NumPad (this isn't a
// touch device).
const CustomTempModal = ({ itemName, initial, onCancel, onCommit }) => {
  const [val, setVal] = useT(initial === null || initial === undefined || initial === '' ? '' : String(initial));
  const n = Number(val);
  const band = window.deriveTemperatureBand(val);
  const bandMeta = band ? BAND_BY_ID[band] : null;
  const canSave = Number.isFinite(n) && val !== '';
  return (
    <PModal open onClose={onCancel} width={360}
      title={'Temperature — ' + itemName}
      footer={
        <>
          <PBtn variant="ghost" onClick={onCancel}>Cancel</PBtn>
          <PBtn variant="primary" disabled={!canSave} onClick={() => onCommit(val)}>Save reading</PBtn>
        </>
      }
    >
      <div style={{ padding: 22, textAlign: 'center' }}>
        <input
          type="number"
          value={val}
          onChange={e => setVal(e.target.value)}
          autoFocus
          onKeyDown={e => { if (e.key === 'Enter' && canSave) onCommit(val); }}
          placeholder="e.g. 158"
          className="portal-input"
          style={{
            height: 60, width: 180,
            textAlign: 'center',
            fontSize: 28, fontFamily: 'var(--font-num)', fontWeight: 600,
            letterSpacing: '-0.02em',
          }}
        />
        <div style={{ marginTop: 8, fontSize: 12, color: 'var(--fg-3)' }}>°F</div>
        {bandMeta && (
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '4px 12px', borderRadius: 999,
            marginTop: 14,
            fontSize: 12, fontWeight: 600,
            background: bandMeta.bg, color: bandMeta.color,
            border: '1px solid ' + bandMeta.border,
          }}>
            Falls in {bandMeta.label}
          </div>
        )}
      </div>
    </PModal>
  );
};

const fmtRelOrTime = (iso) => {
  if (!iso) return '';
  const d = new Date(iso);
  if (isNaN(d.getTime())) return '';
  const now = new Date();
  const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
  if (sameDay) {
    return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
  }
  return d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
};

// ------------------------------------------------------------------
// Tab 2 — Catalog (items)
// ------------------------------------------------------------------
const CatalogTab = ({ items, setItems, setToast }) => {
  const [editing, setEditing] = useT(null); // item | 'new'
  const [showArchived, setShowArchived] = useT(false);
  // Order is owner-controlled via drag-and-drop. We just sort by
  // sort_order; basics and optionals interleave wherever the owner
  // dropped them. The iPad LogSheet honors the same order.
  const visible = items
    .filter(i => showArchived ? i.archived : !i.archived)
    .sort((a, b) => (a.sortOrder - b.sortOrder) || a.name.localeCompare(b.name));

  // ---- Drag-and-drop reordering ----
  // HTML5 native DnD. Desktop-only (the portal is admin-side) so we
  // don't need touch handlers. On drop we recompute sort_order in
  // 10-step increments for the entire visible list; useSupaList diffs
  // and only writes rows whose sortOrder actually changed.
  const [draggingId, setDraggingId] = useT(null);
  const [dragOverId, setDragOverId] = useT(null);

  const onDragStart = (id) => (e) => {
    setDraggingId(id);
    // Some browsers refuse to start a drag without dataTransfer payload.
    try { e.dataTransfer.setData('text/plain', id); } catch (_) {}
    e.dataTransfer.effectAllowed = 'move';
  };
  const onDragOver = (id) => (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    if (id !== dragOverId) setDragOverId(id);
  };
  const onDragLeave = () => setDragOverId(null);
  const onDragEnd = () => { setDraggingId(null); setDragOverId(null); };
  const onDrop = (targetId) => (e) => {
    e.preventDefault();
    const src = draggingId;
    setDraggingId(null); setDragOverId(null);
    if (!src || src === targetId) return;
    const list = visible.slice();
    const fromIdx = list.findIndex(it => it.id === src);
    const toIdx   = list.findIndex(it => it.id === targetId);
    if (fromIdx < 0 || toIdx < 0) return;
    const [moved] = list.splice(fromIdx, 1);
    list.splice(toIdx, 0, moved);
    // Re-stamp sortOrder in 10-step increments. We only touch the
    // currently-visible bucket (active OR archived, depending on the
    // toggle); the other bucket keeps its existing numbers.
    const renumbered = new Map(list.map((it, i) => [it.id, (i + 1) * 10]));
    setItems(cs => cs.map(c => {
      const so = renumbered.get(c.id);
      return so != null && so !== c.sortOrder ? { ...c, sortOrder: so } : c;
    }));
  };

  const save = (item) => {
    if (item.id && items.some(c => c.id === item.id)) {
      setItems(cs => cs.map(c => c.id === item.id ? item : c));
      setToast('Item updated');
    } else {
      const id = 'ti-' + Date.now().toString(36);
      const maxSort = items.reduce((m, c) => Math.max(m, c.sortOrder || 0), 0);
      setItems(cs => [...cs, { ...item, id, sortOrder: maxSort + 10, archived: false }]);
      setToast('Item added');
    }
    setEditing(null);
  };
  const archive = (id) => {
    setItems(cs => cs.map(c => c.id === id ? { ...c, archived: true } : c));
    setToast('Archived');
  };
  const restore = (id) => {
    setItems(cs => cs.map(c => c.id === id ? { ...c, archived: false } : c));
    setToast('Restored');
  };
  const remove = (id) => {
    if (!confirm('Delete this item? Past logs that reference it will show "removed" for the item name.')) return;
    setItems(cs => cs.filter(c => c.id !== id));
    setToast('Item deleted');
  };

  return (
    <>
      <div style={{ display: 'flex', alignItems: 'center', marginBottom: 14, gap: 10 }}>
        <div style={{ fontSize: 13, color: 'var(--fg-2)' }}>
          <strong>Basic</strong> items auto-show on every checkpoint. Optional items are added by crew on the iPad. Drag rows to reorder — same order shows on the iPad.
        </div>
        <div style={{ flex: 1 }} />
        <button onClick={() => setShowArchived(s => !s)} style={{
          fontSize: 12, color: showArchived ? 'var(--fg-1)' : 'var(--fg-3)',
          padding: '6px 10px', borderRadius: 8,
          background: showArchived ? 'var(--bg-sunken)' : 'transparent',
        }}>
          {showArchived
            ? `Showing archived (${items.filter(i => i.archived).length})`
            : `Show archived (${items.filter(i => i.archived).length})`}
        </button>
        <PBtn variant="primary" icon="plus" onClick={() => setEditing('new')}>Add item</PBtn>
      </div>

      <div className="portal-card" style={{ padding: 0 }}>
        <table className="portal-table">
          <thead>
            <tr>
              <th style={{ width: 36 }}></th>
              <th>Name</th>
              <th style={{ width: 110 }}>Type</th>
              <th style={{ width: 100 }}></th>
            </tr>
          </thead>
          <tbody>
            {visible.length === 0 && (
              <tr>
                <td colSpan={4} style={{ padding: 40, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
                  {showArchived ? 'No archived items.' : 'No items yet. Add your first item.'}
                </td>
              </tr>
            )}
            {visible.map(item => {
              const isDragging = draggingId === item.id;
              const isOver = dragOverId === item.id && draggingId && draggingId !== item.id;
              return (
              <tr
                key={item.id}
                draggable
                onDragStart={onDragStart(item.id)}
                onDragOver={onDragOver(item.id)}
                onDragLeave={onDragLeave}
                onDrop={onDrop(item.id)}
                onDragEnd={onDragEnd}
                onClick={() => setEditing(item)}
                style={{
                  cursor: 'pointer',
                  opacity: isDragging ? 0.4 : 1,
                  background: isOver ? '#EEF2FF' : undefined,
                  borderTop: isOver ? '2px solid #4F46E5' : undefined,
                  transition: 'background 80ms, border-top-color 80ms',
                }}
              >
                <td
                  onClick={e => e.stopPropagation()}
                  title="Drag to reorder"
                  style={{
                    cursor: 'grab', textAlign: 'center',
                    color: 'var(--fg-3)', userSelect: 'none',
                    fontSize: 16, lineHeight: 1,
                  }}
                >
                  ⋮⋮
                </td>
                <td style={{ fontWeight: 500 }}>{item.name}</td>
                <td>
                  {item.isBasic ? (
                    <span style={{
                      display: 'inline-flex', alignItems: 'center',
                      fontSize: 10.5, fontWeight: 700, letterSpacing: '0.06em',
                      padding: '2px 8px', borderRadius: 999,
                      background: '#E0F2FE', color: '#075985',
                      textTransform: 'uppercase',
                    }}>Basic</span>
                  ) : (
                    <span style={{ fontSize: 11.5, color: 'var(--fg-3)' }}>Optional</span>
                  )}
                </td>
                <td onClick={e => e.stopPropagation()}>
                  <div className="row-actions">
                    <button onClick={() => setEditing(item)} title="Edit"><PIcon name="edit" size={13} /></button>
                    {item.archived ? (
                      <>
                        <button onClick={() => restore(item.id)} title="Restore"><PIcon name="check" size={13} /></button>
                        <button onClick={() => remove(item.id)} title="Delete forever"><PIcon name="trash" size={13} /></button>
                      </>
                    ) : (
                      <button onClick={() => archive(item.id)} title="Archive"><PIcon name="archive" size={13} /></button>
                    )}
                  </div>
                </td>
              </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      {editing && (
        <TempItemEditor
          initial={editing === 'new' ? null : editing}
          onClose={() => setEditing(null)}
          onSave={save}
        />
      )}
    </>
  );
};

const TempItemEditor = ({ initial, onClose, onSave }) => {
  const [name, setName] = useT(initial?.name || '');
  // Default new items to non-basic — most catalog entries are seasonal /
  // off-menu specials. Basic is a deliberate "always present" call.
  const [isBasic, setIsBasic] = useT(!!initial?.isBasic);
  const canSave = name.trim();
  return (
    <PModal open onClose={onClose} width={460}
      title={initial ? `Edit — ${initial.name}` : 'New temperature item'}
      footer={
        <>
          <PBtn variant="ghost" onClick={onClose}>Cancel</PBtn>
          <PBtn variant="primary" disabled={!canSave} onClick={() => onSave({
            ...(initial || {}),
            id: initial?.id,
            name: name.trim(),
            isBasic,
          })}>{initial ? 'Save changes' : 'Add to catalog'}</PBtn>
        </>
      }
    >
      <div style={{ padding: 18 }}>
        <div style={{ fontSize: 11.5, fontWeight: 500, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 6 }}>Name</div>
        <input
          className="portal-input"
          placeholder="e.g. Hot rice"
          value={name}
          onChange={e => setName(e.target.value)}
          autoFocus
          style={{ height: 36 }}
        />

        <label style={{
          display: 'flex', alignItems: 'flex-start', gap: 12,
          marginTop: 18, padding: '12px 14px',
          borderRadius: 10,
          background: 'var(--bg-sunken)',
          cursor: 'pointer',
        }}>
          <input
            type="checkbox"
            checked={isBasic}
            onChange={e => setIsBasic(e.target.checked)}
            style={{ width: 18, height: 18, marginTop: 2, accentColor: 'var(--fg-1)' }}
          />
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-1)' }}>
              Basic item — show on every checkpoint by default
            </div>
            <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 3, lineHeight: 1.45 }}>
              Always-on staples (hot rice, soups). Non-basic items only show on the iPad after crew adds them — useful for off-menu specials that aren't on the line every day.
            </div>
          </div>
        </label>

        <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 12, lineHeight: 1.45 }}>
          Crew picks &lt;140°F · 140–165°F · &gt;165°F per item at every scheduled checkpoint, or types an exact temperature on the keypad.
        </div>
      </div>
    </PModal>
  );
};

// ------------------------------------------------------------------
// Tab 3 — Schedule (checkpoint times)
// ------------------------------------------------------------------
const ScheduleTab = ({ schedule, setSchedule, setToast }) => {
  const [editing, setEditing] = useT(null);   // entry | 'new'
  const [showArchived, setShowArchived] = useT(false);
  const visible = schedule
    .filter(s => showArchived ? s.archived : !s.archived)
    .sort((a, b) => (a.timeOfDay || '').localeCompare(b.timeOfDay || ''));

  const save = (entry) => {
    if (entry.id && schedule.some(s => s.id === entry.id)) {
      setSchedule(ss => ss.map(s => s.id === entry.id ? entry : s));
      setToast('Checkpoint updated');
    } else {
      const id = 'ts-' + Date.now().toString(36);
      setSchedule(ss => [...ss, { ...entry, id, archived: false }]);
      setToast('Checkpoint added');
    }
    setEditing(null);
  };
  const archive = (id) => {
    setSchedule(ss => ss.map(s => s.id === id ? { ...s, archived: true } : s));
    setToast('Archived — iPad will hide this slot starting today');
  };
  const restore = (id) => {
    setSchedule(ss => ss.map(s => s.id === id ? { ...s, archived: false } : s));
    setToast('Restored');
  };
  const remove = (id) => {
    if (!confirm('Delete this checkpoint? Past logs that used it stay intact.')) return;
    setSchedule(ss => ss.filter(s => s.id !== id));
    setToast('Checkpoint deleted');
  };

  return (
    <>
      <div style={{ display: 'flex', alignItems: 'center', marginBottom: 14, gap: 10 }}>
        <div style={{ fontSize: 13, color: 'var(--fg-2)' }}>
          Daily times the iPad surfaces as checkpoint cards. Order is by time, not entry order.
        </div>
        <div style={{ flex: 1 }} />
        <button onClick={() => setShowArchived(s => !s)} style={{
          fontSize: 12, color: showArchived ? 'var(--fg-1)' : 'var(--fg-3)',
          padding: '6px 10px', borderRadius: 8,
          background: showArchived ? 'var(--bg-sunken)' : 'transparent',
        }}>
          {showArchived
            ? `Showing archived (${schedule.filter(s => s.archived).length})`
            : `Show archived (${schedule.filter(s => s.archived).length})`}
        </button>
        <PBtn variant="primary" icon="plus" onClick={() => setEditing('new')}>Add checkpoint</PBtn>
      </div>

      <div className="portal-card" style={{ padding: 0 }}>
        <table className="portal-table">
          <thead>
            <tr>
              <th style={{ width: 160 }}>Time</th>
              <th>Label</th>
              <th style={{ width: 100 }}></th>
            </tr>
          </thead>
          <tbody>
            {visible.length === 0 && (
              <tr>
                <td colSpan={3} style={{ padding: 40, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
                  {showArchived ? 'No archived checkpoints.' : 'No checkpoints yet. Add one to start logging on the iPad.'}
                </td>
              </tr>
            )}
            {visible.map(s => (
              <tr key={s.id} onClick={() => setEditing(s)} style={{ cursor: 'pointer' }}>
                <td style={{ fontWeight: 600, fontFamily: 'var(--font-num)', letterSpacing: '-0.005em' }}>
                  {fmtTime12(s.timeOfDay)}
                </td>
                <td style={{ fontSize: 13, color: s.label ? 'var(--fg-1)' : 'var(--fg-3)' }}>
                  {s.label || <em>—</em>}
                </td>
                <td onClick={e => e.stopPropagation()}>
                  <div className="row-actions">
                    <button onClick={() => setEditing(s)} title="Edit"><PIcon name="edit" size={13} /></button>
                    {s.archived ? (
                      <>
                        <button onClick={() => restore(s.id)} title="Restore"><PIcon name="check" size={13} /></button>
                        <button onClick={() => remove(s.id)} title="Delete forever"><PIcon name="trash" size={13} /></button>
                      </>
                    ) : (
                      <button onClick={() => archive(s.id)} title="Archive"><PIcon name="archive" size={13} /></button>
                    )}
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {editing && (
        <ScheduleEditor
          initial={editing === 'new' ? null : editing}
          onClose={() => setEditing(null)}
          onSave={save}
        />
      )}
    </>
  );
};

const ScheduleEditor = ({ initial, onClose, onSave }) => {
  const [timeOfDay, setTimeOfDay] = useT(initial?.timeOfDay || '11:00');
  const [label, setLabel] = useT(initial?.label || '');
  const canSave = /^\d{2}:\d{2}$/.test(timeOfDay);
  return (
    <PModal open onClose={onClose} width={460}
      title={initial ? `Edit checkpoint — ${fmtTime12(initial.timeOfDay)}` : 'New checkpoint'}
      footer={
        <>
          <PBtn variant="ghost" onClick={onClose}>Cancel</PBtn>
          <PBtn variant="primary" disabled={!canSave} onClick={() => onSave({
            ...(initial || {}),
            id: initial?.id,
            timeOfDay,
            label: label.trim(),
          })}>{initial ? 'Save changes' : 'Add checkpoint'}</PBtn>
        </>
      }
    >
      <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 14 }}>
        <div>
          <div style={{ fontSize: 11.5, fontWeight: 500, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 6 }}>Time</div>
          <input
            type="time"
            className="portal-input"
            value={timeOfDay}
            onChange={e => setTimeOfDay(e.target.value)}
            style={{ height: 36, fontFamily: 'var(--font-num)', fontWeight: 600, fontSize: 15 }}
          />
          <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 4 }}>
            Restaurant-local time. Displayed in 12-hour format ({fmtTime12(timeOfDay)}).
          </div>
        </div>
        <div>
          <div style={{ fontSize: 11.5, fontWeight: 500, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 6 }}>Label (optional)</div>
          <input
            className="portal-input"
            value={label}
            onChange={e => setLabel(e.target.value)}
            placeholder="e.g. Lunch, Mid-shift, Dinner"
            style={{ height: 36 }}
          />
        </div>
      </div>
    </PModal>
  );
};

window.TemperatureLog = TemperatureLog;
