// me-app.jsx — annotator self-view: My Dashboard + Shifts & Payments.
// Pulls getAnnotatorData + getPaymentInfo. Lighter UI than the team-lead app.

const { useState: meUseState, useEffect: meUseEffect, Fragment: MeF } = React;

// localStorage caching wrapper for annotator-side callables. With 500
// annotators each opening MeApp 5+ times a day, repeat fetches dominate
// the read budget on this surface. Cached responses skip the network
// entirely — paired with stale-while-revalidate semantics on the
// History tab (where past weeks never change), it knocks ~80% off the
// annotator-side reads.
function meCachedCall(key, ttlMs, fn) {
  try {
    const raw = window.localStorage.getItem(key);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && parsed.fetchedAt && (Date.now() - parsed.fetchedAt) < ttlMs) {
        // Resolve from cache. Still kick off a background refresh so the
        // next render sees fresh data without the user waiting.
        Promise.resolve().then(fn).then((fresh) => {
          try {
            window.localStorage.setItem(key, JSON.stringify({ fetchedAt: Date.now(), data: fresh }));
          } catch {}
        }).catch(() => {});
        return Promise.resolve(parsed.data);
      }
    }
  } catch {}
  return fn().then((data) => {
    try {
      window.localStorage.setItem(key, JSON.stringify({ fetchedAt: Date.now(), data }));
    } catch {}
    return data;
  });
}

// Hard cache (no background refresh). Used for past-week history that
// is by definition immutable — once a week closes, nothing changes.
function meHardCachedCall(key, ttlMs, fn) {
  try {
    const raw = window.localStorage.getItem(key);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && parsed.fetchedAt && (Date.now() - parsed.fetchedAt) < ttlMs) {
        return Promise.resolve(parsed.data);
      }
    }
  } catch {}
  return fn().then((data) => {
    try {
      window.localStorage.setItem(key, JSON.stringify({ fetchedAt: Date.now(), data }));
    } catch {}
    return data;
  });
}

// Bonus tier constants — kept in sync with functions/src/lib/bench.ts:16-22.
// Computed client-side so the tracker reacts instantly when getAnnotatorData
// refreshes; the authoritative weekly bonus still comes from getPaymentInfo
// on the Shifts & Payments tab.
const ME_BONUS_FLOOR = 850;
const ME_BONUS_TIERS = [
  { min: 851,  max: 1000,     rate: 0.08 },
  { min: 1001, max: 1200,     rate: 0.15 },
  { min: 1201, max: Infinity, rate: 0.20 },
];

function meCalcBonus(totalResources) {
  if (totalResources <= ME_BONUS_FLOOR) {
    const toFloor = Math.max(0, ME_BONUS_FLOOR - totalResources + 1);
    return { bonusAmount: 0, currentTier: 0, aboveFloor: 0, nextTier: ME_BONUS_TIERS[0], toNext: toFloor };
  }
  let bonusAmount = 0;
  let currentTier = 0;
  for (let i = 0; i < ME_BONUS_TIERS.length; i++) {
    const t = ME_BONUS_TIERS[i];
    const resInTier = Math.max(0, Math.min(totalResources, t.max) - t.min + 1);
    if (resInTier > 0) {
      bonusAmount += resInTier * t.rate;
      currentTier = i + 1;
    }
  }
  const next = ME_BONUS_TIERS[currentTier]; // 0-indexed array, currentTier is 1-indexed
  const toNext = next ? Math.max(0, next.min - totalResources) : 0;
  return {
    bonusAmount: Math.round(bonusAmount * 100) / 100,
    currentTier,
    aboveFloor: totalResources - ME_BONUS_FLOOR,
    nextTier: next || null,
    toNext,
  };
}

function MeFmtK(n) {
  if (n == null || isNaN(n)) return '—';
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(2).replace(/\.?0+$/, '') + 'M';
  if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'k';
  return String(Math.round(n));
}

function MeKpi({ label, value, sub, color }) {
  return (
    <div className="kpi-tile">
      <div className="lbl">{label}</div>
      <div className="val" style={color ? { color } : undefined}>{value}</div>
      {sub && <div className="sub">{sub}</div>}
    </div>
  );
}

// Top-of-dashboard banner shown only when the annotator owes make-up
// from yesterday's incomplete shift. Goes amber while still owed, green
// once today's totals cover the combined target. The math is in
// computeMakeUp (components.jsx) so the same logic runs on the lead side.
function MakeUpBanner({ makeUp }) {
  const { yesterday, today, madeUp } = makeUp;
  const tone = madeUp
    ? { bg: '#f0fdf4', border: '#bbf7d0', text: '#15803d', icon: '✓' }
    : { bg: '#fffbeb', border: '#fde68a', text: '#b45309', icon: '⚠' };
  const parts = [];
  if (yesterday.resDeficit > 0) parts.push(`${MeFmtK(yesterday.resDeficit)} resources`);
  if (yesterday.hrDeficit > 0) parts.push(`${yesterday.hrDeficit.toFixed(1)}h`);
  const stillParts = [];
  if (today.resStillNeeded > 0) stillParts.push(`${MeFmtK(today.resStillNeeded)} resources`);
  if (today.hrStillNeeded > 0) stillParts.push(`${today.hrStillNeeded.toFixed(1)}h`);

  return (
    <div style={{
      background: tone.bg,
      border: `1px solid ${tone.border}`,
      borderRadius: 10,
      padding: '12px 16px',
      marginBottom: 14,
      color: tone.text,
    }}>
      <div style={{ fontSize: 14, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ fontSize: 16 }}>{tone.icon}</span>
        {madeUp
          ? `Yesterday's shift made up — nice work.`
          : `Make-up day · clear ${parts.join(' + ')} from ${yesterday.day} by end of today.`}
      </div>
      {!madeUp && stillParts.length > 0 && (
        <div style={{ fontSize: 12, marginTop: 6, color: tone.text, opacity: 0.85 }}>
          Still need <b>{stillParts.join(' + ')}</b> on top of your normal day to clear it.
        </div>
      )}
      {madeUp && (
        <div style={{ fontSize: 12, marginTop: 6, color: tone.text, opacity: 0.85 }}>
          You logged {MeFmtK(today.actualRes)} resources today (target was {MeFmtK(today.effectiveTargetRes)}).
        </div>
      )}
    </div>
  );
}

