/* ELEV8 ATS - Reports (analytics + data-viz) and the remaining record/ops pages:
   Consultants, Rate library, Audit log, CSMs, Sync queue. */

const DSr = window.Stand8DesignSystem_b5c975;

/* KPI card with a trend sparkline. Numeral uses the MetricCard recipe (.e8-kpi-v in app.css).
   The one canonical stat-tile primitive - stat strips (Job orders, Engagements) route through it too.
   Optional props: spark omitted -> no sparkline slot; valueColor tints the numeral (at-risk signal);
   labelWrap lets long labels wrap (denser strips). All default to the original render. */
function KpiCard({ label, value, delta, dir, spark, valueColor, labelWrap }) {
  const C = window.E8Charts;
  const tone = dir === 'down' ? 'down' : dir === 'flat' ? 'flat' : 'up';
  const deltaColor = dir === 'down' ? 'var(--ui-danger-text)' : dir === 'flat' ? 'var(--ui-text-tertiary)' : 'var(--ui-success-text)';
  const hasSpark = spark && spark.length;
  return (
    <div className="e8-kpi">
      <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', whiteSpace: labelWrap ? 'normal' : 'nowrap' }}>{label}</div>
      <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 8, marginTop: 4 }}>
        <span className="e8-kpi-v" style={valueColor ? { color: valueColor } : undefined}>{value}</span>
        {hasSpark && C ? <span style={{ flex: '0 0 84px' }}><C.Sparkline data={spark} tone={tone} /></span> : null}
      </div>
      {delta ? <div className="tnum" style={{ fontSize: 'var(--ui-text-xs)', color: deltaColor, marginTop: 6 }}>{delta}</div> : null}
    </div>
  );
}

/* ============================== REPORTS ============================== */
function ReportsScreen() {
  const D = window.E8DATA;
  const C = window.E8Charts;
  const R = D.reports;
  const { showToast } = React.useContext(window.E8Ctx);
  const sum = (a) => a.reduce((n, x) => n + x, 0);
  const SOURCE_COLORS = ['var(--ui-accent)', 'var(--ui-chart)', 'var(--ui-success)', 'var(--ui-warning)', 'var(--ui-text-tertiary)'];
  const [range, setRange] = React.useState('This quarter');
  const RANGE_SUB = { 'This quarter': 'Apr - Jun 2026', 'This month': 'June 2026', 'Last 12 months': 'Jul 2025 - Jun 2026' };

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Reports' }]}
        actions={
          <React.Fragment>
            <DSr.MenuButton label={range} icon="calendar_month" size="sm" variant="ghost" align="right"
              items={['This quarter', 'This month', 'Last 12 months'].map((r) => ({ icon: range === r ? 'check' : 'calendar_today', label: r, onClick: () => { setRange(r); showToast('Reports view: ' + r); } }))} />
            <DSr.Button variant="secondary" size="sm" icon="upload" onClick={() => showToast('Report exported as PDF')}>Export</DSr.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 1080 }}>
          <div className="e8-pagehead">
            <div>
              <h1 className="e8-h1">Reports</h1>
              <div className="e8-pagehead-sub">Delivery performance across the desk · {RANGE_SUB[range]}</div>
            </div>
          </div>

          {/* KPI row */}
          <div className="e8-kpi-row">
            <KpiCard label="Submittals" value={sum(R.submittals)} delta="+27% vs prior qtr" dir="up" spark={R.submittals} />
            <KpiCard label="Placements" value={sum(R.placements)} delta="+9 vs prior qtr" dir="up" spark={R.placements} />
            <KpiCard label="Interviews" value={sum(R.interviews)} delta="steady" dir="flat" spark={R.interviews} />
            <KpiCard label="Avg time-to-fill" value="21d" delta="−10 days YTD" dir="up" spark={R.timeToFill.map((t) => -t[1])} />
          </div>

          {/* Hero: trend + win rate + AI impact */}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: 16, alignItems: 'stretch', marginBottom: 16 }}>
            <DSr.Card title="Submittals per week">
              {C ? <C.AreaTrend series={R.submittals} labels={R.weeks} color="var(--ui-accent)" height={180} /> : null}
            </DSr.Card>
            <DSr.Card title="Win rate">
              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, paddingTop: 4 }}>
                {C ? <C.Donut value={R.winRate} centerLabel={Math.round(R.winRate * 100) + '%'} caption="submit → placed" color="var(--ui-accent)" /> : null}
                <DSr.Badge tone="success" dot>{R.winRateDelta} vs prior qtr</DSr.Badge>
              </div>
            </DSr.Card>
          </div>

          {/* AI impact band */}
          <div className="e8-card e8-aibrief" style={{ marginBottom: 16 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
              <span className="material-symbols-outlined" style={{ fontSize: 16, color: 'var(--ui-daybreak-text)' }}>insights</span>
              <span className="e8-sectionlabel">AI impact</span>
              <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>how much the agents moved the desk</span>
            </div>
            <div className="e8-ai-impact">
              {R.aiImpact.map((a) => (
                <div key={a.label}>
                  <div className="tnum" style={{ fontSize: 'var(--ui-text-3xl)', fontWeight: 600, color: 'var(--ui-daybreak-text)' }}>{a.value}</div>
                  <div style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500, marginTop: 2 }}>{a.label}</div>
                  <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{a.sub}</div>
                </div>
              ))}
            </div>
          </div>

          {/* Funnel + conversion */}
          <DSr.Card title="Pipeline funnel" action={<span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>across all open jobs this quarter</span>}>
            {C ? <C.FunnelBars stages={R.funnel} onStage={(label) => { showToast('Opening candidates · ' + label); navigate('candidates'); }} /> : null}
          </DSr.Card>

          {/* Source + leaderboard */}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, margin: '16px 0' }}>
            <DSr.Card title="Where placements came from">
              {C ? <C.BarRow rows={R.sources} color={(i) => SOURCE_COLORS[i % SOURCE_COLORS.length]} /> : null}
            </DSr.Card>
            <DSr.Card title="Recruiter leaderboard" action={<span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>placements</span>}>
              {C ? <C.BarRow rows={R.leaderboard} color="var(--ui-accent)" onRow={(name) => { showToast(name + ' - placements this quarter'); navigate('candidates'); }} renderLabel={(name) => (
                <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><DSr.Avatar name={name} size="sm" />{name}</span>
              )} /> : null}
            </DSr.Card>
          </div>

          {/* Time to fill */}
          <DSr.Card title="Time-to-fill trend" action={<DSr.Badge tone="success" dot>improving</DSr.Badge>}>
            {C ? <C.AreaTrend series={R.timeToFill.map((t) => t[1])} labels={R.timeToFill.map((t) => t[0])} color="var(--ui-success)" height={150} suffix="d" /> : null}
            <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', marginTop: 6 }}>Average days from job open to accepted offer. Down 10 days since January as AI sourcing + screening shortened the top of funnel.</div>
          </DSr.Card>

          {/* Activity heatmap */}
          <div style={{ marginTop: 16 }}>
            <DSr.Card title="Activity this week" action={<span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>touches per day</span>}>
              {C ? <C.Heatmap rows={R.heatmap} days={R.heatmapDays} /> : null}
            </DSr.Card>
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

/* ============================== CONSULTANTS ============================== */
const PO_T = { healthy: 'success', depleting: 'warning', expiring: 'warning', expired: 'danger' };
const PO_LABEL = { healthy: 'Healthy', depleting: 'Depleting', expiring: 'Expiring', expired: 'Expired' };
const marginOf = (e) => Math.round(((e.bill - e.pay) / e.bill) * 100);
const marginTone = (m) => (m >= 28 ? 'success' : m >= 22 ? 'warning' : 'danger');

/* Reusable cell: bill rate over margin. */
function RateCell({ e }) {
  const m = marginOf(e);
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <span className="tnum" style={{ fontWeight: 500 }}>${e.bill}<span style={{ color: 'var(--ui-text-tertiary)', fontWeight: 400 }}>/hr</span></span>
      <DSr.Badge tone={marginTone(m)}>{m}%</DSr.Badge>
    </span>
  );
}

function PoCell({ e }) {
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <DSr.Badge tone={PO_T[e.po]} dot>{PO_LABEL[e.po]}</DSr.Badge>
      <span style={{ width: 54, display: 'inline-flex' }}><DSr.Progress value={e.poBurn} soft={e.po === 'healthy'} /></span>
      <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{e.poBurn}%</span>
    </span>
  );
}

/* R101: reassign a consultant's owning rep. Accepts an array of cids (single = one element). Only
   active reps (departed excluded) can be chosen; the selection defaults to the account CSM's matching
   rep if one exists, else the first active rep, with the account CSM shown as a suggested contact.
   On confirm: setOwner per cid (snapshotting the prior owner for undo) + an audit entry + owner:changed
   per cid, then ONE toast with Undo that restores every prior owner. Gated to Ops/Care by the caller. */
function ReassignOwnerDialog({ cids, onClose }) {
  const D = window.E8DATA;
  const ReassignDialog = window.E8ReassignDialog;
  const { showToast } = React.useContext(window.E8Ctx);
  const list = (cids || []).map((cid) => {
    const o = window.consultantOwner ? window.consultantOwner(cid) : {};
    const en = (D.engagements || []).find((x) => x.cid === cid);
    return { cid, name: en ? en.consultant : cid, client: (o.client || (en ? en.client : '')) || '', csm: o.csm || null };
  });
  const activeReps = (D.reps || []).filter((r) => !r.departed);
  const csmNames = Array.from(new Set(list.map((x) => x.csm).filter(Boolean)));
  const owners = activeReps.map((r) => ({ key: r.id, name: r.name, sub: r.role }));
  // Suggested owner: an active rep whose name matches the first consultant's account CSM, else the first active rep.
  const suggested = (function () {
    const firstCsm = list.length ? list[0].csm : null;
    const match = firstCsm ? activeReps.find((r) => r.name === firstCsm) : null;
    return match || activeReps[0] || null;
  })();
  const n = list.length;
  const doReassign = (opt) => {
    const newRep = opt ? activeReps.find((r) => r.id === opt.key) : null;
    if (!newRep) return;
    const prevs = list.map((x) => {
      const prev = window.E8Store.snapshotOwner(x.cid);
      window.E8Store.setOwner(x.cid, newRep.id);
      if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Reassigned ' + x.name + ' to ' + newRep.name, target: x.name + ' · ' + (x.client || 'no client'), route: 'consultant/' + x.cid });
      if (window.E8Events) window.E8Events.emit('owner:changed', { cid: x.cid, repId: newRep.id });
      return { cid: x.cid, prev: prev };
    });
    onClose();
    showToast('Reassigned ' + n + ' consultant' + (n === 1 ? '' : 's') + ' to ' + newRep.name, 'Undo', () => {
      prevs.forEach((p) => {
        window.E8Store.setOwner(p.cid, p.prev);
        if (window.E8Events) window.E8Events.emit('owner:changed', { cid: p.cid, repId: p.prev });
      });
    });
  };

  return (
    <ReassignDialog
      title={'Reassign ' + (n === 1 ? list[0].name : n + ' consultants')}
      ariaLabel="Reassign owner"
      subjectLabel="Reassigning"
      subject={
        <div className="e8-cown-dlg-list">
          {list.map((x) => (
            <div key={x.cid} className="e8-cown-dlg-row">
              <DSr.Avatar name={x.name} size="sm" />
              <span style={{ minWidth: 0, flex: 1 }}>
                <span style={{ display: 'block', fontWeight: 500, fontSize: 'var(--ui-text-base)' }}>{x.name}</span>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{x.client || 'No client'}{x.csm ? ' · CSM ' + x.csm : ''}</span>
              </span>
            </div>
          ))}
        </div>
      }
      owners={owners}
      currentKey={suggested ? suggested.id : ''}
      pickerHint={csmNames.length ? (
        <div className="e8-cown-dlg-hint">
          <span className="material-symbols-outlined" aria-hidden="true">lightbulb</span>
          <span>Account CSM: {csmNames.join(', ')} - suggested owner</span>
        </div>
      ) : null}
      hint="Reassigning updates the commission owner and clears the orphan flag."
      onReassign={doReassign}
      onClose={onClose}
    />
  );
}

/* Role-aware default segment: Ops/Care land on the orphan pool when it has consultants (else On
   billing); recruiter/AE/CSM land on Mine. Reads live persona + engagements so a persona switch
   snaps straight to the right view. */
function consultantDefaultSeg() {
  const D = window.E8DATA || {};
  const p = window.e8ActivePersona ? window.e8ActivePersona() : null;
  const r = p ? p.role : 'recruiter';
  if (r !== 'ops') return 'mine';
  const pool = (D.engagements || []).filter((e) => window.consultantOwner && window.consultantOwner(e.cid).orphaned);
  return pool.length ? 'unassigned' : 'billing';
}

function ConsultantsScreen({ segment }) {
  const D = window.E8DATA;
  const C = window.E8Charts;
  const { showToast, density } = React.useContext(window.E8Ctx);
  // R101: role-aware "Mine" + Ops/Care-only "Unassigned" pool. The active persona (shared
  // localStorage['e8-persona']) drives which placements are mine, whether the orphan pool + reassign
  // action appear, and the default segment. Re-read on persona/store/owner changes so counts, rows and
  // the pool stay live after a reassignment.
  const [persona, setPersona] = React.useState(() => (window.e8ActivePersona ? window.e8ActivePersona() : null));
  const role = persona ? persona.role : 'recruiter';
  const isOps = role === 'ops';
  const showUnassigned = isOps; // Ops/Care see the orphan pool; recruiter/AE/CSM never do.
  const AVAIL = ['billing', 'mine'].concat(showUnassigned ? ['unassigned'] : []).concat(['bench', 'ending', 'atrisk']);
  const [seg, setSeg] = React.useState(AVAIL.indexOf(segment) > -1 ? segment : consultantDefaultSeg());
  const [reassign, setReassign] = React.useState(null); // array of cids being reassigned, or null
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    if (!window.E8Events) return undefined;
    return window.E8Events.subscribe(['persona:changed', 'store:changed', 'owner:changed'], (ev) => {
      if (window.e8ActivePersona) setPersona(window.e8ActivePersona());
      // On a persona switch, snap to that role's default segment so the demo lands on the right view.
      if (ev && ev.type === 'persona:changed') setSeg(consultantDefaultSeg());
      setTick((t) => t + 1);
    });
  }, []);
  // Guard: if the current segment isn't available for this persona (e.g. Ops -> Recruiter drops the
  // "unassigned" pool), fall back to the role default so segRows/SEG_EMPTY never dereference undefined.
  React.useEffect(() => { if (AVAIL.indexOf(seg) === -1) setSeg(consultantDefaultSeg()); }, [role]);

  const eng = D.engagements;
  const ending = eng.filter((e) => e.renewal !== 'Not due');
  const atRisk = eng.filter((e) => e.po === 'expiring' || e.po === 'expired' || e.sentiment.tone === 'danger');
  const mine = window.myConsultants ? window.myConsultants(persona) : eng;
  const unassigned = eng.filter((e) => (window.consultantOwner ? window.consultantOwner(e.cid).orphaned : false));
  const avgMargin = eng.length ? Math.round(eng.reduce((n, e) => n + marginOf(e), 0) / eng.length) : 0;
  const segCounts = { billing: eng.length, mine: mine.length, unassigned: unassigned.length, bench: D.bench.length, ending: ending.length, atrisk: atRisk.length };
  const segRows = { billing: eng, mine, unassigned, ending, atrisk: atRisk }[seg] || eng;
  // Distinct departed reps behind the pool, for the attention card copy.
  const orphanReps = [];
  unassigned.forEach((e) => { const o = window.consultantOwner(e.cid); if (o.rep && !orphanReps.some((r) => r.id === o.rep.id)) orphanReps.push(o.rep); });
  const lostPhrase = orphanReps.length === 1
    ? ('lost their owner when ' + orphanReps[0].name + ' left' + (orphanReps[0].departedOn ? ' in ' + orphanReps[0].departedOn : ''))
    : 'lost their owner';
  const SEG_EMPTY = {
    billing: { icon: 'badge', title: 'No one on billing', body: 'Active placements appear here once a job order fills.' },
    mine: { icon: 'person', title: 'No consultants assigned to you', body: 'Placements you own show here. Switch the view to see the full desk.' },
    unassigned: { icon: 'verified_user', title: 'No unassigned consultants', body: 'Everyone has an active owner. Orphaned placements surface here when a recruiter leaves.' },
    ending: { icon: 'autorenew', title: 'Nothing in renewal', body: 'Engagements within 90 days of their end date surface here.' },
    atrisk: { icon: 'check_circle', title: 'No consultants at risk', body: 'POs and sentiment are healthy across the desk.' },
  };

  const consultantCols = [
    { key: 'consultant', label: 'Consultant', locked: true, render: (e) => (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
        <DSr.Avatar name={e.consultant} size="sm" />
        <span style={{ minWidth: 0 }}>
          <span style={{ display: 'block', fontWeight: 500 }}>{e.consultant}</span>
          <span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{e.role} · {e.tenure}</span>
        </span>
      </span>
    ) },
    { key: 'client', label: 'Client' },
    { key: 'owner', label: 'Owner', render: (e) => {
      const o = window.consultantOwner ? window.consultantOwner(e.cid) : {};
      if (!o.rep) return <span style={{ color: 'var(--ui-text-tertiary)' }}>-</span>;
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
          {o.rep.name}
          {o.orphaned ? (
            <span className="e8-cown-orphan" title={'Owner left' + (o.rep.departedOn ? ' · ' + o.rep.departedOn : '')}>
              <span className="material-symbols-outlined" aria-hidden="true">warning</span>Left
            </span>
          ) : null}
        </span>
      );
    } },
    { key: 'csm', label: 'CSM', render: (e) => {
      const o = window.consultantOwner ? window.consultantOwner(e.cid) : {};
      return o.csm ? <span>{o.csm}</span> : <span style={{ color: 'var(--ui-text-tertiary)' }}>-</span>;
    } },
    { key: 'rate', label: 'Bill · margin', render: (e) => <RateCell e={e} /> },
    { key: 'po', label: 'PO health', width: 190, render: (e) => <PoCell e={e} /> },
    { key: 'sentiment', label: 'Sentiment', render: (e) => <DSr.Badge tone={e.sentiment.tone} dot>{e.sentiment.label}</DSr.Badge> },
    { key: 'ends', label: 'Ends', num: true },
    { key: 'renewal', label: 'Renewal', render: (e) => e.renewal === 'Not due' ? <span style={{ color: 'var(--ui-text-tertiary)', fontSize: 'var(--ui-text-base)' }}>Not due</span> : <DSr.Badge tone={e.escalated ? 'danger' : 'accent'}>{e.renewal}</DSr.Badge> },
  ];

  return (
    <React.Fragment>
      <Topbar crumbs={[{ label: 'Consultants' }]} actions={
        <React.Fragment>
          <DSr.Button variant="secondary" size="sm" icon="upload">Export</DSr.Button>
          <DSr.Button variant="primary" size="sm" icon="person_add">Add consultant</DSr.Button>
        </React.Fragment>
      } />
      <div className="e8-content">
        <div className="e8-page">
          <div className="e8-pagehead e8-pagehead-wrap">
            <div className="e8-pagehead-main">
              <h1 className="e8-h1">Consultants</h1>
              <div className="e8-pagehead-sub">Your deployed workforce and the bench, synced from Bullhorn</div>
            </div>
            <div className="e8-pagehead-actions">
              <window.PersonaViewAs />
            </div>
          </div>

          {/* KPI band */}
          <div className="e8-kpi-row">
            <KpiCard label="On billing" value={String(eng.length)} delta="+2 vs last month" dir="up" spark={[10, 11, 12, 13, 13, eng.length]} />
            <KpiCard label="Avg margin" value={avgMargin + '%'} delta="+1.5 pts QoQ" dir="up" spark={[26, 26, 27, 27, 28, avgMargin]} />
            <KpiCard label="POs at risk" value={String(atRisk.length)} delta={atRisk.length ? atRisk.length + ' need action now' : 'all healthy'} dir={atRisk.length ? 'down' : 'flat'} spark={[1, 1, 2, 2, 3, atRisk.length]} />
            <KpiCard label="On the bench" value={String(D.bench.length)} delta={D.bench.reduce((n, b) => n + b.matches, 0) + ' open matches'} dir="flat" spark={[8, 7, 7, 6, 6, D.bench.length]} />
          </div>

          {/* Segments */}
          <div style={{ margin: '4px 0 12px' }}>
            <DSr.SubTabs
              items={[
                { id: 'billing', label: 'On billing', count: segCounts.billing },
                { id: 'mine', label: 'Mine', count: segCounts.mine },
              ].concat(showUnassigned ? [{ id: 'unassigned', label: 'Unassigned · needs owner', count: segCounts.unassigned }] : []).concat([
                { id: 'bench', label: 'Bench', count: segCounts.bench },
                { id: 'ending', label: 'In renewal', count: segCounts.ending },
                { id: 'atrisk', label: 'At risk', count: segCounts.atrisk },
              ])}
              active={seg}
              onChange={setSeg}
            />
          </div>

          {seg === 'bench' ? (
            !D.bench.length ? (
              <div style={{ maxWidth: 460, margin: '24px auto' }}><DSr.EmptyState icon="groups" title="Bench is clear" body="Everyone is deployed. Roll-offs surface here before they happen." /></div>
            ) : (
            <React.Fragment>
              <div className="e8-card e8-aibrief" style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
                <span className="material-symbols-outlined" style={{ fontSize: 16, color: 'var(--ui-daybreak-text)' }}>auto_awesome</span>
                <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text)' }}>ELEV8 matched <b>{D.bench.reduce((n, b) => n + b.matches, 0)} open roles</b> across the bench. Redeploying before roll-off protects margin.</span>
              </div>
              <ConfigurableTable listKey="consultants-bench"
                density={density}
                registry={[
                  { key: 'name', label: 'Consultant', render: (b) => (
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
                      <DSr.Avatar name={b.name} size="sm" />
                      <span style={{ minWidth: 0 }}>
                        <span style={{ display: 'block', fontWeight: 500 }}>{b.name}</span>
                        <span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{b.role}</span>
                      </span>
                    </span>
                  ) },
                  { key: 'skills', label: 'Skills', render: (b) => <DSr.SkillChips skills={b.skills} max={(b.skills || []).length} style={{ flexWrap: 'wrap' }} /> },
                  { key: 'available', label: 'Available', render: (b) => <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><span className="material-symbols-outlined" style={{ fontSize: 14, color: 'var(--ui-text-tertiary)' }}>schedule</span><span className="tnum">{b.available}</span></span> },
                  { key: 'target', label: 'Target rate', num: true, render: (b) => <span className="tnum">${b.target}/hr</span> },
                  { key: 'matches', label: 'Matched roles', render: (b) => (
                    <span title={'Top match: ' + b.topMatch}>
                      <DSr.Badge tone={b.matches >= 3 ? 'success' : 'accent'} dot>{b.matches} open</DSr.Badge>
                    </span>
                  ) },
                ]}
                rows={D.bench}
                rowActions={(b) => <DSr.Button variant="secondary" size="sm" icon="search" onClick={() => {
                  const ranked = window.E8Match ? window.E8Match.redeployJobs(b.id, b.skills) : [];
                  if (ranked.length) navigate('job/' + ranked[0].jobId + '/matching');
                  else showToast('No scored openings for ' + b.name + ' yet', 'Open jobs', () => navigate('jobs'));
                }}>Find roles</DSr.Button>}
              />
            </React.Fragment>
            )
          ) : (
            <React.Fragment>
              {seg === 'unassigned' && unassigned.length ? (
                <div className="e8-card e8-cown-attn">
                  <span className="e8-cown-attn-glyph material-symbols-outlined" aria-hidden="true">person_alert</span>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="e8-cown-attn-title">{unassigned.length} consultant{unassigned.length === 1 ? '' : 's'} {lostPhrase} - reassign them to keep coverage.</div>
                    <div className="e8-cown-attn-sub">The owning recruiter departed. Assign each to an active owner so nothing goes uncovered.</div>
                  </div>
                  {isOps ? <DSr.Button variant="primary" size="sm" icon="swap_horiz" onClick={() => setReassign(unassigned.map((e) => e.cid))}>Reassign all</DSr.Button> : null}
                </div>
              ) : null}
              {segRows.length ? (
                <ConfigurableTable
                  listKey="consultants"
                  density={density}
                  onRowClick={(e) => navigate('consultant/' + e.cid)}
                  registry={consultantCols}
                  rows={segRows}
                  rowActions={(e) => (
                    seg === 'unassigned' && isOps ? (
                      <DSr.Button variant="secondary" size="sm" icon="swap_horiz" onClick={() => setReassign([e.cid])}>Reassign</DSr.Button>
                    ) : (
                      <React.Fragment>
                        <DSr.Button variant="ghost" size="sm" icon="call" onClick={() => showToast('Care call queued for ' + e.consultant, 'Undo')}>Log touch</DSr.Button>
                        {e.renewal !== 'Not due'
                          ? <DSr.Button variant="ghost" size="sm" icon="autorenew" onClick={() => navigate('renewals')}>Renew</DSr.Button>
                          : null}
                      </React.Fragment>
                    )
                  )}
                />
              ) : (
                <div style={{ maxWidth: 460, margin: '24px auto' }}><DSr.EmptyState icon={SEG_EMPTY[seg].icon} title={SEG_EMPTY[seg].title} body={SEG_EMPTY[seg].body} /></div>
              )}
            </React.Fragment>
          )}
        </div>
      </div>
      {reassign ? <ReassignOwnerDialog cids={reassign} onClose={() => setReassign(null)} /> : null}
    </React.Fragment>
  );
}

/* ---------- Consultant record ---------- */
/* The consultant time-entry sheet: a daily grid (project/SOW + category rows) that auto-classifies hours
   into REG/OT/DT by the location rule, supports multi-project allocation, live editing, and the
   draft -> submit -> approve lifecycle. Recomputes billings/margin live via E8Time.calcSheet. */