// Live bonus tracker. Three rows:
//   1. Today — big number, progress bar to next tier, $ earned so far,
//      "N more for Tier X" hint.
//   2. Yesterday — closed result so the annotator sees outcome, not promise.
//   3. Week mini-grid — per-day $ earned plus today's running total.
// Past days are read off kpi.dailyBreakdown; future days render dimmed.
function MeBonusTracker({ data }) {
  const dailyBreakdown = (data.kpi && data.kpi.dailyBreakdown) || [];
  const weeklyDays = (data.weekly && data.weekly.days) || [];

  // Bonus is only earned on bonus-eligible resources. PCA work is excluded
  // server-side via the per-project benchmark flag; UGC work the PCA team
  // does still counts. Falls back to total resources when the field is
  // missing (older payloads / no benchmarks loaded).
  const resByDate = {};
  dailyBreakdown.forEach((d) => {
    const eligible = d.eligibleResources != null ? d.eligibleResources : d.resources;
    resByDate[d.date] = Math.round(eligible || 0);
  });

  const todayStr = new Date().toISOString().slice(0, 10);
  const todayIdx = weeklyDays.findIndex((d) => d.date === todayStr);
  const yesterdayIdx = todayIdx > 0 ? todayIdx - 1 : -1;

  const todayRes = todayIdx >= 0 ? (resByDate[weeklyDays[todayIdx].date] || 0) : 0;
  const yesterdayRes = yesterdayIdx >= 0 ? (resByDate[weeklyDays[yesterdayIdx].date] || 0) : null;

  const today = meCalcBonus(todayRes);
  const yesterday = yesterdayRes != null ? meCalcBonus(yesterdayRes) : null;

  // Visualize progress up to 1200 (top of Tier 2) so Tier 3 still has runway.
  const SHOW_MAX = 1200;
  const progressPct = Math.min(100, (todayRes / SHOW_MAX) * 100);
  const floorPct = (ME_BONUS_FLOOR / SHOW_MAX) * 100;

  // Closed-day earnings so far this week (excludes today since it's still
  // running). Matches what the lead's Shifts & Payments tab will eventually
  // settle on.
  let closedEarned = 0;
  weeklyDays.forEach((d, i) => {
    if (todayIdx >= 0 && i < todayIdx) {
      closedEarned += meCalcBonus(resByDate[d.date] || 0).bonusAmount;
    }
  });

  // Bar colour: green if past floor, amber if making progress, grey if zero.
  const barColor = today.bonusAmount > 0 ? '#15803d' : todayRes > 0 ? '#b45309' : '#d1d5db';

  return (
    <div className="card">
      <div className="card-hd card-hd-bottomline">
        <h2>Bonus tracker</h2>
        <span className="meta">Bonus kicks in at {ME_BONUS_FLOOR} resources/day</span>
      </div>
      <div className="card-bd">
        {/* Today */}
        <div className="meta">Today</div>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginTop: 2 }}>
          <div style={{ fontFamily: 'Inconsolata, monospace', fontSize: 32, fontWeight: 700, lineHeight: 1, letterSpacing: '-0.02em' }}>
            {MeFmtK(todayRes)}
          </div>
          <div style={{ fontSize: 13, color: '#6b7280' }}>resources</div>
          <div style={{ marginLeft: 'auto', fontFamily: 'Inconsolata, monospace', fontWeight: 700, fontSize: 22, color: today.bonusAmount > 0 ? '#15803d' : '#9ca3af' }}>
            ZMW {today.bonusAmount.toFixed(2)}
          </div>
        </div>
        <div style={{ fontSize: 12, color: today.bonusAmount > 0 ? '#15803d' : '#6b7280', marginTop: 6 }}>
          {today.bonusAmount > 0 ? (
            <>
              ✓ {today.aboveFloor} above floor
              {today.nextTier && ` · ${today.toNext} more for Tier ${today.currentTier + 1} (ZMW ${(today.nextTier.rate).toFixed(2)}/res)`}
              {!today.nextTier && ' · top tier'}
            </>
          ) : todayRes > 0 ? (
            `${ME_BONUS_FLOOR - todayRes + 1} more to start earning bonus`
          ) : (
            'No resources logged yet today'
          )}
        </div>

        {/* Progress bar with floor + Tier 2 markers */}
        <div style={{ position: 'relative', marginTop: 12 }}>
          <div style={{ height: 10, background: '#f3f4f6', borderRadius: 5, overflow: 'hidden' }}>
            <div style={{
              height: '100%',
              width: progressPct + '%',
              background: barColor,
              transition: 'width 0.3s ease',
            }}/>
          </div>
          {/* Floor marker at 850 */}
          <div title="Bonus floor" style={{
            position: 'absolute', top: -2, left: floorPct + '%',
            width: 2, height: 14, background: '#111',
          }}/>
          {/* Tier 2 marker at 1001 */}
          <div title="Tier 2" style={{
            position: 'absolute', top: -2, left: ((1001 / SHOW_MAX) * 100) + '%',
            width: 2, height: 14, background: '#111', opacity: 0.4,
          }}/>
          <div className="flex jb" style={{ fontSize: 10, color: '#6b7280', marginTop: 4 }}>
            <span>0</span>
            <span>floor {ME_BONUS_FLOOR}</span>
            <span>tier 2</span>
            <span>{SHOW_MAX}+</span>
          </div>
        </div>

        {/* Yesterday + week-to-date earnings */}
        <div className="row g16" style={{ marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--line-2)' }}>
          <div className="f1">
            <div className="meta">Yesterday</div>
            <div style={{ fontSize: 14, marginTop: 2 }}>
              {yesterdayRes == null ? (
                <span className="meta">—</span>
              ) : (
                <>
                  <b>{MeFmtK(yesterdayRes)}</b>
                  <span className="meta"> resources · </span>
                  {yesterday && yesterday.bonusAmount > 0 ? (
                    <span style={{ color: '#15803d', fontWeight: 700 }}>ZMW {yesterday.bonusAmount.toFixed(2)} earned</span>
                  ) : (
                    <span className="meta">below floor</span>
                  )}
                </>
              )}
            </div>
          </div>
          <div className="f1">
            <div className="meta">Earned this week (closed days)</div>
            <div style={{ fontSize: 14, marginTop: 2 }}>
              <b style={{ color: closedEarned > 0 ? '#15803d' : '#111' }}>ZMW {closedEarned.toFixed(2)}</b>
              <span className="meta"> · today still running</span>
            </div>
          </div>
        </div>

        {/* Day-by-day mini grid */}
        {weeklyDays.length > 0 && (
          <div style={{ marginTop: 14 }}>
            <div className="meta" style={{ marginBottom: 6 }}>Day by day</div>
            <table className="me-week" style={{ width: '100%' }}>
              <thead><tr>{weeklyDays.map((d) => <th key={d.date}>{d.day}</th>)}</tr></thead>
              <tbody><tr>
                {weeklyDays.map((d, i) => {
                  const isToday = i === todayIdx;
                  const isFuture = todayIdx >= 0 && i > todayIdx;
                  const r = resByDate[d.date] || 0;
                  const b = meCalcBonus(r);
                  const bg = isFuture ? '#fafafa'
                    : b.bonusAmount > 0 ? '#f0fdf4'
                    : isToday ? '#eff6ff'
                    : r === 0 ? '#fafafa'
                    : '#fffbeb';
                  return (
                    <td key={d.date} style={{
                      background: bg,
                      padding: '8px 4px',
                      textAlign: 'center',
                      borderTop: isToday ? '2px solid #1d4ed8' : undefined,
                    }}>
                      <div className="mono" style={{
                        fontWeight: 700, fontSize: 12,
                        color: isFuture ? '#9ca3af' : b.bonusAmount > 0 ? '#15803d' : r === 0 ? '#9ca3af' : '#b45309',
                      }}>
                        {isFuture ? '—' : (r === 0 ? '—' : MeFmtK(r))}
                      </div>
                      <div className="meta" style={{ marginTop: 2, fontSize: 10 }}>
                        {isFuture ? '' : (b.bonusAmount > 0 ? 'ZMW ' + b.bonusAmount.toFixed(2) : '—')}
                      </div>
                    </td>
                  );
                })}
              </tr></tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  );
}

// Gamification card on My Dashboard. Three pieces:
//   - Streak: consecutive committed days they've hit their daily target
//     (computed server-side over last 21 days; included in getAnnotatorData)
//   - Team rank: where they sit on the leaderboard right now
//   - Top 5: avatars + this-week resources for the top of the team
// Hidden when there's no team or the leaderboard fetch failed.
function WhereYouStand({ data, board, session }) {
  if (!board || !board.leaderboard || board.leaderboard.length === 0) {
    return null;
  }
  const me = board.me;
  const top5 = board.leaderboard.slice(0, 5);
  const streak = (data && data.streak) || 0;
  const myEmail = session.email;

  // Streak tier styling — simple bands so the badge feels distinct as
  // people climb. 0 = grey, 1-2 = blue, 3-6 = amber, 7+ = green/fire.
  const streakTone = streak >= 7
    ? { color: '#b45309', bg: '#fffbeb', icon: '🔥', label: streak === 1 ? 'day' : 'days' }
    : streak >= 3
      ? { color: '#15803d', bg: '#f0fdf4', icon: '✓', label: streak === 1 ? 'day' : 'days' }
      : streak >= 1
        ? { color: '#1d4ed8', bg: '#eff6ff', icon: '•', label: streak === 1 ? 'day' : 'days' }
        : { color: '#9ca3af', bg: '#f9fafb', icon: '–', label: 'days' };

  return (
    <div className="card">
      <div className="card-hd card-hd-bottomline">
        <h2>Where you stand</h2>
        <span className="meta">{board.weekLabel} · {board.team}</span>
      </div>
      <div className="card-bd">
        {/* Top row: rank + streak. Two stat tiles, big numbers. */}
        <div className="row g16" style={{ marginBottom: 16 }}>
          <div className="kpi-tile" style={{ flex: 1 }}>
            <div className="lbl">Your rank</div>
            <div className="val">
              {me ? '#' + me.rank : '—'}
              <span style={{ fontSize: 13, color: '#6b7280', marginLeft: 6 }}>
                of {board.totalAnnotators}
              </span>
            </div>
            <div className="sub">
              {me
                ? `${MeFmtK(me.resources)} resources this week`
                : 'Not ranked yet'}
            </div>
          </div>
          <div className="kpi-tile" style={{ flex: 1 }}>
            <div className="lbl">Daily-target streak</div>
            <div className="val" style={{ color: streakTone.color }}>
              {streakTone.icon} {streak}
              <span style={{ fontSize: 13, color: '#6b7280', marginLeft: 6, fontWeight: 400 }}>
                {streakTone.label}
              </span>
            </div>
            <div className="sub">
              {streak === 0
                ? 'Hit today\'s target to start a new streak'
                : streak >= 7
                  ? 'On fire — keep it going'
                  : 'Consecutive committed days hit'}
            </div>
          </div>
        </div>

        {/* Top 5 leaderboard */}
        <div className="meta" style={{ marginBottom: 8 }}>Top 5 this week</div>
        <div className="col g6">
          {top5.map((row) => {
            const isMe = row.email === myEmail;
            return (
              <div key={row.email} className="lbx-row" style={{
                background: isMe ? '#eff6ff' : 'transparent',
                border: isMe ? '1px solid #bfdbfe' : '1px solid transparent',
                borderRadius: 6,
                padding: '6px 8px',
              }}>
                <div className="lbx-rank" style={{ fontWeight: 700 }}>#{row.rank}</div>
                <Avatar name={row.name} size={26}/>
                <div className="lbx-name" style={{ fontWeight: isMe ? 700 : 500 }}>
                  {row.name}{isMe && <span className="meta" style={{ marginLeft: 6 }}>· you</span>}
                </div>
                <div className="lbx-pace mono" style={{ color: '#6b7280' }}>
                  {row.pace || 0}<span style={{ fontSize: 10 }}>/hr</span>
                </div>
                <div className="lbx-res mono" style={{ fontWeight: 700 }}>
                  {MeFmtK(row.resources)}
                </div>
              </div>
            );
          })}
        </div>

        {/* If they're not in the top 5, show their row at the bottom for
            context — same shape so it lines up visually. */}
        {me && me.rank > 5 && (
          <>
            <div className="meta" style={{ margin: '10px 0 6px', textAlign: 'center' }}>
              ⋯ {me.rank - 5 - 1 > 0 ? `${me.rank - 5 - 1} between you and #5` : ''}
            </div>
            <div className="lbx-row" style={{
              background: '#eff6ff',
              border: '1px solid #bfdbfe',
              borderRadius: 6,
              padding: '6px 8px',
            }}>
              <div className="lbx-rank" style={{ fontWeight: 700 }}>#{me.rank}</div>
              <Avatar name={me.name} size={26}/>
              <div className="lbx-name" style={{ fontWeight: 700 }}>
                {me.name}<span className="meta" style={{ marginLeft: 6 }}>· you</span>
              </div>
              <div className="lbx-pace mono" style={{ color: '#6b7280' }}>
                {me.pace || 0}<span style={{ fontSize: 10 }}>/hr</span>
              </div>
              <div className="lbx-res mono" style={{ fontWeight: 700 }}>
                {MeFmtK(me.resources)}
              </div>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// Make-up shift card. Shows any open shortfall the annotator owes.
//
// The system rule is the source of misunderstandings: "make up" means
// SURPLUS work beyond your normal day's commitment, not just hitting
// your normal target. Hitting Saturday's normal 5h shift does NOT clear
// 1.9h owed from Friday — you have to clock 1.9h *on top of* the 5h.
// Similarly for resources: only output above today's expected target
// counts toward yesterday's deficit.
//
// To kill the confusion this card now leads with the rule explainer,
// shows progress as a bar with a clear "X.Xh left to clear" label,
// renders the day-of-week + date together, and gives a concrete
// deadline timestamp instead of "Nh left".
function MeMakeupShifts({ items }) {
  if (!items || items.length === 0) return null;

  const formatDeadline = (m) => {
    if (!m.owedSinceIso) return null;
    const owedSinceMs = new Date(m.owedSinceIso).getTime();
    if (!Number.isFinite(owedSinceMs)) return null;
    const deadlineMs = owedSinceMs + 48 * 3600 * 1000;
    const d = new Date(deadlineMs);
    return d.toLocaleString(undefined, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
  };

  const formatDay = (iso) => {
    const d = new Date(iso + 'T00:00:00');
    if (isNaN(d.getTime())) return iso;
    return d.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'short' });
  };

  return (
    <div className="card">
      <div className="card-hd card-hd-bottomline">
        <h2>Make-up shifts owed</h2>
      </div>
      <div className="card-bd">
        <div style={{
          background: '#fffbeb',
          border: '1px solid #fde68a',
          borderRadius: 8,
          padding: '10px 12px',
          marginBottom: 14,
          fontSize: 12,
          color: '#92400e',
        }}>
          <b>How make-up works:</b> you have 48 hours to clear a missed shift by
          working <b>extra</b> hours / producing <b>extra</b> resources <b>beyond</b>{' '}
          your normal day's target. Just hitting today's regular target does
          <b> not</b> count — only surplus does. After 48h, a Performance Notice
          is opened automatically.
        </div>

        <div className="col g10">
          {items.map((m) => {
            const hoursOwed = Number(m.hoursOwed || 0);
            const hoursMade = Number(m.hoursMadeUp || 0);
            const hoursLeft = Math.max(0, hoursOwed - hoursMade);
            const hoursPct = hoursOwed > 0 ? Math.min(100, (hoursMade / hoursOwed) * 100) : 100;

            const resOwed = Number(m.resourcesOwed || 0);
            const resMade = Number(m.resourcesMadeUp || 0);
            const resLeft = Math.max(0, resOwed - resMade);
            const resPct = resOwed > 0 ? Math.min(100, (resMade / resOwed) * 100) : 100;

            const elapsed = Number(m.elapsedHours || 0);
            const remainingHrs = Math.max(0, 48 - elapsed);
            const cleared = hoursLeft <= 0 && resLeft <= 0;
            const overdue = remainingHrs <= 0 && !cleared;
            const atRisk = elapsed >= 24 && !cleared;
            const deadline = formatDeadline(m);

            const stripe = cleared ? '#15803d' : overdue ? '#b91c1c' : atRisk ? '#b45309' : '#3730a3';

            return (
              <div key={m.id} style={{
                border: '1px solid var(--line)',
                borderLeft: `4px solid ${stripe}`,
                borderRadius: 8,
                padding: '10px 14px',
              }}>
                <div className="flex jb ac" style={{ marginBottom: 8 }}>
                  <div>
                    <div style={{ fontWeight: 700, fontSize: 14 }}>{formatDay(m.date)}</div>
                    <div className="meta" style={{ fontSize: 11, marginTop: 2 }}>
                      Missed shift
                    </div>
                  </div>
                  <div style={{ textAlign: 'right' }}>
                    <div style={{
                      fontWeight: 700,
                      fontSize: 12,
                      color: cleared ? '#15803d' : overdue ? '#b91c1c' : atRisk ? '#b45309' : '#3730a3',
                    }}>
                      {cleared
                        ? '✓ Cleared'
                        : overdue
                          ? 'Overdue · Performance Notice opened'
                          : `${remainingHrs.toFixed(0)}h left to clear`}
                    </div>
                    {deadline && !cleared && (
                      <div className="meta" style={{ fontSize: 11, marginTop: 2 }}>
                        Deadline: {deadline}
                      </div>
                    )}
                  </div>
                </div>

                {hoursOwed > 0 && (
                  <div style={{ marginBottom: resOwed > 0 ? 8 : 0 }}>
                    <div className="flex jb" style={{ fontSize: 11, color: '#374151', marginBottom: 4 }}>
                      <span>
                        Extra hours needed: <b>{hoursLeft.toFixed(1)}h</b> more
                      </span>
                      <span className="meta">
                        {hoursMade.toFixed(1)}h made up of {hoursOwed.toFixed(1)}h owed
                      </span>
                    </div>
                    <div style={{ height: 6, background: '#f3f4f6', borderRadius: 3, overflow: 'hidden' }}>
                      <div style={{
                        height: '100%',
                        width: hoursPct + '%',
                        background: cleared ? '#15803d' : '#3730a3',
                        transition: 'width 0.2s ease',
                      }}/>
                    </div>
                  </div>
                )}

                {resOwed > 0 && (
                  <div>
                    <div className="flex jb" style={{ fontSize: 11, color: '#374151', marginBottom: 4 }}>
                      <span>
                        Extra resources needed: <b>{resLeft}</b> more
                      </span>
                      <span className="meta">
                        {resMade} made up of {resOwed} owed
                      </span>
                    </div>
                    <div style={{ height: 6, background: '#f3f4f6', borderRadius: 3, overflow: 'hidden' }}>
                      <div style={{
                        height: '100%',
                        width: resPct + '%',
                        background: cleared ? '#15803d' : '#3730a3',
                        transition: 'width 0.2s ease',
                      }}/>
                    </div>
                  </div>
                )}

                {atRisk && !overdue && !cleared && (
                  <div style={{ marginTop: 8, fontSize: 11, color: '#b45309' }}>
                    ⚠ Past 24h — this becomes a Performance Notice if not cleared by the deadline.
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function MyDashboard({ session }) {
  const [data, setData] = meUseState(null);
  const [err, setErr] = meUseState(null);
  const [board, setBoard] = meUseState(null);

  meUseEffect(() => {
    // 5-min cache. Annotator's own KPI / shifts / pace — KPI ingest is
    // every 60 min so 5-min staleness is invisible in practice. Background
    // revalidation keeps the next view fresh.
    meCachedCall(
      `yd-annotator:${session.email}`,
      5 * 60 * 1000,
      () => window.YDApp.call('getAnnotatorData', { email: session.email }),
    )
      .then(setData)
      .catch(e => setErr(e.message || String(e)));

    // Leaderboard cached 5 min as well — same freshness reasoning.
    if (session.team) {
      meCachedCall(
        `yd-leaderboard:${session.team}`,
        5 * 60 * 1000,
        () => window.YDApp.call('getTeamLeaderboard', { team: session.team }),
      )
        .then(setBoard)
        .catch(() => { /* leaderboard is best-effort */ });
    }
  }, [session.email, session.team]);

  if (err) return <div className="card"><div className="card-bd" style={{ color: '#b91c1c' }}>{err}</div></div>;
  if (!data) return <div className="meta">Loading…</div>;

  const kpi = data.kpi || {};
  const weekly = data.weekly || { days: [] };
  const expected = data.expectedPace || data.benchmarkPace || 0;
  const dailyTarget = data.dailyTarget || null;
  const makeUp = window.computeMakeUp({
    days: weekly.days,
    kpi: kpi.dailyBreakdown,
    expectedPace: data.expectedPace || 0,
  });

  return (
    <MeF>
      {makeUp && <MakeUpBanner makeUp={makeUp}/>}

      <div className="kpi-row">
        <MeKpi label="Week resources" value={MeFmtK(kpi.totalResources || 0)} sub="WTD"/>
        <MeKpi label="Pace /hr" value={kpi.avgPace || 0}
          sub={expected > 0 ? `vs ${expected} expected` : ''}
          color={expected > 0 ? (kpi.avgPace >= expected ? '#15803d' : '#b45309') : undefined}/>
        <MeKpi label="Hours" value={(kpi.totalHours || 0).toFixed(1) + 'h'}
          sub={`vs ${weekly.totalCommitted || 0}h committed`}/>
        <MeKpi
          label={makeUp ? "Today's target (+ make-up)" : "Today's target"}
          value={makeUp ? MeFmtK(makeUp.today.effectiveTargetRes) : (dailyTarget ? MeFmtK(dailyTarget.todayTarget) : '—')}
          sub={
            makeUp
              ? `${MeFmtK(makeUp.today.actualRes)} done · base ${MeFmtK(makeUp.today.baseTargetRes)} + ${MeFmtK(makeUp.yesterday.resDeficit)} carry`
              : (dailyTarget ? `${MeFmtK(dailyTarget.todayActual)} done · ${dailyTarget.cumDeficit > 0 ? MeFmtK(dailyTarget.cumDeficit) + ' cum deficit' : 'on pace'}` : '')
          }
          color={makeUp ? (makeUp.madeUp ? '#15803d' : '#b45309') : undefined}/>
      </div>

      <MeBonusTracker data={data}/>

      <MeMakeupShifts items={data.makeupShifts || []}/>

      <WhereYouStand data={data} board={board} session={session}/>

      <div className="card">
        <div className="card-hd card-hd-bottomline"><h2>This week</h2></div>
        <div className="card-bd" style={{ padding: 0 }}>
          <table className="me-week">
            <thead>
              <tr>
                {weekly.days.map(d => <th key={d.date}>{d.day}</th>)}
              </tr>
            </thead>
            <tbody>
              <tr>
                {weekly.days.map(d => (
                  <td key={d.date} style={{
                    background: d.isLeave ? '#eff6ff' : d.isMissed ? '#fef2f2' : d.isIncomplete ? '#fffbeb' : d.quidlo > 0 ? '#f0fdf4' : '#fafafa',
                    textAlign: 'center', padding: '10px 6px',
                  }}>
                    <div className="mono" style={{ fontWeight: 700, fontSize: 14 }}>
                      {d.isLeave ? 'LV' : d.committed === 0 ? '—' : d.quidlo.toFixed(1) + 'h'}
                    </div>
                    <div className="meta" style={{ marginTop: 4 }}>
                      {d.committed > 0 ? `/ ${d.committed}h` : 'off'}
                    </div>
                  </td>
                ))}
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      {kpi.entries && kpi.entries.length > 0 && (
        <div className="card">
          <div className="card-hd card-hd-bottomline"><h2>By project</h2></div>
          <div className="card-bd" style={{ padding: 0 }}>
            <table className="pl-table">
              <thead>
                <tr>
                  <th>Project</th>
                  <th className="num">Resources</th>
                  <th className="num">Hours</th>
                  <th className="num">Pace</th>
                  <th className="num">vs Target</th>
                </tr>
              </thead>
              <tbody>
                {kpi.entries.map(e => (
                  <tr key={e.project}>
                    <td>{e.project}</td>
                    <td className="num mono">{MeFmtK(e.resources)}</td>
                    <td className="num mono">{e.hours}h</td>
                    <td className="num mono" style={{ color: e.paceStatus === 'critical' ? '#b91c1c' : e.paceStatus === 'on_track' ? '#15803d' : '' }}>
                      {e.pace}{e.paceTarget > 0 ? ` / ${e.paceTarget}` : ''}
                    </td>
                    <td className="num mono">{e.resPct != null ? e.resPct + '%' : '—'}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}
    </MeF>
  );
}

function ShiftsAndPayments({ session }) {
  const [data, setData] = meUseState(null);
  const [err, setErr] = meUseState(null);
  const [showChange, setShowChange] = meUseState(false);
  const [showLeave, setShowLeave] = meUseState(false);
  const [toast, setToast] = meUseState(null);

  const reload = (force) => {
    // 5-min cache; force=true bypasses it. Forced after a shift-change
    // submit so the new "Pending" row shows up without waiting on TTL.
    if (force) {
      try { window.localStorage.removeItem(`yd-payment:${session.email}`); } catch {}
    }
    meCachedCall(
      `yd-payment:${session.email}`,
      5 * 60 * 1000,
      () => window.YDApp.call('getPaymentInfo', { email: session.email }),
    )
      .then(setData)
      .catch(e => setErr(e.message || String(e)));
  };

  meUseEffect(() => {
    reload();
  }, [session.email]);

  if (err) return <div className="card"><div className="card-bd" style={{ color: '#b91c1c' }}>{err}</div></div>;
  if (!data) return <div className="meta">Loading…</div>;

  const sched = data.currentSchedule;
  const bonus = data.bonus;
  const reqs = data.shiftRequests || [];
  const DAY_KEYS = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
  const DAY_LABELS = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];

  return (
    <MeF>
      <div className="card">
        <div className="card-hd card-hd-bottomline"><h2>Payment details</h2></div>
        <div className="card-bd">
          <div className="kpi-row" style={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
            <MeKpi label="Phone (payments)" value={data.phonePay || '—'}/>
            <MeKpi label="Provider" value={data.provider || '—'}/>
            <MeKpi label="Confirmed" value={data.confirmed ? '✓' : 'No'}
              sub={data.confirmed?.confirmedAt || (data.confirmed ? 'Confirmed' : 'Pending review')}/>
          </div>
        </div>
      </div>

      {bonus && (
        <div className="card">
          <div className="card-hd card-hd-bottomline"><h2>This week's bonus (estimate)</h2></div>
          <div className="card-bd">
            <div className="kpi-row" style={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
              <MeKpi label="Total resources" value={MeFmtK(bonus.totalResources || 0)}/>
              <MeKpi label="Days worked" value={bonus.daysWorked || 0}/>
              <MeKpi label="Bonus amount" value={bonus.bonusAmount != null ? 'ZMW ' + bonus.bonusAmount.toFixed(2) : '—'}
                sub={bonus.aboveFloor ? `+${bonus.aboveFloor} above floor` : 'Below floor'}
                color={bonus.bonusAmount > 0 ? '#15803d' : undefined}/>
            </div>
            {bonus.tiers && bonus.tiers.length > 0 && (
              <table className="pl-table" style={{ marginTop: 14 }}>
                <thead><tr><th>Tier</th><th className="num">Resources</th><th className="num">Rate</th><th className="num">Amount</th></tr></thead>
                <tbody>
                  {bonus.tiers.map((t, i) => (
                    <tr key={i}>
                      <td>{t.label}</td>
                      <td className="num mono">{t.resources}</td>
                      <td className="num mono">ZMW {t.rate.toFixed(2)}</td>
                      <td className="num mono" style={{ fontWeight: 700 }}>ZMW {t.amount.toFixed(2)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
          </div>
        </div>
      )}

      {sched && (
        <div className="card">
          <div className="card-hd card-hd-bottomline">
            <h2>Current shift schedule</h2>
            <div className="flex g6">
              <button className="btn btn-sm" onClick={() => setShowLeave(true)}>Request leave</button>
              <button className="btn btn-sm" onClick={() => setShowChange(true)}>Request shift change</button>
            </div>
          </div>
          <div className="card-bd" style={{ padding: 0 }}>
            <table className="me-week">
              <thead><tr>{DAY_LABELS.map(d => <th key={d}>{d}</th>)}</tr></thead>
              <tbody>
                <tr>
                  {DAY_KEYS.map((k, i) => {
                    const v = sched.weekSchedule?.[k] || '';
                    return (
                      <td key={k} style={{ padding: '10px 6px', textAlign: 'center', fontSize: 11 }}>
                        {v ? v : <span className="meta">off</span>}
                      </td>
                    );
                  })}
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      )}

      <div className="card">
        <div className="card-hd card-hd-bottomline"><h2>Pending shift change requests</h2></div>
        <div className="card-bd">
          {reqs.length === 0 ? (
            <div className="meta">No pending requests.</div>
          ) : (
            <table className="pl-table">
              <thead><tr><th>Submitted</th><th>Reason</th><th>Status</th></tr></thead>
              <tbody>
                {reqs.map((r, i) => (
                  <tr key={i}>
                    <td className="mono" style={{ fontSize: 11 }}>{r.createdAt?.toString?.() || '—'}</td>
                    <td>{r.reason || '—'}</td>
                    <td>
                      <span className="chip chip-mini">{r.status}</span>
                      {r.status === 'approved-deferred' && r.effectiveFrom ? (
                        <div className="meta" style={{ marginTop: 4, fontSize: 11 }}>
                          Takes effect {r.effectiveFrom} — applying now would breach the 2-day-off policy.
                        </div>
                      ) : null}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {showChange && (
        <ShiftChangeModal
          schedule={sched ? sched.weekSchedule : null}
          onClose={() => setShowChange(false)}
          onSubmitted={() => {
            setShowChange(false);
            setToast('Request submitted — your lead will review it.');
            setTimeout(() => setToast(null), 2800);
            reload(true); // bypass cache so the new pending row appears
          }}/>
      )}
      {showLeave && (
        <RequestLeaveModal
          onClose={() => setShowLeave(false)}
          onSubmitted={() => {
            setShowLeave(false);
            setToast('Leave request submitted — your lead will review it.');
            setTimeout(() => setToast(null), 2800);
          }}/>
      )}
      {toast && <div className="toast">{toast}</div>}
    </MeF>
  );
}

// Annotator-side form for proposing shift changes. One row per day shows
// current vs new; only days the annotator actually edits get sent. The
// backend (submitBulkShiftChangeRequest) accepts whatever subset of days
// is provided plus an optional reason.
function ShiftChangeModal({ schedule, onClose, onSubmitted }) {
  const DAY_KEYS = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
  const DAY_LABELS = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
  const current = schedule || {};
  const [edits, setEdits] = meUseState({}); // day -> new value
  const [reason, setReason] = meUseState('');
  const [busy, setBusy] = meUseState(false);
  const [err, setErr] = meUseState(null);

  const setDay = (k, v) => setEdits((prev) => Object.assign({}, prev, { [k]: v }));
  const clearDay = (k) => setEdits((prev) => {
    const next = Object.assign({}, prev); delete next[k]; return next;
  });

  // Only days where the new value differs from current (and isn't blank-to-blank).
  const dayChanges = {};
  for (const k of DAY_KEYS) {
    if (!(k in edits)) continue;
    const from = (current[k] || '').trim();
    const to = (edits[k] || '').trim();
    if (from === to) continue;
    dayChanges[k] = { from, to };
  }
  const hasChanges = Object.keys(dayChanges).length > 0;

  const submit = async () => {
    if (!hasChanges) { setErr('Edit at least one day before submitting.'); return; }
    setBusy(true); setErr(null);
    try {
      await window.YDApp.call('submitBulkShiftChangeRequest', { dayChanges, reason });
      onSubmitted();
    } catch (e) {
      setErr(e.message || String(e)); setBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div
        className="modal-card modal-card-wide"
        onClick={(e) => e.stopPropagation()}
        style={{ maxHeight: 'calc(100vh - 40px)', display: 'flex', flexDirection: 'column' }}>
        <div className="modal-hd" style={{ flex: '0 0 auto' }}>
          <h3>Request shift change</h3>
          <button className="modal-x" onClick={onClose} aria-label="Close">×</button>
        </div>
        <div className="modal-bd" style={{ overflowY: 'auto', flex: '1 1 auto', minHeight: 0 }}>
          <div className="meta" style={{ marginBottom: 10 }}>
            Edit any day's hours below — leave the others alone. Use formats your team already uses (e.g. <code>09:00-17:00</code>) or blank for a day off. Your lead has to approve before changes take effect.
          </div>
          {DAY_KEYS.map((k, i) => {
            const cur = current[k] || '';
            const next = k in edits ? edits[k] : cur;
            const dirty = (k in edits) && (edits[k] || '').trim() !== cur.trim();
            return (
              <div key={k} className="flex ac g8" style={{ marginBottom: 6 }}>
                <div style={{ width: 96, fontSize: 12, fontWeight: 600 }}>{DAY_LABELS[i]}</div>
                <div style={{ width: 90, fontSize: 12, color: '#6b7280', fontFamily: 'Inconsolata, monospace' }}>
                  {cur || '— off —'}
                </div>
                <span className="meta">→</span>
                <input
                  className="modal-input"
                  style={{ flex: 1, margin: 0, borderColor: dirty ? '#3730a3' : undefined }}
                  value={next}
                  onChange={(e) => setDay(k, e.target.value)}
                  placeholder="e.g. 09:00-17:00 or leave blank"
                  disabled={busy}/>
                {dirty && (
                  <button className="btn btn-sm" onClick={() => clearDay(k)} disabled={busy}>Reset</button>
                )}
              </div>
            );
          })}
          <label className="modal-lbl" style={{ marginTop: 12 }}>Reason (optional)</label>
          <textarea
            className="modal-input"
            value={reason}
            onChange={(e) => setReason(e.target.value)}
            disabled={busy}
            rows={3}
            placeholder="Why you're asking (helps your lead decide)"/>
          {err && <div className="modal-err">{err}</div>}
        </div>
        <div className="modal-actions" style={{ flex: '0 0 auto' }}>
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="btn btn-dark" onClick={submit} disabled={busy || !hasChanges}>
            {busy ? 'Submitting…' : `Submit${hasChanges ? ` (${Object.keys(dayChanges).length} day${Object.keys(dayChanges).length === 1 ? '' : 's'})` : ''}`}
          </button>
        </div>
      </div>
    </div>
  );
}

// Past 8 weeks of self-view. One getWeekHistory call per week, fired in
// parallel — the SDK enforces region pinning so they all hit the same
// endpoint. Each row: weekLabel, total resources (links processed), hours,
// pace and the bonus that resources level would earn at our tier rates.
// Selecting a row drops the per-project breakdown for that week below.
const HISTORY_WEEKS = 8;

// Annotator-side time-off request form. Submits to submitTimeOffRequest
// which writes a `pending` doc to timeOffRequests/{id}; team leads see
// it in the Requests tab and approve/deny. Most fields are optional —
// only date range and reason are required.
function RequestLeaveModal({ onClose, onSubmitted }) {
  const today = new Date().toISOString().slice(0, 10);
  const [startDate, setStartDate] = meUseState(today);
  const [endDate, setEndDate] = meUseState(today);
  const [reason, setReason] = meUseState('');
  const [detail, setDetail] = meUseState('');
  const [capacity, setCapacity] = meUseState('full');
  const [reducedHours, setReducedHours] = meUseState('');
  const [busy, setBusy] = meUseState(false);
  const [err, setErr] = meUseState(null);

  const validRange = startDate && endDate && startDate <= endDate;
  const numDays = validRange
    ? Math.round((new Date(endDate) - new Date(startDate)) / 86400000) + 1
    : 0;

  const submit = async () => {
    if (!validRange) { setErr('End date must be on or after start date.'); return; }
    if (!reason.trim()) { setErr('Pick a reason.'); return; }
    if (capacity === 'reduced' && (!reducedHours || Number(reducedHours) <= 0)) {
      setErr('Enter the reduced hours per day for partial leave.');
      return;
    }
    setBusy(true); setErr(null);
    try {
      const payload = {
        startDate,
        endDate,
        reason: reason.trim(),
        detail: detail.trim() || undefined,
        capacity,
      };
      if (capacity === 'reduced') payload.reducedHours = Number(reducedHours);
      await window.YDApp.call('submitTimeOffRequest', payload);
      onSubmitted();
    } catch (e) {
      setErr(e.message || String(e));
      setBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={busy ? undefined : onClose}>
      <div
        className="modal-card modal-card-wide"
        onClick={(e) => e.stopPropagation()}
        style={{ maxHeight: 'calc(100vh - 40px)', display: 'flex', flexDirection: 'column' }}>
        <div className="modal-hd" style={{ flex: '0 0 auto' }}>
          <h3>Request leave</h3>
          <button className="modal-x" onClick={busy ? undefined : onClose} aria-label="Close">×</button>
        </div>
        <div className="modal-bd" style={{ overflowY: 'auto', flex: '1 1 auto', minHeight: 0 }}>
          <div className="meta" style={{ marginBottom: 12 }}>
            Submit dates you'll be off. Your lead has to approve before the calendar reflects it.
          </div>

          <div className="row g16">
            <div style={{ flex: 1 }}>
              <label className="modal-lbl">Start date</label>
              <input
                type="date"
                className="modal-input"
                value={startDate}
                min={today}
                onChange={(e) => setStartDate(e.target.value)}
                disabled={busy}/>
            </div>
            <div style={{ flex: 1 }}>
              <label className="modal-lbl">End date</label>
              <input
                type="date"
                className="modal-input"
                value={endDate}
                min={startDate || today}
                onChange={(e) => setEndDate(e.target.value)}
                disabled={busy}/>
            </div>
          </div>
          {validRange && (
            <div className="meta" style={{ marginTop: 4 }}>
              {numDays} day{numDays === 1 ? '' : 's'} requested
            </div>
          )}

          <label className="modal-lbl" style={{ marginTop: 12 }}>Reason</label>
          <select className="modal-input" value={reason} onChange={(e) => setReason(e.target.value)} disabled={busy}>
            <option value="">— pick a reason —</option>
            <option value="Sick">Sick</option>
            <option value="Family">Family</option>
            <option value="Personal">Personal</option>
            <option value="Bereavement">Bereavement</option>
            <option value="Vacation">Vacation</option>
            <option value="Other">Other</option>
          </select>

          <label className="modal-lbl">Capacity during leave</label>
          <select className="modal-input" value={capacity} onChange={(e) => setCapacity(e.target.value)} disabled={busy}>
            <option value="full">Full (no work at all)</option>
            <option value="reduced">Reduced hours (partial work)</option>
          </select>
          {capacity === 'reduced' && (
            <>
              <label className="modal-lbl">Hours per day during leave</label>
              <input
                type="number"
                className="modal-input"
                min="0.5"
                step="0.5"
                value={reducedHours}
                onChange={(e) => setReducedHours(e.target.value)}
                disabled={busy}
                placeholder="e.g. 2"
                style={{ width: 140 }}/>
            </>
          )}

          <label className="modal-lbl" style={{ marginTop: 12 }}>Detail (optional)</label>
          <textarea
            className="modal-input"
            value={detail}
            onChange={(e) => setDetail(e.target.value)}
            disabled={busy}
            rows={3}
            placeholder="Any context that helps your lead decide"/>

          {err && <div className="modal-err">{err}</div>}
        </div>
        <div className="modal-actions" style={{ flex: '0 0 auto' }}>
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="btn btn-dark" onClick={submit} disabled={busy || !validRange || !reason}>
            {busy ? 'Submitting…' : 'Submit request'}
          </button>
        </div>
      </div>
    </div>
  );
}

function History({ session }) {
  const [weeks, setWeeks] = meUseState([]); // [{ offset, kpi, weekly, weekLabel, ... }]
  const [loading, setLoading] = meUseState(true);
  const [err, setErr] = meUseState(null);
  const [openOffset, setOpenOffset] = meUseState(null);

  meUseEffect(() => {
    setLoading(true); setErr(null);
    const offsets = [];
    for (let o = -1; o >= -HISTORY_WEEKS; o--) offsets.push(o);
    // 24h hard cache per (email, offset). Past weeks are closed once
    // they roll over — the data is immutable. Annotators reopening
    // History repeatedly during the day pay only one round trip.
    Promise.all(offsets.map((o) =>
      meHardCachedCall(
        `yd-history:${session.email}:${o}`,
        24 * 60 * 60 * 1000,
        () => window.YDApp.call('getWeekHistory', { email: session.email, offset: o }),
      )
        .then((d) => Object.assign({ offset: o }, d))
        .catch(() => ({ offset: o, error: true }))
    ))
      .then((rows) => { setWeeks(rows); setLoading(false); })
      .catch((e) => { setErr(e.message || String(e)); setLoading(false); });
  }, [session.email]);

  if (loading) return <div className="meta">Loading history…</div>;
  if (err) return <div className="card"><div className="card-bd" style={{ color: '#b91c1c' }}>{err}</div></div>;

  return (
    <MeF>
      <div className="card">
        <div className="card-hd card-hd-bottomline">
          <h2>Past {HISTORY_WEEKS} weeks</h2>
          <span className="meta">Click a week to see per-project breakdown</span>
        </div>
        <div className="card-bd" style={{ padding: 0 }}>
          <table className="pl-table">
            <thead>
              <tr>
                <th>Week</th>
                <th className="num">Resources</th>
                <th className="num">Hours</th>
                <th className="num">Pace</th>
                <th className="num">Bonus</th>
                <th/>
              </tr>
            </thead>
            <tbody>
              {weeks.map((w) => {
                if (w.error) return (
                  <tr key={w.offset}>
                    <td>{`${w.offset}w`}</td>
                    <td colSpan={5} className="meta">Failed to load</td>
                  </tr>
                );
                const total = Math.round((w.kpi && w.kpi.totalResources) || 0);
                const hours = (w.kpi && w.kpi.totalHours) || 0;
                const pace  = (w.kpi && w.kpi.avgPace) || 0;
                // Bonus = sum of per-day bonuses computed only on bonus-eligible
                // resources (PCA work excluded server-side via the per-project
                // benchmark flag). The 850 floor applies daily.
                const days = (w.kpi && w.kpi.dailyBreakdown) || [];
                const bonus = days.reduce((s, d) => {
                  const r = Math.round(
                    (d.eligibleResources != null ? d.eligibleResources : d.resources) || 0,
                  );
                  return s + meCalcBonus(r).bonusAmount;
                }, 0);
                const expanded = openOffset === w.offset;
                return (
                  <MeF key={w.offset}>
                    <tr style={{ cursor: 'pointer' }} onClick={() => setOpenOffset(expanded ? null : w.offset)}>
                      <td style={{ fontWeight: 700 }}>
                        <span style={{ display: 'inline-block', width: 14 }}>{expanded ? '▾' : '▸'}</span>
                        {w.weekLabel}
                      </td>
                      <td className="num mono" style={{ fontWeight: 700 }}>{MeFmtK(total)}</td>
                      <td className="num mono">{hours.toFixed(1)}h</td>
                      <td className="num mono">{pace}</td>
                      <td className="num mono" style={{ color: bonus > 0 ? '#15803d' : '#9ca3af', fontWeight: 700 }}>
                        {bonus > 0 ? 'ZMW ' + bonus.toFixed(2) : '—'}
                      </td>
                      <td className="meta" style={{ fontSize: 11 }}>{w.weekStart} – {w.weekEnd}</td>
                    </tr>
                    {expanded && w.kpi && w.kpi.entries && w.kpi.entries.length > 0 && (
                      <tr>
                        <td colSpan={6} style={{ background: '#fafafa', padding: '12px 16px' }}>
                          <table className="pl-table" style={{ background: '#fff' }}>
                            <thead>
                              <tr>
                                <th>Project</th>
                                <th className="num">Resources (links)</th>
                                <th className="num">Hours</th>
                                <th className="num">Pace</th>
                                <th className="num">vs Target</th>
                              </tr>
                            </thead>
                            <tbody>
                              {w.kpi.entries.map((e) => (
                                <tr key={e.project}>
                                  <td>{e.project}</td>
                                  <td className="num mono" style={{ fontWeight: 700 }}>{MeFmtK(e.resources)}</td>
                                  <td className="num mono">{e.hours}h</td>
                                  <td className="num mono">{e.pace}{e.paceTarget > 0 ? ` / ${e.paceTarget}` : ''}</td>
                                  <td className="num mono">{e.resPct != null ? e.resPct + '%' : '—'}</td>
                                </tr>
                              ))}
                            </tbody>
                          </table>
                        </td>
                      </tr>
                    )}
                    {expanded && (!w.kpi || !w.kpi.entries || w.kpi.entries.length === 0) && (
                      <tr>
                        <td colSpan={6} style={{ background: '#fafafa', padding: 16, color: '#9ca3af', fontSize: 12 }}>
                          No project breakdown available for this week.
                        </td>
                      </tr>
                    )}
                  </MeF>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
    </MeF>
  );
}

// Quality tone matches the lead-side bands so the same number reads the
// same colour on both surfaces.
function meQualityTone(q) {
  if (q == null) return { color: '#6b7280', bg: '#f3f4f6' };
  if (q >= 95)  return { color: '#15803d', bg: '#f0fdf4' };
  if (q >= 90)  return { color: '#1d4ed8', bg: '#eff6ff' };
  if (q >= 80)  return { color: '#b45309', bg: '#fffbeb' };
  return { color: '#b91c1c', bg: '#fef2f2' };
}

// Inline SVG sparkline of the 8-week quality trend. Empty weeks render
// at 100% so the gap before the annotator started showing on QC sheets
// doesn't drag the line down — the dot marker only renders when the
// week actually had data.
function MeQualitySparkline({ trend }) {
  const points = trend || [];
  if (points.length === 0) return null;
  const W = 220, H = 46, P = 4;
  const ys = points.map((p) => p.checked > 0 ? p.quality : null);
  const realYs = ys.filter((y) => y != null);
  const minQ = realYs.length ? Math.max(0, Math.min(...realYs) - 5) : 80;
  const maxQ = 100;
  const range = Math.max(1, maxQ - minQ);
  const xStep = (W - 2 * P) / Math.max(1, points.length - 1);
  const yFor = (q) => H - P - ((q - minQ) / range) * (H - 2 * P);
  const segments = [];
  let path = '';
  for (let i = 0; i < points.length; i++) {
    const x = P + i * xStep;
    const q = ys[i] != null ? ys[i] : 100;
    const y = yFor(q);
    path += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
    if (ys[i] != null) {
      segments.push(<circle key={i} cx={x.toFixed(1)} cy={y.toFixed(1)} r="2.5" fill={meQualityTone(ys[i]).color}/>);
    }
  }
  return (
    <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ display: 'block' }}>
      <path d={path} stroke="#1d4ed8" fill="none" strokeWidth="1.5"/>
      {segments}
    </svg>
  );
}

function MeMistakeRow({ m }) {
  const [open, setOpen] = meUseState(false);
  const attrCount = (m.attributes || []).length;
  return (
    <div style={{ borderTop: '1px solid var(--line-2)', padding: '10px 14px' }}>
      <div className="flex jb ac" style={{ cursor: 'pointer' }} onClick={() => setOpen(!open)}>
        <div className="flex g8 ac" style={{ minWidth: 0, flex: 1 }}>
          <span style={{ display: 'inline-block', width: 12, color: '#9ca3af' }}>{open ? '▾' : '▸'}</span>
          <span className="mono" style={{ fontSize: 11, color: '#6b7280' }}>{m.day || '—'}</span>
          <span style={{ fontSize: 13, fontWeight: 600 }}>{m.project || '(no project)'}</span>
          <span className="meta" style={{ fontSize: 11 }}>{attrCount} {attrCount === 1 ? 'change' : 'changes'}</span>
        </div>
        <div className="flex g6">
          {m.qcLink && (
            <a className="btn btn-sm" href={m.qcLink} target="_blank" rel="noopener" onClick={(e) => e.stopPropagation()}>
              View
            </a>
          )}
        </div>
      </div>
      {open && (
        <div style={{ marginTop: 10, marginLeft: 24, paddingLeft: 12, borderLeft: '2px solid #fecaca' }}>
          {(m.attributes || []).map((a, i) => (
            <div key={i} style={{ marginBottom: 8, fontSize: 13 }}>
              <div style={{ fontWeight: 700, color: '#374151' }}>{a.task || '(unknown task)'}</div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 4 }}>
                <div style={{ background: '#fef2f2', padding: '6px 10px', borderRadius: 6, color: '#7f1d1d' }}>
                  <div className="meta" style={{ fontSize: 10, marginBottom: 2 }}>You entered</div>
                  <div className="mono" style={{ fontSize: 12, wordBreak: 'break-word' }}>{a.previous}</div>
                </div>
                <div style={{ background: '#f0fdf4', padding: '6px 10px', borderRadius: 6, color: '#14532d' }}>
                  <div className="meta" style={{ fontSize: 10, marginBottom: 2 }}>Reviewer changed to</div>
                  <div className="mono" style={{ fontSize: 12, wordBreak: 'break-word' }}>{a.current}</div>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function MeQuality({ session }) {
  const [data, setData] = meUseState(null);
  const [err, setErr] = meUseState(null);

  meUseEffect(() => {
    // 15-min cache — QC ingest runs hourly so the annotator's quality
    // updates infrequently. Same stale-while-revalidate pattern as the
    // dashboard call.
    meCachedCall(
      `yd-quality:${session.email}`,
      15 * 60 * 1000,
      () => window.YDApp.call('getQcDataForAnnotator', { email: session.email }),
    )
      .then(setData)
      .catch((e) => setErr(e.message || String(e)));
  }, [session.email]);

  if (err) return <div className="card"><div className="card-bd" style={{ color: '#b91c1c' }}>{err}</div></div>;
  if (!data) return <div className="meta">Loading…</div>;

  const wk = data.thisWeek || { totalChecked: 0, totalMistakes: 0, quality: 100, mistakes: [], byTask: [] };
  const all = data.allTime || { totalChecked: 0, totalMistakes: 0, quality: 100, byTask: [], recentMistakes: [] };
  const trend = data.weeklyTrend || [];
  const wkTone = meQualityTone(wk.totalChecked > 0 ? wk.quality : null);
  const allTone = meQualityTone(all.totalChecked > 0 ? all.quality : null);

  return (
    <MeF>
      {/* Headline this-week + sparkline */}
      <div className="card">
        <div className="card-hd card-hd-bottomline">
          <h2>Quality</h2>
          <span className="meta">How often the reviewer changed your work</span>
        </div>
        <div className="card-bd">
          <div className="row g16">
            <div className="f1">
              <div className="meta">This week</div>
              <div style={{
                fontFamily: 'Inconsolata, monospace', fontSize: 32, fontWeight: 700,
                color: wkTone.color, lineHeight: 1.1,
              }}>
                {wk.totalChecked > 0 ? wk.quality.toFixed(2) + '%' : '—'}
              </div>
              <div className="meta" style={{ fontSize: 12 }}>
                {wk.totalChecked > 0
                  ? `${wk.totalChecked} checked · ${wk.totalMistakes} with changes`
                  : 'No QC data yet this week'}
              </div>
            </div>
            <div className="f1">
              <div className="meta">All-time (last 365 days)</div>
              <div style={{
                fontFamily: 'Inconsolata, monospace', fontSize: 22, fontWeight: 700,
                color: allTone.color, lineHeight: 1.1,
              }}>
                {all.totalChecked > 0 ? all.quality.toFixed(2) + '%' : '—'}
              </div>
              <div className="meta" style={{ fontSize: 12 }}>
                {all.totalChecked > 0
                  ? `${all.totalChecked} checked · ${all.totalMistakes} with changes`
                  : ''}
              </div>
            </div>
            <div style={{ minWidth: 240 }}>
              <div className="meta">Last 8 weeks</div>
              <MeQualitySparkline trend={trend}/>
              <div className="flex jb" style={{ fontSize: 10, color: '#6b7280', marginTop: 2 }}>
                <span>{trend[0]?.weekKey || ''}</span>
                <span>{trend[trend.length - 1]?.weekKey || ''}</span>
              </div>
            </div>
          </div>
        </div>
      </div>

      {/* Worst tasks */}
      {all.byTask && all.byTask.length > 0 && (
        <div className="card">
          <div className="card-hd card-hd-bottomline">
            <h2>Where mistakes happen most</h2>
            <span className="meta">Tasks ranked by reviewer-change rate</span>
          </div>
          <div className="card-bd">
            <div className="flex g6" style={{ flexWrap: 'wrap' }}>
              {all.byTask.slice(0, 8).map((t) => {
                const tone = t.errorRate >= 20 ? { color: '#b91c1c', bg: '#fef2f2' }
                           : t.errorRate >= 5  ? { color: '#b45309', bg: '#fffbeb' }
                                                : { color: '#15803d', bg: '#f0fdf4' };
                return (
                  <span key={t.taskName} className="chip chip-mini" style={{
                    background: tone.bg, color: tone.color, borderColor: 'transparent',
                  }}>
                    <b>{t.taskName}</b> · {t.errorRate}% ({t.mistakes}/{t.checked})
                  </span>
                );
              })}
            </div>
          </div>
        </div>
      )}

      {/* Recent mistakes drill-down */}
      <div className="card">
        <div className="card-hd card-hd-bottomline">
          <h2>Recent changes</h2>
          <span className="meta">Tap a row to see what was changed</span>
        </div>
        <div className="card-bd" style={{ padding: 0 }}>
          {(all.recentMistakes || []).length === 0 ? (
            <div className="meta" style={{ padding: 24, textAlign: 'center' }}>
              {all.totalChecked === 0 ? 'No QC reviews on record yet.' : 'No reviewer changes — nice work.'}
            </div>
          ) : (
            (all.recentMistakes || []).map((m) => <MeMistakeRow key={m.resourceId} m={m}/>)
          )}
        </div>
      </div>
    </MeF>
  );
}

function MeApp({ session }) {
  const [tab, setTab] = meUseState('dashboard');

  return (
    <div className="me-shell">
      <header className="me-hdr">
        <div className="me-brand">
          <img src="/img/yenda-mark.png" alt="" width="20" height="20" style={{ borderRadius: '50%' }}/>
          <img src="/img/yenda-wordmark.svg" alt="Yenda" height="18"/>
        </div>
        <nav className="me-nav">
          <button className={tab === 'dashboard' ? 'active' : ''} onClick={() => setTab('dashboard')}>My dashboard</button>
          <button className={tab === 'quality' ? 'active' : ''} onClick={() => setTab('quality')}>Quality</button>
          <button className={tab === 'history' ? 'active' : ''} onClick={() => setTab('history')}>History</button>
          <button className={tab === 'payments' ? 'active' : ''} onClick={() => setTab('payments')}>Shifts & payments</button>
        </nav>
        <div className="me-user">
          <Avatar name={session.name} size={28}/>
          <div style={{ fontSize: 12 }}>
            <div style={{ fontWeight: 600 }}>{session.name}</div>
            <div className="meta">{session.team || ''}</div>
          </div>
          <button className="btn btn-sm btn-ghost" onClick={() => window.YDApp.signOut()}>Sign out</button>
        </div>
      </header>

      <main className="me-main">
        <div className="page">
          <PageHeader
            title={
              tab === 'dashboard' ? `Hi ${session.name?.split(' ')[0] || ''}` :
              tab === 'quality'  ? 'Quality' :
              tab === 'history'  ? 'History' :
              'Shifts & payments'
            }
            subtitle={
              tab === 'dashboard' ? `${session.team || ''} · ${new Date().toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'short' })}` :
              tab === 'quality'  ? 'Your reviewer-change rate, trends and recent diffs' :
              tab === 'history'  ? 'Your past weeks · resources, hours, pace and bonus' :
              'Your phone, provider and bonus this week'
            }/>
          {tab === 'dashboard' ? <MyDashboard session={session}/> :
           tab === 'quality'   ? <MeQuality session={session}/> :
           tab === 'history'   ? <History session={session}/> :
                                 <ShiftsAndPayments session={session}/>}
        </div>
      </main>
    </div>
  );
}

Object.assign(window, { MeApp });