function TimeSheetPanel({ e }) {
  const T = window.E8Time, E = window.E8Econ, D = window.E8DATA;
  const { showToast } = React.useContext(window.E8Ctx);
  const cats = D.payCats || [];
  const dayNames = D.dayNames || [];
  const weeks = D.tsWeeks || [];
  const weekLabel = weeks[weeks.length - 1] || 'This week';
  const sheet = (D.timesheets || {})[e.cid];
  const storeKey = 'e8-ts-' + e.cid;
  const [cur, setCur] = React.useState(function () {
    try { const s = localStorage.getItem(storeKey); if (s) return JSON.parse(s); } catch (err) {}
    return sheet ? JSON.parse(JSON.stringify(sheet.current)) : { status: 'draft', lines: [], extra: [] };
  });
  const [editing, setEditing] = React.useState(false);
  React.useEffect(function () { try { localStorage.setItem(storeKey, JSON.stringify(cur)); } catch (err) {} }, [cur]);
  /* R80: external writers exist (notification approvals, TimeHub in another tab) - re-read
     the stored sheet when they decide this cid, so this panel's persist effect can't revert it. */
  React.useEffect(function () {
    return window.E8Events ? window.E8Events.subscribe(['timesheet:decided'], function (ev) {
      if (!ev.payload || ev.payload.cid !== e.cid || ev.payload.via === 'panel') return;
      try { const s2 = localStorage.getItem(storeKey); if (s2) setCur(JSON.parse(s2)); } catch (err) {}
    }) : undefined;
  }, [e.cid]);
  if (!sheet || !T) return null;

  const cc = T.calcSheet(e, { workState: sheet.workState, prior: sheet.prior, current: cur });
  const c = cc.current, rule = cc.rule;
  const owner = E ? E.rep(E.repOf(e.cid)) : null;
  const repTier = (E && owner) ? E.tierRateAt(E.repSummary(owner.id).marginApproved, owner) : 0;
  const money = function (n) { return '$' + Math.round(n).toLocaleString(); };
  const catMap = {}; cats.forEach(function (k) { catMap[k.id] = k; });
  const ST = { draft: { t: 'neutral', l: 'Draft' }, pending: { t: 'neutral', l: 'Not submitted' }, submitted: { t: 'warning', l: 'Submitted' }, approved: { t: 'success', l: 'Approved' } };
  const stat = ST[c.status] || ST.draft;
  const locked = c.status === 'approved';

  const setDay = function (li, di, v) {
    const n = Math.max(0, Math.min(24, parseInt(v, 10) || 0));
    setCur(function (prev) {
      const lines = (prev.lines || []).map(function (l, i) { return i === li ? Object.assign({}, l, { days: l.days.map(function (d, j) { return j === di ? n : d; }) }) : l; });
      return Object.assign({}, prev, { lines: lines });
    });
  };
  const act = function (status, kind) {
    const prev = cur; /* R78: snapshot for Undo - approvals move real commissions now */
    setCur(function (p) {
      return Object.assign({}, p, {
        status: status,
        submittedOn: status === 'submitted' ? (p.submittedOn || weekLabel) : (status === 'pending' ? null : p.submittedOn),
        approvedOn: status === 'approved' ? weekLabel : (status === 'submitted' || status === 'pending' ? null : p.approvedOn),
      });
    });
    const actionLbl = status === 'approved' ? 'Approved timesheet' : kind === 'rejected' ? 'Returned timesheet for changes' : status === 'submitted' ? 'Submitted timesheet' : null;
    if (actionLbl && window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: actionLbl, target: e.consultant + ' · ' + money(c.billings), route: 'consultant/' + e.cid });
    if (window.E8Events) window.E8Events.emit('timesheet:decided', { cid: e.cid, status: status, via: 'panel' });
    const undoAct = function () { setCur(prev); if (window.E8Events) window.E8Events.emit('timesheet:decided', { cid: e.cid, status: 'undone', via: 'panel' }); };
    if (status === 'approved') showToast('Timesheet approved · ' + money(c.billings) + ' billings booked' + (owner ? ', ~' + money(c.margin * repTier) + ' to ' + owner.name.split(' ')[0] : ''), 'Undo', undoAct);
    else if (kind === 'rejected') showToast('Returned to ' + e.consultant + ' for changes', 'Undo', undoAct);
    else if (status === 'submitted') showToast('Submitted for approval to ' + (cur.approver || 'manager'), 'Undo', undoAct);
  };

  return (
    <div className="e8-card e8-tsheet">
      <div className="e8-tsheet-head">
        <div style={{ minWidth: 0 }}>
          <span className="e8-sectionlabel">Time entry</span>
          <div className="e8-tsheet-sub">Week of {weekLabel} · {rule.label} · {rule.note}</div>
        </div>
        <DSr.Badge tone={stat.t} dot>{stat.l}</DSr.Badge>
      </div>

      <div className="e8-tsheet-prior">
        <span className="material-symbols-outlined" style={{ fontSize: 14 }}>history</span>
        {weeks.slice(0, -1).map(function (w, i) { return <span key={w} className="e8-tsheet-prior-w"><i>{w}</i><b className="tnum">{(sheet.prior || [])[i] || 0}h</b></span>; })}
        <span className="e8-tsheet-prior-sum">{cc.prior.hours}h booked · {money(cc.prior.billings)}</span>
      </div>

      <div className="e8-tsheet-scroll">
        <div className="e8-tsheet-grid">
          <div className="e8-tsheet-row e8-tsheet-hrow">
            <span className="e8-tsheet-cell e8-tsheet-lh">Project / type</span>
            {dayNames.map(function (d, i) { return <span key={d} className={'e8-tsheet-cell e8-tsheet-dh' + (i === 5 || i === 6 ? ' wknd' : '')}>{d}</span>; })}
            <span className="e8-tsheet-cell e8-tsheet-th">Tot</span>
          </div>
          {(cur.lines || []).map(function (ln, li) {
            const proj = ln.projectId ? (D.managedEngagements || []).find(function (m) { return m.id === ln.projectId; }) : null;
            const lt = (ln.days || []).reduce(function (s, x) { return s + x; }, 0);
            return (
              <div key={li} className="e8-tsheet-row">
                <span className="e8-tsheet-cell e8-tsheet-lh">
                  <b>{proj ? proj.name : (e.client || 'Staff aug')}</b>
                  <span className="e8-tsheet-task">{ln.task}{proj ? ' · SOW' : ' · Staff aug'}</span>
                </span>
                {(ln.days || []).map(function (h, di) {
                  return (editing && !locked)
                    ? <input key={di} className="e8-tsheet-cell e8-tsheet-in tnum" type="number" min="0" max="24" value={h} onChange={function (ev) { setDay(li, di, ev.target.value); }} />
                    : <span key={di} className={'e8-tsheet-cell e8-tsheet-dc' + (h ? '' : ' z') + ((di === 5 || di === 6) && h ? ' wknd' : '')}>{h || '·'}</span>;
                })}
                <span className="e8-tsheet-cell e8-tsheet-tc tnum">{lt}</span>
              </div>
            );
          })}
          {(cur.extra || []).map(function (x, xi) {
            const cm = catMap[x.cat] || {};
            const xt = (x.days || []).reduce(function (s, h) { return s + h; }, 0);
            return (
              <div key={'x' + xi} className="e8-tsheet-row">
                <span className="e8-tsheet-cell e8-tsheet-lh"><b>{cm.label || x.cat}</b><span className={'e8-tsheet-tag cat-' + x.cat}>{cm.abbr || x.cat}</span></span>
                {(x.days || []).map(function (h, di) { return <span key={di} className={'e8-tsheet-cell e8-tsheet-dc' + (h ? '' : ' z')}>{h || '·'}</span>; })}
                <span className="e8-tsheet-cell e8-tsheet-tc tnum">{xt}</span>
              </div>
            );
          })}
          <div className="e8-tsheet-row e8-tsheet-clsrow">
            <span className="e8-tsheet-cell e8-tsheet-lh">Classified</span>
            {c.days.map(function (d, di) {
              return (
                <span key={di} className="e8-tsheet-cell e8-tsheet-cls">
                  {d.worked ? (
                    <span className="e8-tsheet-pips">
                      {d.reg ? <i className="e8-pip cat-reg" title={d.reg + 'h regular'}>{d.reg}</i> : null}
                      {d.ot ? <i className="e8-pip cat-ot" title={d.ot + 'h overtime'}>{d.ot}</i> : null}
                      {d.dt ? <i className="e8-pip cat-dt" title={d.dt + 'h double-time'}>{d.dt}</i> : null}
                    </span>
                  ) : <span className="e8-tsheet-z">·</span>}
                </span>
              );
            })}
            <span className="e8-tsheet-cell e8-tsheet-tc tnum">{c.worked}</span>
          </div>
        </div>
      </div>

      <div className="e8-tsheet-cats">
        {cats.map(function (k) {
          const h = c.cats[k.id]; if (!h) return null;
          return <span key={k.id} className={'e8-tsheet-chip cat-' + k.id}><b>{k.abbr}</b> {h}h <span className="e8-tsheet-chip-x">{k.billMult}x bill</span></span>;
        })}
        {c.flags.length ? <span className="e8-tsheet-flagsep" /> : null}
        {c.flags.map(function (f) { return <span key={f.k} className={'e8-tsheet-flag cat-' + f.k}>{f.label}</span>; })}
      </div>

      {c.byProject.length > 1 ? (
        <div className="e8-tsheet-proj-roll">
          <div className="e8-tsheet-proj-h">Allocation across SOWs</div>
          {c.byProject.map(function (p, pi) {
            const proj = p.projectId ? (D.managedEngagements || []).find(function (m) { return m.id === p.projectId; }) : null;
            const ph = p.reg + p.ot + p.dt + (p.oncall || 0);
            return (
              <div key={pi} className={'e8-tsheet-proj-row' + (proj ? ' link' : '')} {...(proj ? window.clickableProps(function () { navigate('engagement/' + proj.id); }) : {})}>
                <span className="e8-tsheet-proj-n">{proj ? proj.name : (e.client || 'Staff aug')}</span>
                <span className="tnum">{ph}h · {money(p.billings)}</span>
              </div>
            );
          })}
        </div>
      ) : null}

      <div className="e8-tsheet-tot">
        <div><span className="e8-tsheet-tot-k">Billable</span><span className="e8-tsheet-tot-v tnum">{c.billableHours}h</span></div>
        <div><span className="e8-tsheet-tot-k">Billings</span><span className="e8-tsheet-tot-v tnum">{money(c.billings)}</span></div>
        <div><span className="e8-tsheet-tot-k">Gross margin</span><span className="e8-tsheet-tot-v tnum" style={{ color: 'var(--ui-success-text)' }}>{money(c.margin)}</span></div>
        <div><span className="e8-tsheet-tot-k">Eff. rate</span><span className="e8-tsheet-tot-v tnum">${Math.round(c.effBill)}/hr</span></div>
      </div>

      {c.note ? <div className="e8-tsheet-note"><span className="material-symbols-outlined" style={{ fontSize: 15 }}>sticky_note_2</span><span>{c.note}</span></div> : null}

      <div className="e8-tsheet-foot">
        <div className="e8-tsheet-metaline">
          {c.status === 'approved' ? 'Approved ' + (c.approvedOn || weekLabel) + ' by ' + (cur.approver || 'manager')
            : c.status === 'submitted' ? 'Submitted ' + (c.submittedOn || weekLabel) + ' · awaiting ' + (cur.approver || 'manager')
              : 'Approver ' + (cur.approver || 'manager') + ' · not submitted'}
        </div>
        <div className="e8-tsheet-acts">
          {editing
            ? <DSr.Button variant="primary" size="sm" icon="check" onClick={function () { setEditing(false); showToast('Timesheet saved'); }}>Save</DSr.Button>
            : <DSr.Button variant="ghost" size="sm" icon="edit" onClick={function () { setEditing(true); }} disabled={locked}>Edit hours</DSr.Button>}
          {(c.status === 'draft' || c.status === 'pending') ? <DSr.Button variant="primary" size="sm" icon="send" onClick={function () { act('submitted'); }}>Submit</DSr.Button> : null}
          {c.status === 'submitted' ? <DSr.Button variant="ghost" size="sm" icon="undo" onClick={function () { act('pending', 'rejected'); }}>Reject</DSr.Button> : null}
          {c.status === 'submitted' ? <DSr.Button variant="primary" size="sm" icon="check" onClick={function () { act('approved'); }}>Approve</DSr.Button> : null}
          {c.status === 'approved' ? <DSr.Button variant="ghost" size="sm" icon="lock_open" onClick={function () { act('submitted'); }}>Reopen</DSr.Button> : null}
        </div>
      </div>

      {owner ? (
        <div className="e8-tsheet-rep">
          <DSr.Avatar name={owner.name} size="sm" />
          <div style={{ flex: 1, minWidth: 0, fontSize: 'var(--ui-text-sm)' }}>
            <b>{owner.name}</b> earns on this placement <span style={{ color: 'var(--ui-text-tertiary)' }}>· ~{money(c.margin * repTier)} this period at {Math.round(repTier * 100)}%</span>
          </div>
          {c.status === 'submitted' ? <span className="e8-tsheet-rep-pend">+{money(c.margin * repTier)} pending</span> : null}
        </div>
      ) : null}
    </div>
  );
}

function ConsultantDetailScreen({ id }) {
  const D = window.E8DATA;
  const C = window.E8Charts;
  const { showToast } = React.useContext(window.E8Ctx);
  const cap = React.useContext(window.CaptureCtx);
  const [tab, setTab] = React.useState('overview');
  const swt = useSwipeTabs(['overview', 'time', 'casestudy'], tab, setTab);
  // R101: reassign the owning rep (Ops/Care only). Subscribe before the not-found guard so hook order
  // stays stable; re-render on owner/store/persona changes so the Owner rail + orphan chip stay live.
  const [reassignOpen, setReassignOpen] = React.useState(false);
  const [, setDetailTick] = React.useState(0);
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['owner:changed', 'store:changed', 'persona:changed'], () => setDetailTick((t) => t + 1)) : undefined), []);
  const found = D.engagements.find((x) => x.cid === id);

  /* Not-found guard (R73-75 convention) - runs before ANY dereference of the row,
     so a bogus id or an empty engagements collection renders safely. */
  if (!found) {
    return (
      <React.Fragment>
        <Topbar crumbs={[{ label: 'Consultants', to: 'consultants' }, { label: 'Not found' }]} />
        <div className="e8-content"><div className="e8-page">
          <div style={{ maxWidth: 460, margin: '24px auto', textAlign: 'center' }}>
            <DSr.EmptyState icon="person_off" title="Consultant not found" body="This consultant is not on billing, or the link is out of date." />
            <div style={{ marginTop: 12 }}><DSr.Button variant="secondary" size="sm" icon="arrow_back" onClick={() => navigate('consultants')}>Back to consultants</DSr.Button></div>
          </div>
        </div></div>
      </React.Fragment>
    );
  }

  const e = found;
  const detailPersona = window.e8ActivePersona ? window.e8ActivePersona() : null;
  const canReassign = !!(detailPersona && detailPersona.role === 'ops');
  const ownerInfo = window.consultantOwner ? window.consultantOwner(e.cid) : {};
  const client = D.clients.find((cl) => cl.name === e.client);
  const CaseStudyCard = window.CaseStudyCard;
  const caseStudy = D.caseStudies && D.caseStudies[e.id];
  const program = e.projectId ? (D.managedEngagements || []).find((mm) => mm.id === e.projectId) : null;
  const m = marginOf(e);
  const daysLabel = 100 - e.poBurn;
  const tt = window.E8Econ ? window.E8Econ.tsTotals(e.cid) : null;
  const owner = window.E8Econ ? window.E8Econ.rep(window.E8Econ.repOf(e.cid)) : null;
  const payRule = tt && tt.calc ? tt.calc.rule : null;
  const TS_TONE = { draft: 'neutral', pending: 'neutral', submitted: 'warning', approved: 'success' };
  const TS_LABEL = { draft: 'Draft', pending: 'Not submitted', submitted: 'Submitted', approved: 'Approved' };
  const cc = tt && tt.calc ? tt.calc.current : null;
  const tsumMaxD = cc ? Math.max.apply(null, cc.days.map(function (d) { return d.worked; }).concat([1])) : 1;
  const tsumBreakdown = cc ? (cc.cats.ot || cc.cats.dt || cc.cats.oncall || cc.cats.pto || cc.cats.hol) : 0;
  const tsumAppr = !cc ? '' : (tt.status === 'approved' ? ('Approved ' + (cc.approvedOn || '') + (cc.approver ? ' by ' + cc.approver : ''))
    : tt.status === 'submitted' ? ('Submitted ' + (cc.submittedOn || '') + (cc.approver ? ' · awaiting ' + cc.approver : ''))
    : ('Not submitted' + (cc.approver ? ' · approver ' + cc.approver : '')));

  /* Redeploy matches - E8Match-scored open jobs (skill-overlap fallback), surfaced when the engagement is winding down. */
  const winding = e.po === 'expiring' || e.po === 'expired' || (e.redeploy && e.redeploy.tone !== 'neutral');
  const redeployMatches = winding && window.E8Match ? window.E8Match.redeployJobs(e.cid, e.skills).slice(0, 3) : [];

  /* Care + renewal timeline, derived from the engagement. R104 T2: merge the consultant's own
     structured notes into it (newest first), and - because Task 1 threads the consultant back to
     the candidate it was placed from - optionally surface candidate-phase notes as earlier history. */
  const baseTimeline = [
    e.renewal !== 'Not due' ? { id: 'ct-r', when: 'This week', actor: 'Renewals', ai: true, prov: 'drafted', title: 'Renewal ' + e.renewal.toLowerCase(), body: e.escalated ? 'PO expired - renewal escalated to the CSM.' : 'Renewal draft built from rate, tenure and care sentiment - waiting in Approvals.' } : null,
    { id: 'ct-c', when: e.lastTouch, actor: 'Sarah Kim', ai: false, prov: 'human', title: 'Care touch logged', body: 'Sentiment read as ' + e.sentiment.label.toLowerCase() + '. ' + (e.sentiment.tone === 'danger' ? 'Flagged for follow-up.' : 'No blockers raised.') },
    { id: 'ct-s', when: e.start, actor: 'ELEV8', ai: true, prov: 'qa', title: 'Engagement started', body: e.consultant + ' placed at ' + e.client + ' as ' + e.role + '.' },
  ].filter(Boolean);
  const consultNotes = window.e8NoteEvents ? window.e8NoteEvents('consultant', e.cid) : [];
  const candPhaseNotes = (e.candId && window.e8NoteEvents) ? window.e8NoteEvents('candidate', e.candId, { candidatePhase: true }) : [];
  const timeline = window.e8MergeEvents ? window.e8MergeEvents(baseTimeline.concat(consultNotes, candPhaseNotes)) : baseTimeline;
  const openConsultNote = () => (cap ? cap.open({ target: 'note', entityLabel: e.consultant, refType: 'consultant', refId: e.cid }) : null);

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Consultants', to: 'consultants' }, { label: e.consultant }]}
        actions={
          <React.Fragment>
            <DSr.Button variant="ghost" size="sm" icon="note_add" onClick={openConsultNote}>Add note</DSr.Button>
            <DSr.Button variant="secondary" size="sm" icon="autorenew" onClick={() => (e.renewal !== 'Not due' ? navigate('renewals') : showToast('Renewal started for ' + e.consultant))}>{e.renewal !== 'Not due' ? 'View renewal' : 'Start renewal'}</DSr.Button>
            <DSr.Button variant="primary" size="sm" icon="mail" onClick={() => showToast('Message drafted to ' + e.consultant)}>Message</DSr.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page">
          <RecordHeader
            mark={<DSr.Avatar name={e.consultant} size="xl" />}
            title={e.consultant}
            badges={<React.Fragment>
              <DSr.Badge tone={e.sentiment.tone} dot>{e.sentiment.label}</DSr.Badge>
              <DSr.Badge tone={PO_T[e.po]} dot>PO {PO_LABEL[e.po].toLowerCase()}</DSr.Badge>
            </React.Fragment>}
            meta={<React.Fragment>{e.role} · <a className="e8-link" tabIndex={0} onKeyDown={window.keyActivate} onClick={() => (client ? navigate('client/' + client.id) : null)}>{e.client}</a> · {e.location}</React.Fragment>}
          />

          {program ? (
            <div className="e8-card e8-prog-banner" style={{ marginBottom: 16, cursor: 'pointer' }} {...window.clickableProps(() => navigate('engagement/' + program.id))}>
              <span className="material-symbols-outlined" style={{ color: 'var(--ui-accent)' }}>hub</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 600 }}>Part of {program.name}</div>
                <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Managed service · {program.team.length} consultants · {program.contract.tcv} TCV · health {program.health.score}</div>
              </div>
              <DSr.Badge tone={program.health.tone} dot>{program.health.tone === 'success' ? 'Healthy' : 'At risk'}</DSr.Badge>
              <span className="material-symbols-outlined" style={{ color: 'var(--ui-text-tertiary)' }}>chevron_right</span>
            </div>
          ) : null}

          <div className="e8-record-2col">
            <div style={{ flex: 1, minWidth: 0 }}>
              <DSr.Tabs
                items={[{ id: 'overview', label: 'Overview' }, { id: 'time', label: 'Timesheet' }, { id: 'casestudy', label: 'Case study' }]}
                active={tab}
                onChange={setTab}
              />
              <div style={{ paddingTop: 18, minWidth: 0, display: 'grid', gap: 18, ...(swt.style || {}) }} {...swt.bind}>
                {tab === 'overview' ? (
                  <React.Fragment>
                    {/* PO burn + economics */}
                    <div className="e8-card e8-intel-card">
                      <div className="e8-intel-cardhead">
                        <span className="e8-sectionlabel">Purchase order burn</span>
                        <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{e.poBurn}% consumed · {daysLabel}% remaining</span>
                      </div>
                      {C ? <C.AreaTrend series={e.burnSpark} labels={['', '', '', '', '', 'now']} color={e.po === 'expired' || e.po === 'expiring' ? 'var(--ui-danger)' : 'var(--ui-accent)'} height={120} suffix="%" showValue={false} /> : null}
                      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginTop: 12 }}>
                        {[['Bill rate', '$' + e.bill + '/hr'], ['Pay rate', '$' + e.pay + '/hr'], ['Margin', m + '%'], ['Annual value', e.renewalValue]].map(([k, v], i) => (
                          <div key={k} style={{ borderLeft: i === 2 ? '2px solid var(--ui-accent)' : '1px solid var(--ui-border)', paddingLeft: 10 }}>
                            <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{k}</div>
                            <div className="tnum" style={{ fontSize: 'var(--ui-text-lg)', fontWeight: 600, marginTop: 2 }}>{v}</div>
                          </div>
                        ))}
                      </div>
                    </div>

                    {/* Compact time summary (daily rhythm) - opens the Timesheet tab */}
                    {tt && cc ? (
                      <div className="e8-card e8-tsum" role="button" tabIndex={0} onClick={() => setTab('time')} onKeyDown={(ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); setTab('time'); } }}>
                        <div className="e8-intel-cardhead">
                          <span className="e8-sectionlabel">Time entry</span>
                          <DSr.Badge tone={TS_TONE[tt.status] || 'neutral'} dot>{TS_LABEL[tt.status] || tt.status}</DSr.Badge>
                        </div>
                        <div className="e8-tsum-hero">
                          <div className="e8-tsum-hero-h">
                            <div className="e8-tsum-hero-n tnum">{cc.worked}<span className="e8-tsum-hero-u">h</span></div>
                            <div className="e8-tsum-hero-l">this week</div>
                          </div>
                          <div className="e8-tsum-days" aria-hidden="true">
                            {cc.days.map((d, i) => (
                              <div key={i} className="e8-tsum-day">
                                <div className="e8-tsum-daycol">
                                  {d.worked ? (
                                    <div className="e8-tsum-daybar" style={{ height: Math.max(14, d.worked / tsumMaxD * 100) + '%' }} title={(D.dayNames[i] || '') + ' · ' + d.worked + 'h'}>
                                      {d.dt ? <span className="e8-tsum-seg-dt" style={{ flexGrow: d.dt, flexBasis: 0 }} /> : null}
                                      {d.ot ? <span className="e8-tsum-seg-ot" style={{ flexGrow: d.ot, flexBasis: 0 }} /> : null}
                                      {d.reg ? <span className="e8-tsum-seg-reg" style={{ flexGrow: d.reg, flexBasis: 0 }} /> : null}
                                    </div>
                                  ) : <div className="e8-tsum-daybar empty" title={(D.dayNames[i] || '') + ' · off'} />}
                                </div>
                                <span className="e8-tsum-daylbl">{(D.dayNames[i] || ' ')[0]}</span>
                              </div>
                            ))}
                          </div>
                        </div>
                        {tsumBreakdown ? (
                          <div className="e8-tsum-cats">
                            {(D.payCats || []).map((k) => { const h = cc.cats[k.id]; if (!h) return null; return <span key={k.id} className={'e8-tsheet-chip cat-' + k.id}><b>{k.abbr}</b> {h}h</span>; })}
                          </div>
                        ) : null}
                        <div className="e8-tsum-money">
                          <span><b>${Math.round(cc.billings).toLocaleString()}</b> <span className="e8-tsum-money-k">billed</span></span>
                          <span><b style={{ color: 'var(--ui-success-text)' }}>${Math.round(cc.margin).toLocaleString()}</b> <span className="e8-tsum-money-k">margin</span></span>
                          <span><b>${Math.round(cc.effBill)}</b><span className="e8-tsum-money-k">/hr eff</span></span>
                        </div>
                        <div className="e8-tsum-foot">
                          <span className="e8-tsum-appr">{tsumAppr}</span>
                          <span className="e8-tsum-open">Open timesheet <span className="material-symbols-outlined" style={{ fontSize: 15 }}>arrow_forward</span></span>
                        </div>
                      </div>
                    ) : null}

                    {/* Renewal status */}
                    {e.renewal !== 'Not due' ? (
                      <div className="e8-card e8-intel-card">
                        <div className="e8-intel-cardhead">
                          <span className="e8-sectionlabel">Renewal</span>
                          <DSr.Badge tone={e.escalated ? 'danger' : 'accent'} dot>{e.escalated ? 'Escalated' : e.renewal}</DSr.Badge>
                        </div>
                        <div style={{ marginBottom: 12 }}><DSr.StageCell stages={D.renewalStages} current={e.renewalStage || 0} /></div>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 18, flexWrap: 'wrap' }}>
                          <span style={{ fontSize: 'var(--ui-text-sm)' }}><span style={{ color: 'var(--ui-text-tertiary)' }}>Extension value </span><b className="tnum">{e.renewalValue}</b></span>
                          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 'var(--ui-text-sm)' }}>
                            <span style={{ color: 'var(--ui-text-tertiary)' }}>Likelihood</span>
                            <span style={{ width: 70, display: 'inline-flex' }}><DSr.Progress value={e.extensionPct} soft={e.extensionPct < 50} /></span>
                            <b className="tnum">{e.extensionPct}%</b>
                          </span>
                          <DSr.Button variant="secondary" size="sm" icon="mail" onClick={() => showToast('Renewal email drafted to ' + e.sponsor + ' - review in Approvals', 'Open approvals', () => navigate('approvals'))}>Draft renewal</DSr.Button>
                        </div>
                      </div>
                    ) : null}

                    {/* Redeploy options */}
                    {redeployMatches.length ? (
                      <div>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                          <span className="e8-sectionlabel">Redeploy options</span>
                          <DSr.ProvenanceBadge kind="advised" />
                          <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>matched to {e.consultant.split(' ')[0]}’s skills before roll-off</span>
                        </div>
                        <div style={{ display: 'grid', gap: 8 }}>
                          {redeployMatches.map((rj) => (
                            <button key={rj.jobId} type="button" className="e8-jobmatch" onClick={() => navigate('job/' + rj.jobId + '/matching')}>
                              <span style={{ minWidth: 0, flex: 1, textAlign: 'left' }}>
                                <span style={{ display: 'flex', alignItems: 'center', gap: 7, flexWrap: 'wrap' }}>
                                  <DSr.MatchBadge tier={rj.tier} score={rj.score} dot />
                                  <span style={{ fontWeight: 600, fontSize: 'var(--ui-text-base)' }}>{rj.job ? rj.job.title : rj.jobId}</span>
                                  <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{rj.job ? rj.job.client : ''}{rj.job && rj.job.rate ? ' · ' + rj.job.rate : ''}</span>
                                </span>
                                {rj.reasons && rj.reasons.length ? (
                                  <span style={{ display: 'flex', flexWrap: 'wrap', gap: '2px 12px', marginTop: 5 }}>
                                    {rj.reasons.slice(0, 3).map((why, i) => (
                                      <span key={i} className="e8-jobmatch-why"><span className="material-symbols-outlined" style={{ fontSize: 12, color: 'var(--ui-success-text)' }}>check</span>{why}</span>
                                    ))}
                                  </span>
                                ) : null}
                              </span>
                              <span className="material-symbols-outlined" style={{ fontSize: 18, color: 'var(--ui-text-tertiary)', alignSelf: 'center' }}>chevron_right</span>
                            </button>
                          ))}
                        </div>
                      </div>
                    ) : null}

                    {/* Care timeline - one merged activity feed incl. this consultant's own notes */}
                    <div>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                        <span className="e8-sectionlabel">Care & history</span>
                        <DSr.Button variant="ghost" size="sm" icon="note_add" style={{ marginLeft: 'auto' }} onClick={openConsultNote}>Add note</DSr.Button>
                      </div>
                      <Timeline events={timeline} />
                    </div>
                  </React.Fragment>
                ) : null}

                {tab === 'time' ? <TimeSheetPanel key={e.cid} e={e} /> : null}

                {tab === 'casestudy' ? (
                  <div>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                      <span className="e8-sectionlabel">Case study</span>
                      {caseStudy ? <a className="e8-link" style={{ fontSize: 'var(--ui-text-sm)', marginLeft: 'auto' }} tabIndex={0} onKeyDown={window.keyActivate} onClick={() => navigate('casestudies')}>All case studies →</a> : null}
                    </div>
                    {caseStudy && CaseStudyCard
                      ? <CaseStudyCard cs={{ id: e.id, ...caseStudy }} />
                      : (
                        <div className="e8-card e8-intel-card e8-cs-empty">
                          <span className="material-symbols-outlined" style={{ fontSize: 22, color: 'var(--ui-text-tertiary)' }}>auto_stories</span>
                          <div style={{ flex: 1, minWidth: 0 }}>
                            <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 500 }}>No case study yet</div>
                            <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{e.sentiment.tone === 'danger' ? 'Resolve the at-risk signals first, then capture the turnaround.' : 'ELEV8 can draft one from this engagement’s history and results.'}</div>
                          </div>
                          <DSr.Button variant="secondary" size="sm" icon="auto_awesome" onClick={() => showToast('ELEV8 drafted a case study for ' + e.consultant + ' - review in Approvals', 'Open approvals', () => navigate('approvals'))}>Draft with AI</DSr.Button>
                        </div>
                      )}
                  </div>
                ) : null}
              </div>
            </div>

            {/* Right rail */}
            <div className="e8-rail">
              <div className="e8-rail-block">
                <div className="e8-rail-title">Assignment</div>
                <div className="e8-kv"><span className="e8-kv-k">Sponsor</span><span className="e8-kv-v">{e.sponsor || '-'}</span></div>
                <div className="e8-kv"><span className="e8-kv-k">Hours/wk</span><span className="e8-kv-v tnum">{e.hoursWk}</span></div>
                {payRule ? <div className="e8-kv"><span className="e8-kv-k">Pay rule</span><span className="e8-kv-v">{payRule.label}</span></div> : null}
                {owner ? (
                  <div className="e8-kv"><span className="e8-kv-k">Owner</span>
                    <span className="e8-kv-v" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, justifyContent: 'flex-end' }}>
                      {owner.name}
                      {ownerInfo.orphaned ? (
                        <span className="e8-cown-orphan" title={'Owner left' + (owner.departedOn ? ' · ' + owner.departedOn : '')}>
                          <span className="material-symbols-outlined" aria-hidden="true">warning</span>Left
                        </span>
                      ) : null}
                    </span>
                  </div>
                ) : null}
                {canReassign ? (
                  <div style={{ marginTop: 8 }}>
                    <DSr.Button variant="secondary" size="sm" icon="swap_horiz" onClick={() => setReassignOpen(true)}>Reassign owner</DSr.Button>
                  </div>
                ) : null}
              </div>
              <div className="e8-rail-block">
                <div className="e8-rail-title">Contract</div>
                <div className="e8-kv"><span className="e8-kv-k">Started</span><span className="e8-kv-v">{e.start}</span></div>
                <div className="e8-kv"><span className="e8-kv-k">Tenure</span><span className="e8-kv-v">{e.tenure}</span></div>
                <div className="e8-kv"><span className="e8-kv-k">Ends</span><span className="e8-kv-v">{e.ends}</span></div>
              </div>
              <div className="e8-rail-block">
                <div className="e8-rail-title">Skills</div>
                <DSr.SkillChips skills={e.skills} max={(e.skills || []).length} style={{ flexWrap: 'wrap' }} />
              </div>
              {client ? (
                <div className="e8-rail-block">
                  <div className="e8-rail-title">CSM</div>
                  <ActorChip name={client.csm} />
                </div>
              ) : null}
            </div>
          </div>
        </div>
      </div>
      {reassignOpen ? <ReassignOwnerDialog cids={[e.cid]} onClose={() => setReassignOpen(false)} /> : null}
    </React.Fragment>
  );
}

/* ============================== RATE LIBRARY ============================== */
/* Rate library - delivery-geo aware (US onshore / Mexico nearshore / India offshore).
   Geo series use the categorical --ui-cat-* tokens, NOT status colors: geography is a
   category, and status colors stay reserved for actual health/risk semantics. */
const RL_GEOS = [
  { id: 'US', label: 'US', c: 'var(--ui-cat-1-text)', bg: 'var(--ui-cat-1-bg)' },
  { id: 'Mexico', label: 'Mexico', c: 'var(--ui-cat-6-text)', bg: 'var(--ui-cat-6-bg)' },
  { id: 'India', label: 'India', c: 'var(--ui-cat-3-text)', bg: 'var(--ui-cat-3-bg)' },
];
const rlMedian = (arr) => { if (!arr.length) return 0; const s = arr.slice().sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : Math.round((s[m - 1] + s[m]) / 2); };
/* YoY: latest quarter vs the same quarter a year earlier (4 back). */
const rlYoy = (hist) => { if (!hist || hist.length < 5) return 0; const prev = hist[hist.length - 5]; return prev ? Math.round(((hist[hist.length - 1] - prev) / prev) * 100) : 0; };

function RlGeoChip({ geo }) {
  const g = RL_GEOS.find((x) => x.id === geo) || RL_GEOS[0];
  return <span className="e8-rl-geochip" style={{ color: g.c, background: g.bg }}>{g.label}</span>;
}

/* The living layer: observed bill rates from the ACTUAL book - active engagements whose role
   matches the card's family (US delivery today). Re-derives on store changes, so the library
   reads as a living view of what we really bill, not a static list. */
const RL_FAMILY = {
  'Senior Java Developer': /java/i,
  'Cloud Security Engineer': /security/i,
  'Data Engineer': /data engineer/i,
  'Data Analyst': /data analyst/i,
  'ServiceNow Developer': /servicenow dev/i,
  'ServiceNow Admin': /servicenow admin/i,
  'Salesforce Administrator': /salesforce admin/i,
  'QA Automation Engineer': /qa/i,
  'Scrum Master': /scrum/i,
};
function rlObserved(role) {
  const rx = RL_FAMILY[role];
  if (!rx) return null;
  const bills = ((window.E8DATA && window.E8DATA.engagements) || []).filter((e) => rx.test(e.role || '') && e.bill).map((e) => e.bill);
  if (!bills.length) return null;
  return { n: bills.length, min: Math.min.apply(null, bills), max: Math.max.apply(null, bills), med: rlMedian(bills) };
}

/* Where the card sits inside the market band: p25-p75 range, median tick, card-rate dot,
   and the delta vs median. Under p25 = leaving money on the table; over p75 = premium. */
function RlBandBar({ market, bill }) {
  if (!market) return <span style={{ color: 'var(--ui-text-tertiary)' }}>—</span>;
  const lo = Math.min(market.p25, bill) * 0.92, hi = Math.max(market.p75, bill) * 1.06;
  const pct = (v) => Math.max(0, Math.min(100, Math.round(((v - lo) / (hi - lo || 1)) * 100)));
  const delta = Math.round(((bill - market.p50) / market.p50) * 100);
  const under = bill < market.p25, over = bill > market.p75;
  const tone = under ? 'var(--ui-warning-text)' : over ? 'var(--ui-accent)' : 'var(--ui-success-text)';
  return (
    <span className="e8-rl-band" title={'Market $' + market.p25 + '–$' + market.p75 + '/hr · median $' + market.p50 + ' · this card ' + (delta > 0 ? '+' : '') + delta + '% vs median'}>
      <span className="e8-rl-band-track">
        <i className="e8-rl-band-range" style={{ left: pct(market.p25) + '%', width: Math.max(4, pct(market.p75) - pct(market.p25)) + '%' }} />
        <i className="e8-rl-band-mid" style={{ left: pct(market.p50) + '%' }} />
        <i className="e8-rl-band-dot" style={{ left: pct(bill) + '%', background: under ? 'var(--ui-warning)' : over ? 'var(--ui-accent)' : 'var(--ui-success)' }} />
      </span>
      <span className="tnum e8-rl-band-delta" style={{ color: tone }}>{delta > 0 ? '+' : ''}{delta}%</span>
    </span>
  );
}

/* Median bill by quarter, one line per geo. Lines only in the SVG (labels live in HTML so
   nothing stretches); the legend carries current values + YoY. Dims non-active geos. */
function RlTrendChart({ histByGeo, quarters, activeGeo }) {
  const W = 600, H = 150, pad = 8;
  const all = RL_GEOS.flatMap((g) => histByGeo[g.id] || []).filter((v) => v > 0);
  if (!all.length) return null;
  const max = Math.max.apply(null, all), min = Math.min.apply(null, all);
  const rng = (max - min) || 1;
  const x = (i) => pad + (i / (quarters.length - 1)) * (W - pad * 2);
  const y = (v) => H - pad - ((v - min) / rng) * (H - pad * 2);
  return (
    <React.Fragment>
      <svg viewBox={'0 0 ' + W + ' ' + H} preserveAspectRatio="none" style={{ display: 'block', width: '100%', height: 150 }} aria-hidden="true">
        {[0.25, 0.5, 0.75].map((t) => <line key={t} x1={pad} x2={W - pad} y1={pad + t * (H - pad * 2)} y2={pad + t * (H - pad * 2)} stroke="var(--ui-border)" strokeWidth="1" strokeDasharray="3 5" />)}
        {RL_GEOS.map((g) => {
          const data = histByGeo[g.id];
          if (!data || !data.some((v) => v > 0)) return null;
          const dim = activeGeo !== 'all' && activeGeo !== g.id;
          const pts = data.map((v, i) => x(i).toFixed(1) + ',' + y(v).toFixed(1)).join(' ');
          return (
            <g key={g.id} style={{ opacity: dim ? 0.22 : 1, transition: 'opacity .18s ease' }}>
              <polyline points={pts} fill="none" stroke={g.c} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
              <circle cx={x(data.length - 1)} cy={y(data[data.length - 1])} r="3.5" fill={g.c} />
            </g>
          );
        })}
      </svg>
      <div className="e8-rl-qlabels" aria-hidden="true">{quarters.map((qq, i) => <span key={qq} className={i % 2 ? 'e8-rl-q-alt' : ''}>{qq}</span>)}</div>
      <div className="e8-rl-legend">
        {RL_GEOS.map((g) => {
          const data = histByGeo[g.id] || [];
          const cur = data[data.length - 1] || 0;
          const yoy = data.length > 4 && data[data.length - 5] ? Math.round(((cur - data[data.length - 5]) / data[data.length - 5]) * 100) : 0;
          const dim = activeGeo !== 'all' && activeGeo !== g.id;
          return (
            <span key={g.id} className="e8-rl-leg" style={{ opacity: dim ? 0.4 : 1 }}>
              <i style={{ background: g.c }} /> {g.label} <b className="tnum">${cur}</b>
              <span className="tnum" style={{ color: yoy > 0 ? 'var(--ui-success-text)' : 'var(--ui-text-tertiary)' }}>{yoy > 0 ? '+' : ''}{yoy}% YoY</span>
            </span>
          );
        })}
      </div>
    </React.Fragment>
  );
}

function RateLibraryScreen() {
  const D = window.E8DATA;
  const { showToast, density } = React.useContext(window.E8Ctx);
  const [q, setQ] = React.useState('');
  const [geo, setGeo] = React.useState('all');
  /* Living library: observed book rates re-derive when engagements change. */
  const [, setTick] = React.useState(0);
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['store:changed'], () => setTick((t) => t + 1)) : undefined), []);
  const margin = (r) => Math.round(((r.bill - r.pay) / r.bill) * 100);
  const ql = q.trim().toLowerCase();
  /* search narrows everything; the geo tab narrows tiles + table only - the trend chart and
     benchmark keep all three geos visible (dimmed) so the comparison never disappears. */
  const searched = D.rates.filter((r) => !ql || [r.role, r.skill, r.client, r.region, r.geo].join(' ').toLowerCase().includes(ql));
  const rows = searched.filter((r) => geo === 'all' || r.geo === geo).map((r, i) => ({ ...r, id: 'rate-' + r.geo + '-' + i }));
  const geoCount = (g) => searched.filter((r) => r.geo === g).length;
  const medians = {};
  RL_GEOS.forEach((g) => { medians[g.id] = rlMedian(searched.filter((r) => r.geo === g.id).map((r) => r.bill)); });
  const histByGeo = {};
  RL_GEOS.forEach((g) => {
    const set = searched.filter((r) => r.geo === g.id && r.hist);
    histByGeo[g.id] = (D.rateQuarters || []).map((_, i) => rlMedian(set.map((r) => r.hist[i])));
  });
  const avgMargin = rows.length ? Math.round(rows.reduce((n, r) => n + margin(r), 0) / rows.length) : 0;
  const usMed = medians.US || 0;
  const deltaVs = (g) => (usMed && medians[g]) ? Math.round(((medians[g] - usMed) / usMed) * 100) : 0;
  return (
    <React.Fragment>
      <Topbar crumbs={[{ label: 'Rate library' }]} actions={<React.Fragment>
        <DSr.Button variant="secondary" size="sm" icon="upload">Export</DSr.Button>
        <DSr.Button variant="primary" size="sm" icon="add">New rate</DSr.Button>
      </React.Fragment>} />
      <div className="e8-content">
        <div className="e8-page">
          {/* Search + geo filter */}
          <div className="e8-rl-filters">
            <span className="e8-rl-search"><DSr.Input icon="search" value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search role, skill, client, market…" /></span>
            <DSr.SubTabs
              items={[{ id: 'all', label: 'All geos', count: searched.length }].concat(RL_GEOS.map((g) => ({ id: g.id, label: g.label, count: geoCount(g.id) })))}
              active={geo}
              onChange={setGeo}
            />
          </div>

          {/* Vital signs - react to the current filter */}
          <div className="e8-rl-tiles">
            <window.StatTile icon="payments" value={String(rows.length)} label="Rate cards" tone="neutral" />
            <window.StatTile icon="percent" value={avgMargin + '%'} label="Avg margin" tone={avgMargin >= 28 ? 'success' : 'warning'} />
            <window.StatTile icon="south_america" value={medians.Mexico ? deltaVs('Mexico') + '%' : '—'} label="Mexico vs US bill" tone="accent" />
            <window.StatTile icon="public" value={medians.India ? deltaVs('India') + '%' : '—'} label="India vs US bill" tone="accent" />
          </div>

          {/* Trends + geo benchmark */}
          <div className="e8-eng-2col" style={{ marginBottom: 14 }}>
            <div className="e8-card" style={{ padding: '14px 16px', flex: 1.5 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
                <span className="e8-sectionlabel">Median bill rate · 8 quarters</span>
                {ql ? <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>filtered: “{q.trim()}”</span> : null}
                <span style={{ marginLeft: 'auto', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap' }} title="Market bands calibrated from published rate research - see docs/RATES-SOURCES.md">market bands · {D.ratesAsOf}</span>
              </div>
              <RlTrendChart histByGeo={histByGeo} quarters={D.rateQuarters || []} activeGeo={geo} />
            </div>
            <div className="e8-card" style={{ padding: '14px 16px' }}>
              <div className="e8-sectionlabel" style={{ marginBottom: 10 }}>Geo benchmark · median bill</div>
              {RL_GEOS.map((g) => {
                const med = medians[g.id];
                const dim = geo !== 'all' && geo !== g.id;
                return (
                  <div key={g.id} className="e8-rl-bench-row" style={{ opacity: dim ? 0.45 : 1 }}>
                    <RlGeoChip geo={g.id} />
                    <span className="e8-rl-bench-bar"><i style={{ width: usMed ? Math.max(6, Math.round(((med || 0) / Math.max(usMed, med || 1)) * 100)) + '%' : '0%', background: g.c }} /></span>
                    <span className="e8-rl-bench-val tnum">{med ? '$' + med + '/hr' : '—'}</span>
                    <span className="e8-rl-bench-delta tnum">{g.id === 'US' ? 'anchor' : (med && usMed ? deltaVs(g.id) + '% vs US' : '')}</span>
                  </div>
                );
              })}
              <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 8, lineHeight: 1.5 }}>
                Blend example: 1 US lead + 2 nearshore ≈ <b className="tnum">${usMed && medians.Mexico ? Math.round((usMed + 2 * medians.Mexico) / 3) : '—'}/hr</b> blended vs ${usMed || '—'} all-onshore.
              </div>
            </div>
          </div>

          {/* Rate cards */}
          <window.ConfigurableTable
            listKey="rates"
            density={density}
            registry={[
              { key: 'role', label: 'Role', locked: true, sortable: true, sortValue: (r) => r.role, render: (r) => (
                <span style={{ display: 'block', minWidth: 0 }}>
                  <span style={{ fontWeight: 500 }}>{r.role}</span>
                  <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 1 }}>{r.skill} · {r.type}</span>
                </span>
              ) },
              { key: 'geo', label: 'Geo', sortable: true, sortValue: (r) => r.geo, render: (r) => (
                <span style={{ display: 'inline-flex', flexDirection: 'column', gap: 2, alignItems: 'flex-start' }}>
                  <RlGeoChip geo={r.geo} />
                  <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{r.region}</span>
                </span>
              ) },
              { key: 'client', label: 'Client', sortable: true, sortValue: (r) => r.client, render: (r) => r.client === 'Market' ? <span style={{ color: 'var(--ui-text-tertiary)' }}>Market</span> : r.client },
              { key: 'bill', label: 'Bill', num: true, sortable: true, sortValue: (r) => r.bill, render: (r) => {
                const ob = r.geo === 'US' ? rlObserved(r.role) : null;
                return (
                  <span style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'flex-end', gap: 1 }}>
                    <span className="tnum" style={{ fontWeight: 600 }}>${r.bill}/hr</span>
                    {ob ? <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap' }} title={'Observed on ' + ob.n + ' active placement' + (ob.n > 1 ? 's' : '') + ' in the book'}>book {ob.n > 1 ? '$' + ob.min + '–' + ob.max : '$' + ob.med} · n{ob.n}</span> : null}
                  </span>
                );
              } },
              { key: 'pay', label: 'Pay', num: true, sortable: true, sortValue: (r) => r.pay, render: (r) => <span className="tnum">${r.pay}/hr</span> },
              { key: 'margin', label: 'Margin', num: true, sortable: true, sortValue: (r) => margin(r), render: (r) => {
                const m = margin(r);
                return <DSr.Badge tone={m >= 28 ? 'success' : m >= 22 ? 'warning' : 'danger'}>{m}%</DSr.Badge>;
              } },
              { key: 'market', label: 'vs market', sortable: true, sortValue: (r) => r.market ? (r.bill - r.market.p50) / r.market.p50 : -99, render: (r) => <RlBandBar market={r.market} bill={r.bill} /> },
              { key: 'trend', label: 'Trend · YoY', render: (r) => {
                const yy = rlYoy(r.hist);
                return (
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                    {window.E8Charts ? <window.E8Charts.Sparkline data={r.hist} w={72} h={22} tone={yy > 1 ? 'up' : 'flat'} /> : null}
                    <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', fontWeight: 600, color: yy > 1 ? 'var(--ui-success-text)' : 'var(--ui-text-tertiary)' }}>{yy > 0 ? '+' : ''}{yy}%</span>
                  </span>
                );
              } },
              { key: 'updated', label: 'Updated', render: (r) => <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap' }}>{r.updated}</span> },
            ]}
            mobileCard={(r) => {
              const m = margin(r);
              const yy = rlYoy(r.hist);
              const ob = r.geo === 'US' ? rlObserved(r.role) : null;
              const mk = r.market;
              const delta = mk ? Math.round(((r.bill - mk.p50) / mk.p50) * 100) : null;
              return (
                <div className="e8-rl-mcard">
                  <div className="e8-rl-mcard-top">
                    <span className="e8-rl-mcard-role">{r.role}</span>
                    <RlGeoChip geo={r.geo} />
                  </div>
                  <div className="e8-rl-mcard-meta">{r.skill} · {r.type} · {r.region}{r.client !== 'Market' ? ' · ' + r.client : ''}</div>
                  <div className="e8-rl-mcard-rates">
                    <span className="e8-rl-mcard-bill tnum">${r.bill}<span className="e8-rl-mcard-unit">/hr</span></span>
                    <span className="e8-rl-mcard-pay tnum">${r.pay}/hr pay</span>
                    <DSr.Badge tone={m >= 28 ? 'success' : m >= 22 ? 'warning' : 'danger'}>{m}%</DSr.Badge>
                    <span style={{ flex: 1 }} />
                    {window.E8Charts ? <window.E8Charts.Sparkline data={r.hist} w={56} h={18} tone={yy > 1 ? 'up' : 'flat'} /> : null}
                    <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', fontWeight: 600, color: yy > 1 ? 'var(--ui-success-text)' : 'var(--ui-text-tertiary)' }}>{yy > 0 ? '+' : ''}{yy}%</span>
                  </div>
                  {mk ? (
                    <div className="e8-rl-mcard-mkt">
                      Market ${mk.p25}–${mk.p75} · <b className="tnum" style={{ color: r.bill < mk.p25 ? 'var(--ui-warning-text)' : r.bill > mk.p75 ? 'var(--ui-accent)' : 'var(--ui-success-text)' }}>{delta > 0 ? '+' : ''}{delta}% vs median</b>
                      {ob ? <span> · book {ob.n > 1 ? '$' + ob.min + '–' + ob.max : '$' + ob.med} (n{ob.n})</span> : null}
                    </div>
                  ) : null}
                  <div className="e8-rl-mcard-foot">
                    <span>Updated {r.updated}</span>
                    <span>{r.client === 'Market' ? 'Market benchmark' : 'Client rate · ' + r.client}</span>
                  </div>
                </div>
              );
            }}
            rows={rows}
            empty={<div style={{ maxWidth: 420, margin: '32px auto' }}><DSr.EmptyState icon="payments" title="No rates match" body="Try a different search, or clear the geo filter." action={<DSr.Button variant="secondary" size="sm" onClick={() => { setQ(''); setGeo('all'); }}>Clear filters</DSr.Button>} /></div>}
            rowActions={(r) => <DSr.Button variant="ghost" size="sm" icon="content_copy" onClick={() => showToast('Rate applied to a new job')}>Use</DSr.Button>}
          />
        </div>
      </div>
    </React.Fragment>
  );
}

/* ============================== AUDIT LOG ============================== */
/* R78: the log is live - E8Audit entries (queue decisions, agent runs, on-device AI calls)
   merge ahead of the seeded history in the same row shape the screen always rendered. */
function liveAuditRows() {
  const fmt = (iso) => {
    try {
      const d = new Date(iso);
      const t = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }).toLowerCase();
      return (d.toDateString() === new Date().toDateString() ? 'Today · ' : d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' · ') + t;
    } catch (err) { return ''; }
  };
  return (window.E8Audit ? window.E8Audit.list() : []).map((e) => ({
    id: e.id, when: fmt(e.at), actor: e.agent, ai: e.prov !== 'human', prov: e.prov, action: e.action, target: e.target || '', targetTo: e.route,
  }));
}

function AuditLogScreen() {
  const D = window.E8DATA;
  const { showToast } = React.useContext(window.E8Ctx);
  const [filter, setFilter] = React.useState('all');
  const [agentQ, setAgentQ] = React.useState(() => (parseHash().query || {}).agent || null);
  React.useEffect(() => {
    const fn = () => setAgentQ((parseHash().query || {}).agent || null);
    window.addEventListener('hashchange', fn);
    return () => window.removeEventListener('hashchange', fn);
  }, []);
  const [live, setLive] = React.useState(liveAuditRows);
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['audit:logged'], () => setLive(liveAuditRows())) : undefined), []);
  const all = live.concat(D.audit);
  const rows = all.filter((a) => (filter === 'all' || (filter === 'ai' ? a.ai : !a.ai)) && (!agentQ || a.actor === agentQ));
  return (
    <React.Fragment>
      <Topbar crumbs={[{ label: 'Audit log' }]} actions={<DSr.Button variant="secondary" size="sm" icon="upload" onClick={() => showToast('Audit log exported')}>Export</DSr.Button>} />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 900 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '0 0 14px', flexWrap: 'wrap' }}>
            <DSr.SubTabs
              items={[{ id: 'all', label: 'All', count: all.length }, { id: 'ai', label: 'AI actions', count: all.filter((a) => a.ai).length }, { id: 'human', label: 'People', count: all.filter((a) => !a.ai).length }]}
              active={filter}
              onChange={setFilter}
            />
            {agentQ ? <button type="button" className="e8-whybtn" onClick={() => navigate('audit')}>{agentQ} runs · show all</button> : null}
            <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>Every consequential action - human or agent - is logged with provenance and is reversible.</span>
          </div>
          <div className="e8-card" style={{ boxShadow: 'none', padding: '2px 0' }}>
            {rows.map((a) => (
              <div key={a.id} className="e8-audit-row">
                <ActorChip name={a.actor} ai={a.ai} />
                <DSr.ProvenanceBadge kind={a.prov} />
                <span style={{ flex: 1, minWidth: 0 }}>
                  <span style={{ fontSize: 'var(--ui-text-base)', color: 'var(--ui-text)' }}>{a.action}</span>
                  {' '}
                  {a.targetTo
                    ? <a className="e8-link" style={{ fontSize: 'var(--ui-text-base)' }} tabIndex={0} onKeyDown={window.keyActivate} onClick={() => navigate(a.targetTo)}>{a.target}</a>
                    : <span style={{ fontSize: 'var(--ui-text-base)', color: 'var(--ui-text-secondary)' }}>{a.target}</span>}
                </span>
                <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', flex: 'none', whiteSpace: 'nowrap' }}>{a.when}</span>
              </div>
            ))}
            {!rows.length ? <div style={{ padding: '14px 16px', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>No matching entries yet.</div> : null}
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

/* ============================== CSMs ============================== */
/* Book of business for a CSM, derived live from the clients they own. */
function csmBook(name) {
  const D = window.E8DATA;
  const book = D.clients.filter((c) => c.csm === name);
  return {
    book,
    clients: book.length,
    jobs: book.reduce((n, c) => n + c.openJobs, 0),
    consultants: book.reduce((n, c) => n + c.consultants, 0),
    renewals: book.reduce((n, c) => n + c.renewals, 0),
    atRisk: book.filter((c) => c.health.tone === 'danger' || c.health.tone === 'warning').length,
  };
}

function CsmCard({ csm, onOpen }) {
  const C = window.E8Charts;
  const b = csmBook(csm.name);
  return (
    <button type="button" className="e8-csm-card" onClick={onOpen}>
      <div className="e8-csm-cardhead">
        <DSr.Avatar name={csm.name} size="lg" />
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ fontSize: 'var(--ui-text-md)', fontWeight: 600 }}>{csm.name}</div>
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{csm.title} · {csm.region}</div>
        </div>
        {b.atRisk ? <DSr.Badge tone="warning" dot>{b.atRisk} to watch</DSr.Badge> : <DSr.Badge tone="success" dot>Healthy</DSr.Badge>}
      </div>

      <div className="e8-csm-gm">
        <div>
          <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>GM owned</div>
          <div className="tnum" style={{ fontSize: 'var(--ui-text-2xl)', fontWeight: 600 }}>{csm.gmOwned}</div>
        </div>
        <span style={{ flex: '0 0 96px' }}>{C ? <C.Sparkline data={csm.gmTrend} tone="up" w={96} h={30} /> : null}</span>
      </div>

      <div className="e8-csm-stats">
        {[['Clients', b.clients], ['On billing', b.consultants], ['Renewals 90d', b.renewals || '-']].map(([k, v]) => (
          <div key={k}><div className="tnum" style={{ fontSize: 'var(--ui-text-lg)', fontWeight: 600 }}>{v}</div><div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{k}</div></div>
        ))}
      </div>

      <div className="e8-csm-meters">
        <div className="e8-csm-meter"><span className="e8-csm-meter-k">Renewal rate</span><span className="e8-csm-meter-v"><span style={{ width: 56, display: 'inline-flex' }}><DSr.Progress value={Math.round(csm.renewalRate * 100)} /></span><b className="tnum">{Math.round(csm.renewalRate * 100)}%</b></span></div>
        <div className="e8-csm-meter"><span className="e8-csm-meter-k">CSAT</span><span className="e8-csm-meter-v tnum"><span className="material-symbols-outlined" style={{ fontSize: 13, color: 'var(--ui-warning)' /* base token on purpose: icon fill, not text ink */ }}>star</span><b>{csm.csat}</b><span style={{ color: 'var(--ui-text-tertiary)' }}>/5</span></span></div>
        <div className="e8-csm-meter"><span className="e8-csm-meter-k">Capacity</span><span className="e8-csm-meter-v"><span style={{ width: 56, display: 'inline-flex' }}><DSr.Progress value={csm.capacity} soft={csm.capacity < 80} /></span><b className="tnum">{csm.capacity}%</b></span></div>
      </div>

      <div className="e8-csm-focus">{csm.focus.map((f) => <span key={f} className="e8-techchip">{f}</span>)}</div>
    </button>
  );
}

function CsmsScreen() {
  const D = window.E8DATA;
  const { showToast } = React.useContext(window.E8Ctx);
  const csms = D.csms;
  const totalGm = csms.reduce((n, c) => n + parseFloat(String(c.gmOwned).replace(/[^0-9.]/g, '')), 0);
  const avgRenewal = csms.length ? Math.round((csms.reduce((n, c) => n + c.renewalRate, 0) / csms.length) * 100) : 0;
  const avgCsat = csms.length ? (csms.reduce((n, c) => n + c.csat, 0) / csms.length).toFixed(1) : '0.0';
  const totalAtRisk = csms.reduce((n, c) => n + csmBook(c.name).atRisk, 0);

  return (
    <React.Fragment>
      <Topbar crumbs={[{ label: 'CSMs' }]} actions={
        <React.Fragment>
          <DSr.Button variant="secondary" size="sm" icon="upload">Export</DSr.Button>
          <DSr.Button variant="primary" size="sm" icon="person_add">Add CSM</DSr.Button>
        </React.Fragment>
      } />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 1000 }}>
          <div className="e8-pagehead">
            <div>
              <h1 className="e8-h1">Client success managers</h1>
              <div className="e8-pagehead-sub">Who owns each relationship and how their book is performing</div>
            </div>
          </div>

          <div className="e8-kpi-row">
            <KpiCard label="GM under management" value={'$' + totalGm.toFixed(1) + 'M'} delta="+8% YoY" dir="up" spark={[6.4, 6.8, 7.1, 7.4, 7.7, totalGm]} />
            <KpiCard label="Avg renewal rate" value={avgRenewal + '%'} delta="+3 pts QoQ" dir="up" spark={[83, 84, 85, 86, 87, avgRenewal]} />
            <KpiCard label="Avg CSAT" value={avgCsat} delta="steady" dir="flat" spark={[4.4, 4.5, 4.5, 4.6, 4.6, parseFloat(avgCsat)]} />
            <KpiCard label="Accounts to watch" value={String(totalAtRisk)} delta="across the team" dir={totalAtRisk > 2 ? 'down' : 'flat'} spark={[2, 3, 3, 2, 3, totalAtRisk]} />
          </div>

          <div className="e8-csm-grid">
            {csms.map((csm) => <CsmCard key={csm.id} csm={csm} onOpen={() => navigate('csm/' + csm.id)} />)}
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

/* ---------- CSM workspace ---------- */
function CsmDetailScreen({ id }) {
  const D = window.E8DATA;
  const C = window.E8Charts;
  const { showToast, density } = React.useContext(window.E8Ctx);
  const isMobile = useIsMobile();
  const csm = D.csms.find((x) => x.id === id);
  if (!csm) {
    return (
      <React.Fragment>
        <Topbar crumbs={[{ label: 'Client success', to: 'csms' }, { label: 'Not found' }]} />
        <div className="e8-content"><div className="e8-page"><div style={{ maxWidth: 460, margin: '48px auto' }}>
          <DSr.EmptyState icon="person_off" title="CSM not found" body="This client-success manager does not exist, or the link is out of date." cta={<DSr.Button variant="primary" icon="groups" onClick={() => navigate('csms')}>Back to client success</DSr.Button>} />
        </div></div></div>
      </React.Fragment>
    );
  }
  const b = csmBook(csm.name);
  const renewalsOwned = D.engagements.filter((e) => {
    const cl = D.clients.find((c) => c.name === e.client);
    return cl && cl.csm === csm.name && e.renewal !== 'Not due';
  });
  const watchList = b.book.filter((c) => c.health.tone === 'danger' || c.health.tone === 'warning');

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'CSMs', to: 'csms' }, { label: csm.name }]}
        actions={
          <React.Fragment>
            <DSr.Button variant="ghost" size="sm" icon="forum" onClick={() => showToast('1:1 agenda drafted for ' + csm.name)}>Draft 1:1</DSr.Button>
            <DSr.Button variant="primary" size="sm" icon="apartment" onClick={() => navigate('clients')}>View book in Clients</DSr.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page">
          <RecordHeader
            mark={<DSr.Avatar name={csm.name} size="xl" />}
            title={csm.name}
            badges={<React.Fragment>
              <DSr.Badge tone="neutral">{csm.title}</DSr.Badge>
              {b.atRisk ? <DSr.Badge tone="warning" dot>{b.atRisk} to watch</DSr.Badge> : <DSr.Badge tone="success" dot>Book healthy</DSr.Badge>}
            </React.Fragment>}
            meta={<React.Fragment>{csm.region} · {csm.tenure} at Stand8 · {csm.wins}</React.Fragment>}
          />

          <div className="e8-record-2col">
            <div style={{ flex: 1, minWidth: 0, display: 'grid', gap: 18 }}>
              {/* Performance */}
              <div className="e8-card e8-intel-card">
                <div className="e8-intel-cardhead">
                  <span className="e8-sectionlabel">Gross margin under management</span>
                  <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{csm.gmOwned} · trailing 6 months</span>
                </div>
                {C ? <C.AreaTrend series={csm.gmTrend} labels={['', '', '', '', '', 'now']} color="var(--ui-accent)" height={120} suffix="M" /> : null}
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginTop: 12 }}>
                  {[['Renewal rate', Math.round(csm.renewalRate * 100) + '%'], ['CSAT', csm.csat + '/5'], ['NPS', String(csm.nps)], ['Avg response', csm.avgResponse]].map(([k, v]) => (
                    <div key={k} style={{ borderLeft: '1px solid var(--ui-border)', paddingLeft: 10 }}>
                      <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{k}</div>
                      <div className="tnum" style={{ fontSize: 'var(--ui-text-lg)', fontWeight: 600, marginTop: 2 }}>{v}</div>
                    </div>
                  ))}
                </div>
              </div>

              {/* Accounts to watch */}
              {watchList.length ? (
                <div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                    <span className="e8-sectionlabel">Accounts to watch</span>
                    <DSr.ProvenanceBadge kind="advised" />
                  </div>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    {watchList.map((c) => (
                      <button key={c.id} type="button" className="e8-nextaction" onClick={() => navigate('client/' + c.id)}>
                        <span className="material-symbols-outlined" style={{ fontSize: 14 }}>apartment</span>
                        {c.name} - {c.health.note}
                      </button>
                    ))}
                  </div>
                </div>
              ) : null}

              {/* Book */}
              <div>
                <div className="e8-sectionlabel" style={{ marginBottom: 8 }}>Book of business · {b.clients} clients</div>
                <ConfigurableTable listKey="csm-client-book" primary={false}
                  density={density}
                  onRowClick={(c) => navigate('client/' + c.id)}
                  registry={[
                    { key: 'name', label: 'Client', render: (c) => <NameCell name={c.name} sub={c.industry + ' · ' + c.tier} /> },
                    { key: 'health', label: 'Health', render: (c) => <span title={c.health.note}><DSr.Badge tone={c.health.tone} dot={c.health.tone !== 'neutral'}>{c.health.label}</DSr.Badge></span> },
                    { key: 'openJobs', label: 'Open jobs', num: true },
                    { key: 'consultants', label: 'On billing', num: true },
                    { key: 'renewals', label: 'Renewals 90d', num: true, render: (c) => c.renewals || <span style={{ color: 'var(--ui-text-tertiary)' }}>-</span> },
                  ]}
                  rows={b.book}
                />
              </div>

              {/* Renewals owned */}
              {renewalsOwned.length ? (
                <div>
                  <div className="e8-sectionlabel" style={{ marginBottom: 8 }}>Renewals in flight · {renewalsOwned.length}</div>
                  {isMobile ? (
                    <div className="e8-cards">{renewalsOwned.map((e) => <EngagementRenewalCard key={e.cid} e={e} />)}</div>
                  ) : (
                  <ConfigurableTable listKey="csm-renewals" primary={false}
                    density={density}
                    onRowClick={(e) => navigate('consultant/' + e.cid)}
                    registry={[
                      { key: 'consultant', label: 'Consultant', render: (e) => <NameCell name={e.consultant} sub={e.role + ' · ' + e.client} /> },
                      { key: 'value', label: 'Value', num: true, render: (e) => <span className="tnum">{e.renewalValue}</span> },
                      { key: 'stage', label: 'Stage', width: 200, render: (e) => <DSr.StageCell stages={D.renewalStages} current={e.renewalStage || 0} /> },
                      { key: 'status', label: 'Status', render: (e) => <DSr.Badge tone={e.escalated ? 'danger' : 'accent'} dot>{e.escalated ? 'Escalated' : e.renewal}</DSr.Badge> },
                    ]}
                    rows={renewalsOwned}
                  />
                  )}
                </div>
              ) : null}
            </div>

            {/* Right rail */}
            <div className="e8-rail">
              <div className="e8-rail-block">
                <div className="e8-rail-title">Snapshot</div>
                <div className="e8-kv"><span className="e8-kv-k">Region</span><span className="e8-kv-v">{csm.region}</span></div>
                <div className="e8-kv"><span className="e8-kv-k">Tenure</span><span className="e8-kv-v">{csm.tenure}</span></div>
                <div className="e8-kv"><span className="e8-kv-k">GM owned</span><span className="e8-kv-v tnum">{csm.gmOwned}</span></div>
                <div className="e8-kv"><span className="e8-kv-k">Wins</span><span className="e8-kv-v">{csm.wins}</span></div>
                <div style={{ marginTop: 9 }}>
                  <div className="e8-csm-meter" style={{ marginBottom: 6 }}><span className="e8-csm-meter-k">Capacity</span><span className="e8-csm-meter-v"><span style={{ width: 56, display: 'inline-flex' }}><DSr.Progress value={csm.capacity} soft={csm.capacity < 80} /></span><b className="tnum">{csm.capacity}%</b></span></div>
                </div>
              </div>
              <div className="e8-rail-block">
                <div className="e8-rail-title">Industry focus</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{csm.focus.map((f) => <span key={f} className="e8-techchip">{f}</span>)}</div>
              </div>
              <div className="e8-rail-block">
                <div className="e8-rail-title">About</div>
                <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', lineHeight: 1.5 }}>{csm.bio}</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

/* ============================== SYNC QUEUE ============================== */
const SYNC_T = { Synced: 'success', Pending: 'info', Error: 'danger' };
function SyncQueueScreen() {
  const D = window.E8DATA;
  const { showToast, density } = React.useContext(window.E8Ctx);
  const counts = { Synced: 0, Pending: 0, Error: 0 };
  D.syncQueue.forEach((s) => { counts[s.status] += 1; });
  return (
    <React.Fragment>
      <Topbar crumbs={[{ label: 'Sync queue' }]} actions={<DSr.Button variant="secondary" size="sm" icon="sync" onClick={() => showToast('Forcing a full re-sync with Bullhorn…')}>Re-sync all</DSr.Button>} />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 900 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, margin: '0 0 14px', flexWrap: 'wrap' }}>
            <DSr.Badge tone="success" dot>{counts.Synced} synced</DSr.Badge>
            <DSr.Badge tone="info" dot>{counts.Pending} pending</DSr.Badge>
            <DSr.Badge tone="danger" dot>{counts.Error} error</DSr.Badge>
            <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', marginLeft: 'auto' }}>Two-way sync with Bullhorn · last full sync 24 min ago</span>
          </div>
          <ConfigurableTable listKey="sync-queue"
            density={density}
            registry={[
              { key: 'record', label: 'Record', render: (s) => (
                <span title={s.note}>
                  <span style={{ fontWeight: 500 }}>{s.record}</span>
                  {s.note ? <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-danger-text)' }}>{s.note}</span> : null}
                </span>
              ) },
              { key: 'type', label: 'Type', secondary: true },
              { key: 'system', label: 'System' },
              { key: 'status', label: 'Status', render: (s) => <DSr.Badge tone={SYNC_T[s.status]} dot>{s.status}</DSr.Badge> },
              { key: 'when', label: 'When', secondary: true, render: (s) => <span className="tnum">{s.when}</span> },
            ]}
            rows={D.syncQueue}
            rowActions={(s) => s.status === 'Error'
              ? <DSr.Button variant="secondary" size="sm" icon="refresh" onClick={() => showToast('Retrying sync for ' + s.record)}>Retry</DSr.Button>
              : s.status === 'Pending'
                ? <DSr.Button variant="ghost" size="sm" icon="bolt" onClick={() => showToast('Pushed ' + s.record + ' now')}>Sync now</DSr.Button>
                : <DSr.Button variant="ghost" size="sm" icon="history" onClick={() => showToast('Sync history opens here')}>History</DSr.Button>}
          />
        </div>
      </div>
    </React.Fragment>
  );
}

/* ============================== COMMISSIONS / SALES ============================== */
function CommissionsScreen() {
  const D = window.E8DATA; const E = window.E8Econ;
  const { showToast } = React.useContext(window.E8Ctx);
  const [, setTick] = React.useState(0);
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['timesheet:decided'], () => setTick((t) => t + 1)) : undefined), []);
  const reps = D.reps || [];
  const summaries = reps.map((r) => E.repSummary(r.id)).sort((a, b) => b.attainment - a.attainment);
  const [selId, setSelId] = React.useState((summaries[0] && summaries[0].rep.id) || null);
  const [proj, setProj] = React.useState(false);
  const sel = summaries.find((s) => s.rep.id === selId) || summaries[0];

  /* Empty-data guard: no reps means no summaries and nothing to select. */
  if (!summaries.length) {
    return (
      <React.Fragment>
        <Topbar crumbs={[{ label: 'Commissions' }]} />
        <div className="e8-content"><div className="e8-page" style={{ maxWidth: 1080 }}>
          <div style={{ maxWidth: 460, margin: '48px auto' }}>
            <DSr.EmptyState icon="payments" title="No salespeople yet" body="Commission rollups appear here once reps own placements. Approved timesheet hours become billings, margin and tiered commission." />
          </div>
        </div></div>
      </React.Fragment>
    );
  }

  const money = (n) => (Math.abs(n) >= 1000 ? '$' + (n / 1000).toFixed(1) + 'K' : '$' + Math.round(n).toLocaleString());
  const pct = (x) => Math.round(x * 100) + '%';
  const bookedH = (s) => s.perCon.reduce((n, c) => n + c.booked, 0);
  const pendH = (s) => s.perCon.reduce((n, c) => n + c.pending, 0);

  const desk = summaries.reduce((a, s) => ({
    billings: a.billings + s.billings, hours: a.hours + bookedH(s), pendHours: a.pendHours + pendH(s),
    marginApproved: a.marginApproved + s.marginApproved, marginPending: a.marginPending + s.marginPending,
    commApproved: a.commApproved + s.commApproved, commProjected: a.commProjected + s.commProjected,
    pendingCount: a.pendingCount + s.perCon.filter((c) => c.pending > 0).length,
  }), { billings: 0, hours: 0, pendHours: 0, marginApproved: 0, marginPending: 0, commApproved: 0, commProjected: 0, pendingCount: 0 });

  const chain = [
    { k: 'Approved hours', v: desk.hours.toLocaleString() + 'h', sub: desk.pendHours ? '+' + desk.pendHours + 'h pending' : 'all in', icon: 'schedule' },
    { k: 'Billings', v: money(desk.billings), sub: 'hours x bill rate', icon: 'receipt_long' },
    { k: 'Gross margin', v: money(desk.marginApproved), sub: desk.marginPending ? '+' + money(desk.marginPending) + ' pending' : 'the spread', icon: 'savings' },
    { k: 'Rep commission', v: money(desk.commApproved), sub: desk.commProjected > desk.commApproved ? '+' + money(desk.commProjected - desk.commApproved) + ' if approved' : 'earned', icon: 'paid' },
  ];

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Commissions' }]}
        actions={
          <React.Fragment>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, marginRight: 4, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>
              Include pending<DSr.Toggle checked={proj} onChange={() => setProj(!proj)} />
            </span>
            <DSr.Button variant="secondary" size="sm" icon="upload" onClick={() => showToast('Commission statement exported (prototype)')}>Export</DSr.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 1080 }}>
          <div className="e8-pagehead">
            <div>
              <h1 className="e8-h1">Commissions</h1>
              <div className="e8-pagehead-sub">Timesheets to billings to gross margin to rep commission · this cycle</div>
            </div>
          </div>

          <div className="e8-comm-chain">
            {chain.map((s, i) => (
              <React.Fragment key={s.k}>
                <div className="e8-comm-step">
                  <span className="material-symbols-outlined" style={{ fontSize: 16, color: 'var(--ui-daybreak-text)' }}>{s.icon}</span>
                  <div className="e8-comm-step-v tnum">{s.v}</div>
                  <div className="e8-comm-step-k">{s.k}</div>
                  <div className="e8-comm-step-sub">{s.sub}</div>
                </div>
                {i < chain.length - 1 ? <span className="material-symbols-outlined e8-comm-arrow">arrow_forward</span> : null}
              </React.Fragment>
            ))}
          </div>
          {desk.pendingCount ? (
            <div className="e8-comm-pendbar">
              <span className="material-symbols-outlined" style={{ fontSize: 16, color: 'var(--ui-warning-text)' }}>pending_actions</span>
              <span style={{ flex: 1, minWidth: 0, fontSize: 'var(--ui-text-sm)' }}><b>{desk.pendingCount}</b> timesheets awaiting approval = <b className="tnum">{money(desk.marginPending)}</b> more margin and <b className="tnum">{money(desk.commProjected - desk.commApproved)}</b> more commission across the team.</span>
              <DSr.Button variant="primary" size="sm" icon="done_all" onClick={() => {
                /* R78: real approvals - write the same e8-ts-* keys TimeHub does, so E8Econ books them. */
                const pending = summaries.flatMap((sm) => sm.perCon.filter((c) => c.pending > 0).map((c) => c.cid));
                const prevs = [];
                const gain = money(desk.commProjected - desk.commApproved);
                pending.forEach((cid) => {
                  let raw = null; try { raw = localStorage.getItem('e8-ts-' + cid); } catch (err) {}
                  prevs.push([cid, raw]);
                  const base = raw ? JSON.parse(raw) : JSON.parse(JSON.stringify(((D.timesheets || {})[cid] || {}).current || {}));
                  base.status = 'approved'; base.approvedOn = (D.tsWeeks || []).slice(-1)[0] || 'This week';
                  try { localStorage.setItem('e8-ts-' + cid, JSON.stringify(base)); } catch (err) {}
                  if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Approved timesheet', target: cid.replace('cons-', ''), route: 'consultant/' + cid });
                });
                if (window.E8Events) window.E8Events.emit('timesheet:decided', { cids: pending, status: 'approved' });
                showToast('Approved ' + pending.length + ' timesheets · +' + gain + ' commission booked', 'Undo', () => {
                  prevs.forEach(([cid, raw]) => { try { raw == null ? localStorage.removeItem('e8-ts-' + cid) : localStorage.setItem('e8-ts-' + cid, raw); } catch (err) {} });
                  if (window.E8Events) window.E8Events.emit('timesheet:decided', { cids: pending, status: 'undone' });
                });
              }}>Approve all</DSr.Button>
            </div>
          ) : null}

          <div className="e8-sectionlabel" style={{ margin: '20px 0 10px' }}>By salesperson · attainment to quota{proj ? ' (incl. pending)' : ''}</div>
          <div className="e8-comm-reps">
            {summaries.map((s) => {
              const att = proj ? s.attainmentProjected : s.attainment;
              const comm = proj ? s.commProjected : s.commApproved;
              return (
                <button key={s.rep.id} type="button" className={'e8-comm-rep' + (s.rep.id === selId ? ' sel' : '')} onClick={() => setSelId(s.rep.id)}>
                  <div className="e8-comm-rep-h">
                    <DSr.Avatar name={s.rep.name} size="sm" />
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 600 }}>{s.rep.name}</div>
                      <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{s.book.length} consultants · {s.perCon.filter((c) => c.pending > 0).length} pending</div>
                    </div>
                    <DSr.Badge tone={att >= 1 ? 'success' : att >= 0.8 ? 'warning' : 'neutral'}>{pct(att)}</DSr.Badge>
                  </div>
                  <div className="e8-comm-bar" title={'Quota ' + money(s.quota)}>
                    <span className="e8-comm-bar-fill" style={{ width: Math.min(100, s.attainment * 100) + '%' }} />
                    {s.attainmentProjected > s.attainment ? <span className="e8-comm-bar-ghost" style={{ left: Math.min(100, s.attainment * 100) + '%', width: Math.max(0, Math.min(100 - Math.min(100, s.attainment * 100), (s.attainmentProjected - s.attainment) * 100)) + '%' }} /> : null}
                    <span className="e8-comm-bar-quota" style={{ left: '100%' }} />
                  </div>
                  <div className="e8-comm-rep-f">
                    <span><span className="e8-comm-rep-k">Margin</span><b className="tnum">{money(proj ? s.marginProjected : s.marginApproved)}</b> <span style={{ color: 'var(--ui-text-tertiary)' }}>/ {money(s.quota)}</span></span>
                    <span><span className="e8-comm-rep-k">Commission</span><b className="tnum" style={{ color: 'var(--ui-success-text)' }}>{money(comm)}</b></span>
                  </div>
                </button>
              );
            })}
          </div>

          {sel ? (
            <div style={{ marginTop: 20 }}>
              <div className="e8-sectionlabel" style={{ marginBottom: 10 }}>{sel.rep.name} · book of business</div>
              <div className="e8-card" style={{ padding: 0, overflow: 'hidden' }}>
                {sel.perCon.map((c) => {
                  const tier = E.tierRateAt(sel.marginApproved, sel.rep);
                  return (
                    <div key={c.cid} className="e8-comm-row" {...window.clickableProps(() => navigate('consultant/' + c.cid))}>
                      <span className={'e8-ts-dot ' + c.status} title={c.status} style={{ marginTop: 6 }} />
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 500 }}>{c.name}</div>
                        <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{c.role} · {c.client}</div>
                      </div>
                      <div className="e8-comm-row-n"><span className="e8-comm-rep-k">Margin</span><b className="tnum">{money(c.margin)}</b></div>
                      <div className="e8-comm-row-n"><span className="e8-comm-rep-k">Commission</span><b className="tnum" style={{ color: 'var(--ui-success-text)' }}>{money(c.margin * tier)}</b></div>
                      <div className="e8-comm-row-n" style={{ width: 116 }}>{c.pending ? <span style={{ color: 'var(--ui-warning-text)' }}>+{money(c.pendingMargin * tier)} pending</span> : <span style={{ color: 'var(--ui-text-tertiary)' }}>approved</span>}</div>
                    </div>
                  );
                })}
              </div>
              <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 8 }}>
                {sel.rep.name.split(' ')[0]} is on the {pct(E.tierRateAt(proj ? sel.marginProjected : sel.marginApproved, sel.rep))} tier ({sel.rep.tiers.map((t) => Math.round(t.rate * 100) + '%').join(' / ')} at {money(sel.rep.quota)} quota).{sel.attainmentProjected >= 1 && sel.attainment < 1 ? ' Approving the pending timesheets crosses quota into the accelerator.' : ''}
              </div>
            </div>
          ) : null}
        </div>
      </div>
    </React.Fragment>
  );
}

/* The Time hub: a manager view aggregating every consultant's current period - the approval queue,
   exceptions (OT/DT/weekend/not-submitted), category mix, and rollups by SOW + work location. Reads
   the same localStorage the entry sheet writes, so approvals stay consistent across both surfaces. */
function TimeHub() {
  const D = window.E8DATA, T = window.E8Time;
  const { showToast } = React.useContext(window.E8Ctx);
  const cats = D.payCats || [];
  const weekLabel = (D.tsWeeks || []).slice(-1)[0] || 'This week';
  const [tick, setTick] = React.useState(0);
  const [filter, setFilter] = React.useState('all');
  const money = function (n) { return '$' + Math.round(n).toLocaleString(); };
  const effCur = function (cid) { try { const s = localStorage.getItem('e8-ts-' + cid); if (s) return JSON.parse(s); } catch (err) {} const sh = (D.timesheets || {})[cid]; return sh ? sh.current : null; };

  const rows = !T ? [] : (D.engagements || []).filter(function (e) { return (D.timesheets || {})[e.cid]; }).map(function (e) {
    const sh = (D.timesheets || {})[e.cid];
    const cc = T.calcSheet(e, { workState: sh.workState, prior: sh.prior, current: effCur(e.cid) });
    return { e: e, cc: cc, c: cc.current };
  });

  const agg = { worked: 0, billable: 0, billings: 0, margin: 0, pendCount: 0, pendBill: 0, pendMargin: 0 };
  const catTot = { reg: 0, ot: 0, dt: 0, oncall: 0, hol: 0, pto: 0 };
  rows.forEach(function (r) {
    const c = r.c; agg.worked += c.worked; agg.billable += c.billableHours; agg.billings += c.billings; agg.margin += c.margin;
    Object.keys(catTot).forEach(function (k) { catTot[k] += c.cats[k] || 0; });
    if (c.status === 'submitted') { agg.pendCount++; agg.pendBill += c.billings; agg.pendMargin += c.margin; }
  });
  const otdt = catTot.ot + catTot.dt;
  const totalCatH = Object.keys(catTot).reduce(function (s, k) { return s + catTot[k]; }, 0) || 1;
  const exceptions = rows.filter(function (r) { return r.c.flags.some(function (f) { return f.k === 'ot' || f.k === 'dt' || f.k === 'weekend'; }) || r.c.status === 'pending'; });
  const submitted = rows.filter(function (r) { return r.c.status === 'submitted'; });

  const projMap = {};
  rows.forEach(function (r) { r.c.byProject.forEach(function (p) { const key = p.projectId || '_staff'; if (!projMap[key]) projMap[key] = { projectId: p.projectId, hours: 0, billings: 0 }; projMap[key].hours += p.reg + p.ot + p.dt + (p.oncall || 0); projMap[key].billings += p.billings; }); });
  const projRoll = Object.keys(projMap).map(function (k) { return projMap[k]; }).sort(function (a, b) { return b.billings - a.billings; });
  const locMap = {};
  rows.forEach(function (r) { const st = r.cc.state; if (!locMap[st]) locMap[st] = { state: st, label: r.cc.rule.label, daily: !!r.cc.rule.daily, hours: 0, ot: 0, dt: 0, n: 0 }; locMap[st].hours += r.c.worked; locMap[st].ot += r.c.cats.ot; locMap[st].dt += r.c.cats.dt; locMap[st].n++; });
  const locRoll = Object.keys(locMap).map(function (k) { return locMap[k]; }).sort(function (a, b) { return b.hours - a.hours; });

  const setStatus = function (cid, status) { const cur = effCur(cid) || {}; const next = Object.assign({}, cur, { status: status, approvedOn: status === 'approved' ? weekLabel : cur.approvedOn, submittedOn: status === 'pending' ? null : cur.submittedOn }); try { localStorage.setItem('e8-ts-' + cid, JSON.stringify(next)); } catch (err) {} };
  const bump = function () { setTick(function (t) { return t + 1; }); };
  /* R78: decisions are real now - E8Econ reads the same e8-ts-* keys, so approving moves
     billings/commissions. Every decision hits the bus + audit log, and Undo restores the sheet. */
  const snapTs = function (cid) { try { return localStorage.getItem('e8-ts-' + cid); } catch (err) { return null; } };
  const restoreTs = function (cid, raw) { try { if (raw == null) localStorage.removeItem('e8-ts-' + cid); else localStorage.setItem('e8-ts-' + cid, raw); } catch (err) {} bump(); if (window.E8Events) window.E8Events.emit('timesheet:decided', { cid: cid, status: 'undone' }); };
  const record = function (r, status, action) {
    if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: action, target: r.e.consultant + ' · ' + money(r.c.billings), route: 'consultant/' + r.e.cid });
    if (window.E8Events) window.E8Events.emit('timesheet:decided', { cid: r.e.cid, status: status, consultant: r.e.consultant, billings: r.c.billings });
  };
  const approve = function (r) { const prev = snapTs(r.e.cid); setStatus(r.e.cid, 'approved'); bump(); record(r, 'approved', 'Approved timesheet'); showToast('Approved ' + r.e.consultant + ' · ' + money(r.c.billings) + ' booked', 'Undo', function () { restoreTs(r.e.cid, prev); }); };
  const reject = function (r) { const prev = snapTs(r.e.cid); setStatus(r.e.cid, 'pending'); bump(); record(r, 'returned', 'Returned timesheet for changes'); showToast('Returned to ' + r.e.consultant + ' for changes', 'Undo', function () { restoreTs(r.e.cid, prev); }); };
  const approveAll = function () { let n = 0, b = 0; const prevs = []; submitted.forEach(function (r) { prevs.push([r.e.cid, snapTs(r.e.cid)]); setStatus(r.e.cid, 'approved'); record(r, 'approved', 'Approved timesheet'); n++; b += r.c.billings; }); bump(); showToast(n + ' timesheets approved · ' + money(b) + ' booked', 'Undo', function () { prevs.forEach(function (p) { restoreTs(p[0], p[1]); }); }); };

  const STt = { draft: 'neutral', pending: 'neutral', submitted: 'warning', approved: 'success' };
  const STl = { draft: 'Draft', pending: 'Not submitted', submitted: 'Submitted', approved: 'Approved' };
  const order = { submitted: 0, pending: 1, draft: 2, approved: 3 };
  const shown = rows.filter(function (r) {
    if (filter === 'all') return true;
    if (filter === 'submitted') return r.c.status === 'submitted';
    if (filter === 'exceptions') return exceptions.indexOf(r) >= 0;
    return r.cc.state === filter;
  }).sort(function (a, b) { return (order[a.c.status] || 9) - (order[b.c.status] || 9); });
  const chips = [
    { id: 'all', label: 'All (' + rows.length + ')' },
    { id: 'submitted', label: 'Awaiting approval (' + submitted.length + ')' },
    { id: 'exceptions', label: 'Exceptions (' + exceptions.length + ')' },
  ].concat(locRoll.map(function (l) { return { id: l.state, label: l.state + ' (' + l.n + ')' }; }));

  return (
    <React.Fragment>
      <Topbar crumbs={[{ label: 'Time' }]} actions={<React.Fragment><window.NotifyBell audience="ops" />{submitted.length ? <DSr.Button variant="primary" size="sm" icon="done_all" onClick={approveAll}>Approve all ({submitted.length})</DSr.Button> : null}</React.Fragment>} />
      <div className="e8-content">
        <div className="e8-page e8-thub">
          <div className="e8-pagehead">
            <div><h1>Time</h1><div className="e8-pagehead-sub">Week of {weekLabel} · {rows.length} consultants · hours auto-classified by work location</div></div>
          </div>

          <div className="e8-thub-kpis">
            <div className="e8-thub-kpi"><span className="e8-thub-kpi-k">Worked hours</span><span className="e8-thub-kpi-v tnum">{agg.worked}h</span></div>
            <div className="e8-thub-kpi"><span className="e8-thub-kpi-k">Billable</span><span className="e8-thub-kpi-v tnum">{agg.billable}h</span></div>
            <div className="e8-thub-kpi"><span className="e8-thub-kpi-k">Billings</span><span className="e8-thub-kpi-v tnum">{money(agg.billings)}</span></div>
            <div className="e8-thub-kpi"><span className="e8-thub-kpi-k">Gross margin</span><span className="e8-thub-kpi-v tnum" style={{ color: 'var(--ui-success-text)' }}>{money(agg.margin)}</span></div>
            <div className="e8-thub-kpi"><span className="e8-thub-kpi-k">OT + DT</span><span className="e8-thub-kpi-v tnum" style={otdt ? { color: 'var(--ui-warning-text)' } : null}>{otdt}h</span></div>
            <div className="e8-thub-kpi e8-thub-kpi-pend"><span className="e8-thub-kpi-k">Awaiting approval</span><span className="e8-thub-kpi-v tnum">{agg.pendCount} · {money(agg.pendBill)}</span></div>
          </div>

          <div className="e8-thub-mix">
            <div className="e8-thub-mixbar">
              {cats.map(function (k) { const h = catTot[k.id]; if (!h) return null; return <span key={k.id} className={'e8-thub-mixseg cat-' + k.id} style={{ width: (h / totalCatH * 100) + '%' }} title={k.label + ' ' + h + 'h'} />; })}
            </div>
            <div className="e8-thub-mixleg">
              {cats.map(function (k) { const h = catTot[k.id]; if (!h) return null; return <span key={k.id} className={'e8-thub-mixlegi cat-' + k.id}><i />{k.abbr} {h}h</span>; })}
            </div>
          </div>

          <div className="e8-thub-filters">
            {chips.map(function (ch) { return <button key={ch.id} type="button" className={'e8-thub-chip' + (filter === ch.id ? ' on' : '')} onClick={function () { setFilter(ch.id); }}>{ch.label}</button>; })}
          </div>

          <div className="e8-thub-cols">
            <div className="e8-thub-queue">
              {shown.length === 0 ? <div className="e8-thub-empty">No timesheets match this filter.</div> : shown.map(function (r) {
                const e = r.e, c = r.c;
                const proj = (c.byProject[0] && c.byProject[0].projectId) ? (D.managedEngagements || []).find(function (m) { return m.id === c.byProject[0].projectId; }) : null;
                return (
                  <div key={e.cid} className="e8-thub-row">
                    <div className="e8-thub-row-id" {...window.clickableProps(function () { navigate('consultant/' + e.cid); })}>
                      <DSr.Avatar name={e.consultant} size="sm" />
                      <div style={{ minWidth: 0 }}>
                        <div className="e8-thub-row-n">{e.consultant}{r.cc.rule.daily ? <span className="e8-thub-castate">CA</span> : null}</div>
                        <div className="e8-thub-row-sub">{e.role} · {proj ? proj.name : (e.client || 'Staff aug')}</div>
                      </div>
                    </div>
                    <div className="e8-thub-row-hrs">
                      <span className="tnum">{c.worked}h</span>
                      <div className="e8-thub-row-pips">
                        {c.cats.ot ? <i className="e8-pip cat-ot" title={c.cats.ot + 'h OT'}>{c.cats.ot}</i> : null}
                        {c.cats.dt ? <i className="e8-pip cat-dt" title={c.cats.dt + 'h DT'}>{c.cats.dt}</i> : null}
                        {c.cats.oncall ? <i className="e8-pip cat-oncall" title={c.cats.oncall + 'h on-call'}>{c.cats.oncall}</i> : null}
                        {c.cats.pto ? <i className="e8-pip cat-pto" title={c.cats.pto + 'h PTO'}>{c.cats.pto}</i> : null}
                      </div>
                    </div>
                    <div className="e8-thub-row-bill tnum">{money(c.billings)}</div>
                    <DSr.Badge tone={STt[c.status]} dot>{STl[c.status]}</DSr.Badge>
                    <div className="e8-thub-row-acts">
                      {c.status === 'submitted' ? (
                        <React.Fragment>
                          <DSr.Button variant="ghost" size="sm" icon="undo" onClick={function () { reject(r); }} />
                          <DSr.Button variant="primary" size="sm" icon="check" onClick={function () { approve(r); }}>Approve</DSr.Button>
                        </React.Fragment>
                      ) : <DSr.Button variant="ghost" size="sm" icon="chevron_right" onClick={function () { navigate('consultant/' + e.cid); }} />}
                    </div>
                  </div>
                );
              })}
            </div>

            <div className="e8-thub-side">
              {agg.pendCount ? (
                <div className="e8-card e8-thub-sidecard e8-thub-pendcard">
                  <div className="e8-thub-side-h">Awaiting approval</div>
                  <div className="e8-thub-pendbig tnum">{agg.pendCount}</div>
                  <div className="e8-thub-pendsub">{money(agg.pendBill)} billings · {money(agg.pendMargin)} margin pending</div>
                  <DSr.Button variant="primary" size="sm" icon="done_all" onClick={approveAll}>Approve all</DSr.Button>
                </div>
              ) : null}
              <div className="e8-card e8-thub-sidecard">
                <div className="e8-thub-side-h">By SOW / project</div>
                {projRoll.map(function (p, i) { const proj = p.projectId ? (D.managedEngagements || []).find(function (m) { return m.id === p.projectId; }) : null; return <div key={i} className={'e8-thub-side-row' + (proj ? ' link' : '')} {...(proj ? window.clickableProps(function () { navigate('engagement/' + proj.id); }) : {})}><span>{proj ? proj.name : 'Staff aug'}</span><span className="tnum">{p.hours}h · {money(p.billings)}</span></div>; })}
              </div>
              <div className="e8-card e8-thub-sidecard">
                <div className="e8-thub-side-h">By work location</div>
                {locRoll.map(function (l, i) { return <div key={i} className="e8-thub-side-row"><span>{l.state}{l.daily ? <span className="e8-thub-castate">daily OT</span> : <span className="e8-thub-rulelbl"> {l.label}</span>}</span><span className="tnum">{l.hours}h{l.ot + l.dt ? ' · ' + (l.ot + l.dt) + 'h OT/DT' : ''}</span></div>; })}
              </div>
            </div>
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

Object.assign(window, { ReportsScreen, ConsultantsScreen, ConsultantDetailScreen, RateLibraryScreen, AuditLogScreen, CsmsScreen, CsmDetailScreen, SyncQueueScreen, KpiCard, CommissionsScreen, TimeSheetPanel, TimeHub });
