/* Cycle overview — the management view. Three tabs, three readers:
   the book (president), the season (operations lead), charts (both).
   Everything is derived at render time. No charting library: this project runs
   React through runtime Babel with no bundler, so every chart here is inline
   SVG or CSS bars built from the existing colour variables. */
const { useState: mS } = React;

const median = (a) => { if (!a.length) return 0; const s = a.slice().sort((x, y) => x - y); const m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : Math.round((s[m - 1] + s[m]) / 2); };
const CYCLE_DAYS = 84;
const dtYear = (iso) => { const d = new Date(iso); return dt(iso, false) + (d.getFullYear() !== new Date(window.MD.TODAY).getFullYear() ? ' ' + d.getFullYear() : ''); };
const owingBucket = (o) => (o === 'Client' || o === 'Advisor' ? o : 'Internal');
/* No second copy of the picklist: MD.STAGES is the source. */
const stageIdx = (r) => window.MD.STAGES.indexOf(r.stage);
const pctOf = (n, d) => (d ? Math.round((n / d) * 100) : 0);
/* MD.daysTo now counts whole calendar days, so the ladder and every condition-level
   display share one computation. The local copy this file used to carry is gone. */
const calDays = (dateStr) => window.MD.daysTo(dateStr);

function cycleModel() {
  const MD = window.MD; const AG = window.AG;
  const all = MD.renewals;
  const sum = (a) => a.reduce((t, r) => t + (r.premiumPrior || 0), 0);
  const sapSum = (a) => a.reduce((t, r) => t + (r.sapAum || 0), 0);
  const terminal = all.filter(MD.isTerminal);
  const open = all.filter(MD.isActiveRenewal);
  /* The third state has to appear anywhere the book is PARTITIONED, or the parts stop
     summing to the whole while the same sentence asserts the total. */
  const offPath = all.filter(MD.isOffPath);
  const renewed = terminal.filter((r) => r.stage === 'Renewed');
  const lapsed = terminal.filter((r) => r.stage === 'Not Renewed');
  const overdueOf = (r) => AG.openC(r).some((c) => c.pressure === 'overdue' || c.daysTo < 0);
  const overdue = open.filter(overdueOf);

  /* deadline ladder — three deadlines, three populations, deliberately not one date */
  const ladder = [
    { key: 'submission', name: 'Submission', date: MD.dl('submission'), days: calDays(MD.dl('submission')),
      pop: all.filter(window.GATES.submission), view: 'submission',
      what: 'have not reached underwriting' },
    { key: 'selection', name: 'Selection', date: MD.dl('selection'), days: calDays(MD.dl('selection')),
      pop: all.filter(window.GATES.selection), view: 'selection',
      what: 'have not returned a selection' },
    { key: 'payment', name: 'Payment', date: MD.dl('payment'), days: calDays(MD.dl('payment')),
      pop: all.filter(window.GATES.payment), view: 'all', what: 'have payment outstanding' },
  ].map((x) => Object.assign(x, { n: x.pop.length, premium: sum(x.pop), passed: x.days < 0 }));

  /* retention */
  const retCount = terminal.length ? renewed.length / terminal.length : 0;
  const retPremium = sum(terminal) ? sum(renewed) / sum(terminal) : 0;
  /* Projected: the whole book concludes, every currently overdue renewal lapses, and
     everything else renews. Counting only terminal + overdue would silently assume
     the other 190 open renewals never renew either, which answers a different and
     much bleaker question than "is the goal still reachable". */
  const notOverdueOpen = open.filter((r) => !overdueOf(r));
  const projRenewed = renewed.concat(notOverdueOpen);
  const retProjected = all.length ? projRenewed.length / all.length : 0;
  const retProjectedPremium = sum(all) ? sum(projRenewed) / sum(all) : 0;

  /* pace */
  const weeksElapsed = CYCLE_DAYS / 7;
  const daysLeft = calDays(MD.dl('payment')); /* same basis as the ladder, so the screen never quotes two figures */
  const actualPerWeek = terminal.length / weeksElapsed;
  const requiredPerWeek = open.length / Math.max(daysLeft / 7, 0.1);
  const weekly = [];
  for (let w = 11; w >= 0; w--) {
    const hi = +MD.TODAY - w * 7 * MD.DAY, lo = hi - 7 * MD.DAY;
    weekly.push({ label: 'wk ' + (12 - w), n: terminal.filter((r) => { const t = +new Date(r.closedAt); return t > lo && t <= hi; }).length });
  }

  /* stuck */
  const byCause = {}; const byParty = {};
  open.forEach((r) => AG.openC(r).forEach((c) => {
    (byCause[c.cat] = byCause[c.cat] || { cat: c.cat, label: MD.B[c.cat] || c.cat, n: 0, days: 0, ages: [] });
    byCause[c.cat].n += 1; byCause[c.cat].days += c.age; byCause[c.cat].ages.push(c.age);
    const p = owingBucket(c.owing);
    (byParty[p] = byParty[p] || { party: p, n: 0, days: 0 });
    byParty[p].n += 1; byParty[p].days += c.age;
  }));
  const causes = Object.keys(byCause).map((k) => Object.assign(byCause[k], { medianAge: median(byCause[k].ages) })).sort((a, b) => b.days - a.days);
  const totalStuckDays = causes.reduce((t, c) => t + c.days, 0) || 1;
  const parties = ['Client', 'Advisor', 'Internal'].map((p) => byParty[p] || { party: p, n: 0, days: 0 }).sort((a, b) => b.days - a.days);

  /* stalled — inside its deadline but dead in the water */
  const stalled = open.map((r) => ({ r, days: Math.round((MD.TODAY - new Date(r.stageSince)) / MD.DAY) }))
    .filter((x) => x.days > 14).sort((a, b) => b.days - a.days);

  /* workload */
  const cycleStart = +MD.TODAY - CYCLE_DAYS * MD.DAY;
  const workload = MD.staff.map((s) => {
    const mine = all.filter((r) => r.owner === s.id);
    const myOpen = mine.filter(MD.isActiveRenewal);
    const myClosed = mine.filter(MD.isTerminal);
    return { s, total: mine.length, open: myOpen.length, overdue: myOpen.filter(overdueOf).length, closed: myClosed.length,
      medianClose: median(myClosed.map((r) => Math.round((+new Date(r.closedAt) - cycleStart) / MD.DAY))), premium: sum(mine) };
  });

  /* advisors */
  const advisorRows = MD.advisors.filter((a) => a.policies.length).map((a) => {
    const rs = a.policies.map((id) => MD.byId[id]).filter(Boolean);
    const act = rs.filter(MD.isActiveRenewal);
    const atRisk = act.filter(overdueOf);
    return { a, rs, clients: new Set(rs.map((r) => r.account)).size, active: act.length,
      need: act.filter(AG.needsA).length, premium: sum(rs), sap: sapSum(rs),
      atRisk: sum(atRisk), atRiskN: atRisk.length,
      retLast: a.bookLast ? a.renewedLast / a.bookLast : null,
      quietDays: Math.round((MD.TODAY - new Date(a.lastTouch)) / MD.DAY) };
  }).sort((x, y) => y.premium - x.premium);
  const bookPremium = sum(all) || 1;
  const bookSap = sapSum(all) || 1;
  const top15 = advisorRows.slice(0, 15);

  /* who to call — most premium at risk, least recently contacted */
  const toCall = advisorRows.filter((x) => x.atRiskN && x.quietDays >= 7)
    .sort((x, y) => y.atRisk - x.atRisk).slice(0, 6);

  /* agent */
  const drafts = MD.drafts;
  const inReview = drafts.filter((d) => d.state === 'ready');
  const noPerson = all.filter((r) => {
    if ((r.timeline || []).some((e) => e.src === 'user')) return false;
    return (r.resolvedConds || []).length || r.conditions.some((c) => c.status === 'resolved') || MD.isTerminal(r);
  }).length;
  const firstContact = [];
  all.forEach((r) => AG.openC(r).forEach((c) => { if (c.attempts > 0 && c.lastAttempt !== null) firstContact.push(Math.max(0, c.age - c.lastAttempt) * 24 / Math.max(1, c.attempts)); }));

  /* stage distribution */
  const stageDist = MD.STAGES.concat(['Not Renewed']).map((st) => ({ stage: st, n: all.filter((r) => r.stage === st).length, terminal: MD.TERMINAL.indexOf(st) >= 0, offPath: MD.OFF_PATH.indexOf(st) >= 0 })).filter((x) => x.n);
  /* Eighteen segments is unreadable, so the chart renders the six phases. */
  const groupDist = MD.STAGE_GROUPS.map((g) => ({ id: g.id, label: g.label, n: all.filter((r) => MD.stageGroup(r.stage) === g.id).length })).filter((x) => x.n);

  return { all, open, offPath, terminal, renewed, lapsed, overdue, sum, sapSum,
    closedPremium: sum(terminal), overduePremium: sum(overdue), lapsedPremium: sum(lapsed),
    ladder, retCount, retPremium, retProjected, retProjectedPremium,
    actualPerWeek, requiredPerWeek, daysLeft, weekly,
    causes, totalStuckDays, parties, stalled, workload,
    advisorRows, top15, bookPremium, bookSap, toCall, stageDist, groupDist,
    agent: { prepared: drafts.length, sent: drafts.filter((d) => d.state === 'sent').length,
      consolidated: drafts.filter((d) => d.consolidatedInto).length || (AG.consolidationGroups ? AG.consolidationGroups().reduce((t, g) => t + g.drafts.length, 0) : 0),
      noPerson, medianFirstContact: median(firstContact), inReview: inReview.length,
      stale: inReview.filter((d) => (MD.TODAY - new Date(d.createdAt)) / MD.DAY > 1).length } };
}

/* ---------------- shared bits ---------------- */
function Bar({ pct, tone, h = 6 }) {
  return <span style={{ display: 'block', height: h, borderRadius: 3, background: 'var(--surface-3)', overflow: 'hidden' }}>
    <span style={{ display: 'block', height: '100%', width: Math.max(1, pct) + '%', background: tone || 'var(--line-strong)' }} />
  </span>;
}
function ChartCard({ title, caption, children, wide }) {
  return <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 'var(--r-lg)', padding: '16px 18px 18px', gridColumn: wide ? '1 / -1' : undefined, minWidth: 0 }}>
    <div className="eyebrow">{title}</div>
    <div className="sm mut" style={{ marginTop: 4, marginBottom: 14, lineHeight: 1.5 }}>{caption}</div>
    {children}
  </div>;
}
function Read({ eyebrow, headline, paragraphs, ico = 'gauge' }) {
  const MD = window.MD;
  return <div className="assess">
    <div className="head">
      <span style={{ width: 32, height: 32, borderRadius: 999, background: 'var(--tint-purple)', color: 'var(--purple-deep)', display: 'grid', placeItems: 'center' }}><Icon n={ico} s={15} /></span>
      <div className="grow"><span className="eyebrow">{eyebrow}</span><h2 style={{ marginTop: 3 }}>{headline}</h2></div>
      <span className="src engine" style={{ flex: '0 0 auto' }}><i className="sd" />Derived {rel(MD.health.engine.lastFull)}</span>
    </div>
    {paragraphs.map((p, i) => <p key={i} className="prose" dangerouslySetInnerHTML={{ __html: p }} />)}
  </div>;
}

/* ---------------- THE BOOK ---------------- */
function BookView({ m }) {
  const app = useApp(); const MD = window.MD;
  const [aSort, setASort] = mS({ k: 'premium', d: 'desc' });

  const worstAdvisor = m.toCall[0];
  const headline = m.retCount >= MD.CYCLE.target
    ? 'Retention is at goal on the renewals decided so far.'
    : 'Retention is running under goal, and the overdue set decides whether it recovers.';
  const paras = [
    '<b>' + m.terminal.length + ' of ' + m.all.length + ' renewals</b> have concluded — ' + m.renewed.length + ' renewed, ' + m.lapsed.length + ' lapsed, ' + money(m.closedPremium) + ' of premium decided. Count-weighted retention is <b>' + (m.retCount * 100).toFixed(1) + '%</b> against a ' + Math.round(MD.CYCLE.target * 100) + '% goal and ' + Math.round(MD.CYCLE.priorRate * 100) + '% last cycle.',
    /* Both the assumption and the basis change have to be named. 66.4% already counts
       the other open renewals as renewing, and it is measured across all 363 while
       81.0% is measured on the 63 concluded — calling it a "fall" compares two
       different denominators and contradicts this page's own caveat. */
    'The largest single risk is <b>' + money(m.overduePremium) + ' of premium</b> on ' + m.overdue.length + ' renewals that are already past a deadline. If all of them lapsed and every other renewal came through, the full cycle would land at <b>' + (m.retProjected * 100).toFixed(1) + '%</b> — against the ' + (m.retCount * 100).toFixed(1) + '% measured on the ' + pctOf(m.terminal.length, m.all.length) + '% concluded so far. On that basis the goal is ' + (m.retProjected >= MD.CYCLE.target ? 'still reachable even in that case' : 'not reachable unless most of that set closes') + '.',
    m.ladder[0].n
      ? '<b>' + m.ladder[0].n + ' renewals still have not reached underwriting</b>, ' + Math.abs(m.ladder[0].days) + ' days after the submission deadline passed. That is ' + money(m.ladder[0].premium) + ' of premium behind the first gate, and it is the population nobody sees on the daily screens.'
      : 'Every renewal has cleared the submission gate.',
    worstAdvisor
      ? 'This week: <b>' + worstAdvisor.a.name + '</b> holds ' + money(worstAdvisor.atRisk) + ' of at-risk premium across ' + worstAdvisor.atRiskN + ' renewals and has not been contacted in ' + worstAdvisor.quietDays + ' days. Start there.'
      : 'This week: no advisor is holding at-risk premium without recent contact.',
  ];

  const adv = m.top15.slice().sort((x, y) => {
    const v = (o) => ({ name: o.a.name, clients: o.clients, premium: o.premium, sap: o.sap, retLast: o.retLast || 0, atRisk: o.atRisk }[aSort.k]);
    const a = v(x), b = v(y);
    const c = typeof a === 'string' ? a.localeCompare(b) : a - b;
    return aSort.d === 'asc' ? c : -c;
  });
  const top15Premium = m.top15.reduce((t, x) => t + x.premium, 0);
  const top15Sap = m.top15.reduce((t, x) => t + x.sap, 0);

  return <div>
    <Read eyebrow="Where the cycle stands" headline={headline} paragraphs={paras} />

    {/* deadline ladder */}
    <div className="secwrap" style={{ marginTop: 26 }}>
      <div className="sec"><span className="st">Deadline ladder</span><span className="snote">Three deadlines, three populations — they cannot be collapsed into one date</span></div>
      <div className="mon">{m.ladder.map((d) => <div className="mrow" key={d.key} onClick={() => app.nav('#/book/' + d.view)}>
        <span className="mw">{d.name}<div className="xs dim" style={{ marginTop: 2 }}>{d.date}</div></span>
        <span className="mx">
          <span className={d.passed ? 'hot' : ''} style={{ fontWeight: d.passed ? 600 : 400 }}>
            {d.passed ? 'PASSED ' + Math.abs(d.days) + ' days ago' : d.days + ' days remaining'}
          </span>
          <div className="xs dim" style={{ marginTop: 2 }}>{d.n} renewals {d.what}{d.passed ? ' — every one is already late' : ''}</div>
        </span>
        <span className="mt" style={{ flex: '0 0 190px' }}>
          <span className="tab" style={{ fontSize: 15, fontWeight: 600, color: d.passed ? 'var(--red-deep)' : 'var(--text)' }}>{d.n}</span>
          <span className="tab" style={{ fontSize: 12.5, color: 'var(--text-muted)', marginLeft: 8 }}>{money(d.premium)}</span>
          <div className="xs dim" style={{ marginTop: 3 }}>{d.view === 'all' ? 'open all active' : 'open these ' + d.n}<Icon n="chevron-right" s={10} style={{ display: 'inline-block', verticalAlign: -1 }} /></div>
        </span>
      </div>)}</div>
    </div>

    {/* retention */}
    <div className="secwrap">
      <div className="sec"><span className="st">Retention</span><span className="snote">Against a {Math.round(MD.CYCLE.target * 100)}% goal and {Math.round(MD.CYCLE.priorRate * 100)}% last cycle</span></div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(220px,1fr))', gap: 12 }}>
        {[{ rate: m.retCount, label: 'Count-weighted', note: m.renewed.length + ' of ' + m.terminal.length + ' concluded renewals', lead: true, sub: 'the figure Madison quotes' },
          { rate: m.retPremium, label: 'Premium-weighted', note: money(m.sum(m.renewed)) + ' of ' + money(m.closedPremium), sub: 'weights large accounts properly' },
          { rate: m.retProjected, label: 'Projected full cycle', note: 'if all ' + m.overdue.length + ' overdue renewals lapse and the rest renew', sub: 'whether the goal is still reachable', proj: true }].map((x) => {
          const under = x.rate < MD.CYCLE.target;
          const vsPrior = x.rate - MD.CYCLE.priorRate;
          return <div key={x.label} style={{ background: 'var(--surface)', border: '1px solid ' + (x.proj ? 'rgba(229,107,111,.35)' : 'var(--line)'), borderRadius: 'var(--r-lg)', padding: '16px 18px' }}>
            <div className="eyebrow">{x.label}</div>
            <div style={{ fontSize: x.lead ? 34 : 27, fontWeight: 600, letterSpacing: '-.025em', marginTop: 4, fontVariantNumeric: 'tabular-nums', color: x.proj ? 'var(--red-deep)' : under ? 'var(--amber-deep)' : 'var(--green-deep)' }}>{(x.rate * 100).toFixed(1)}%</div>
            <div className="wrap" style={{ marginTop: 7 }}>
              <span className={'pill ' + (under ? 't-amber' : 't-green')}>{under ? (MD.CYCLE.target * 100 - x.rate * 100).toFixed(1) + ' under goal' : 'at goal'}</span>
              <span className={'pill ' + (vsPrior >= 0 ? 't-green' : 't-red')}>{vsPrior >= 0 ? '+' : ''}{(vsPrior * 100).toFixed(1)} vs last cycle</span>
            </div>
            <div className="sm mut" style={{ marginTop: 8 }}>{x.note}</div>
            <div className="xs dim" style={{ marginTop: 3 }}>{x.sub}</div>
          </div>;
        })}
      </div>
      <div className="note" style={{ marginTop: 12 }}>
        <Icon n="info" s={15} style={{ color: 'var(--text-muted)' }} />
        <span><b>{m.terminal.length} of {m.all.length} renewals in this cycle have concluded</b> — {pctOf(m.terminal.length, m.all.length)}% of the book. These rates describe that {pctOf(m.terminal.length, m.all.length)}%, not the cycle. {m.open.length} are still open{m.offPath.length ? ' and ' + m.offPath.length + ' have been routed to Client Concierge, off the renewal path' : ''}.</span>
      </div>

      <div className="sec" style={{ marginTop: 22 }}><span className="st">Lapsed this cycle</span><span className="sn">{m.lapsed.length}</span><span className="snote">{money(m.lapsedPremium)} of premium</span></div>
      <div className="mon">{m.lapsed.slice().sort((a, b) => b.premiumPrior - a.premiumPrior).map((r) => {
        const a = window.AG.advOf(r);
        return <div className="mrow" key={r.id} onClick={() => app.nav('#/renewal/' + r.id)}>
          <span className="mw">{r.account}<div className="xs dim" style={{ marginTop: 2 }}>{r.policy}</div></span>
          <span className="mx">{a ? a.name + ' · ' + a.firm : 'No referring advisor'}</span>
          <span className="mt"><span className="tab" style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)' }}>{money(r.premiumPrior)}</span><div className="xs dim" style={{ marginTop: 3 }}>closed {dt(r.closedAt, false)}</div></span>
        </div>;
      })}</div>
    </div>

    {/* who is bringing revenue */}
    <div className="secwrap">
      <div className="sec"><span className="st">Who is bringing revenue</span><span className="snote">Top fifteen advisors by prior-year book premium</span></div>
      <div className="sm mut" style={{ marginBottom: 11 }}>
        These fifteen hold <b>{pctOf(top15Premium, m.bookPremium)}% of book premium</b> ({money(top15Premium)} of {money(m.bookPremium)}) and <b>{pctOf(top15Sap, m.bookSap)}% of SAP AUM</b> ({money(top15Sap)} of {money(m.bookSap)}). Concentration is the point.
      </div>
      <div className="btbl-wrap">
        <table className="btbl dense">
          <thead><tr>
            <Th id="name" sort={aSort} setSort={setASort}>Advisor</Th>
            <Th id="clients" sort={aSort} setSort={setASort} right>Clients</Th>
            <Th id="premium" sort={aSort} setSort={setASort} right>Book premium</Th>
            <Th id="sap" sort={aSort} setSort={setASort} right>SAP AUM</Th>
            <Th id="retLast" sort={aSort} setSort={setASort} right>Retention last cycle</Th>
            <Th id="atRisk" sort={aSort} setSort={setASort} right>Premium at risk</Th>
          </tr></thead>
          <tbody>{adv.map((x) => <tr key={x.a.id} onClick={() => app.nav('#/advisor/' + x.a.id)}>
            <td><b>{x.a.name}</b><div className="xs dim">{x.a.firm}</div></td>
            <td style={{ textAlign: 'right' }}>{x.clients}</td>
            <td style={{ textAlign: 'right' }}>{money(x.premium)}</td>
            <td style={{ textAlign: 'right' }}>{money(x.sap)}</td>
            <td style={{ textAlign: 'right' }} className={x.retLast !== null && x.retLast < MD.CYCLE.priorRate ? 'hot' : ''}>{x.retLast === null ? '—' : Math.round(x.retLast * 100) + '%'}</td>
            <td style={{ textAlign: 'right' }} className={x.atRisk ? 'hot' : ''}>{x.atRisk ? money(x.atRisk) : '—'}<div className="xs dim">{x.atRiskN ? x.atRiskN + ' renewals' : ''}</div></td>
          </tr>)}</tbody>
        </table>
      </div>
    </div>

    {/* who to call */}
    <div className="secwrap">
      <div className="sec"><span className="st">Who to call this week</span><span className="sn">{m.toCall.length}</span><span className="snote">Most at-risk premium, least recent contact</span></div>
      {m.toCall.length ? <div className="mon">{m.toCall.map((x) => <div className="mrow" key={x.a.id} onClick={() => app.nav('#/advisor/' + x.a.id)}>
        <span className="mw">{x.a.name}<div className="xs dim" style={{ marginTop: 2 }}>{x.a.firm}</div></span>
        <span className="mx">{money(x.atRisk)} at risk across {x.atRiskN} renewal{x.atRiskN === 1 ? '' : 's'}
          <div className="xs dim" style={{ marginTop: 2 }}>
            <span style={{ color: x.quietDays >= 14 ? 'var(--amber-deep)' : 'var(--text-muted)', fontWeight: x.quietDays >= 14 ? 600 : 400 }}>no contact in {x.quietDays} days</span>
            {x.a.portal !== 'active' ? <span style={{ color: 'var(--red-deep)', fontWeight: 600 }}> · portal {x.a.portal}</span> : null}
          </div></span>
        <span className="mt" style={{ flex: '0 0 130px' }}><span className="pill t-amber">Open briefing</span></span>
      </div>)}</div>
        : <div className="note"><Icon n="check" s={15} style={{ color: 'var(--green-deep)' }} /><span>No advisor is holding at-risk premium without recent contact. Nothing to chase at the advisor level this week.</span></div>}
    </div>
  </div>;
}

/* ---------------- THE SEASON ---------------- */
function SeasonView({ m }) {
  const app = useApp(); const MD = window.MD;
  const [wSort, setWSort] = mS({ k: 'open', d: 'desc' });
  const rd = MD.settings.repeatedDelay;
  const offenders = rd.enabled && window.repeatOffenders ? window.repeatOffenders(rd.threshold, rd.window) : [];
  const staleGroups = MD.health.email.groups.filter((g) => g.state === 'stale');

  const heaviest = m.workload.slice().sort((a, b) => b.open - a.open)[0];
  const headline = m.ladder[0].n
    ? m.ladder[0].n + ' renewals are behind the submission gate with ' + m.daysLeft + ' days of cycle left.'
    : 'The book is past the submission gate; selection is the live constraint.';
  const paras = [
    'Between now and ' + MD.dl('payment') + ' the pinch is <b>' + m.parties[0].party.toLowerCase() + '</b>, holding ' + pctOf(m.parties[0].days, m.totalStuckDays) + '% of all days lost across ' + m.parties[0].n + ' open conditions. The biggest single cause is <b>' + (m.causes[0] ? m.causes[0].label.toLowerCase() + '</b> — ' + m.causes[0].n + ' renewals, median age ' + m.causes[0].medianAge + ' days' : 'nothing</b>') + '.',
    '<b>' + m.stalled.length + ' renewals have not changed stage in over a fortnight</b>, and ' + m.overdue.length + ' are past a deadline. Those two sets overlap but are not the same — a renewal can sit inside its deadline and still be dead in the water.',
    heaviest ? '<b>' + heaviest.s.name + '</b> is carrying ' + heaviest.open + ' open renewals with ' + heaviest.overdue + ' overdue, the heaviest queue on the team. Median close times are similar across everyone, so this is distribution rather than speed.' : '',
    staleGroups.length
      ? 'Watch the sources: <b>' + staleGroups.length + ' mailbox group' + (staleGroups.length > 1 ? 's are' : ' is') + ' stale</b>, so the agent is not treating absence of a reply as fact on the renewals they cover.'
      : 'All sources are current, so the agent is drawing conclusions from a complete picture.',
  ].filter(Boolean);

  const wl = m.workload.slice().sort((x, y) => {
    const v = (o) => ({ name: o.s.name, open: o.open, overdue: o.overdue, closed: o.closed, medianClose: o.medianClose, premium: o.premium }[wSort.k]);
    const a = v(x), b = v(y);
    const c = typeof a === 'string' ? a.localeCompare(b) : a - b;
    return wSort.d === 'asc' ? c : -c;
  });

  return <div>
    <Read eyebrow="What needs watching" headline={headline} paragraphs={paras} ico="alert" />

    {/* workload */}
    <div className="secwrap" style={{ marginTop: 26 }}>
      <div className="sec"><span className="st">Where work is piling up</span><span className="snote">Distribution, not speed — median close times are similar across the team</span></div>
      <div className="btbl-wrap">
        <table className="btbl dense">
          <thead><tr>
            <Th id="name" sort={wSort} setSort={setWSort}>Owner</Th>
            <Th id="open" sort={wSort} setSort={setWSort} right>Open</Th>
            <Th id="overdue" sort={wSort} setSort={setWSort} right>Overdue</Th>
            <Th id="closed" sort={wSort} setSort={setWSort} right>Closed this cycle</Th>
            <Th id="medianClose" sort={wSort} setSort={setWSort} right>Median days to close</Th>
            <Th id="premium" sort={wSort} setSort={setWSort} right>Premium under management</Th>
          </tr></thead>
          <tbody>{wl.map((x) => <tr key={x.s.id} style={{ cursor: 'default' }}>
            <td><b>{x.s.name}</b><div className="xs dim">{x.s.role}</div></td>
            <td style={{ textAlign: 'right' }} className={x.open > m.open.length / m.workload.length * 1.5 ? 'hot' : ''}>{x.open}</td>
            <td style={{ textAlign: 'right' }}>{x.overdue}</td>
            <td style={{ textAlign: 'right' }}>{x.closed}</td>
            <td style={{ textAlign: 'right' }}>{x.medianClose}d</td>
            <td style={{ textAlign: 'right' }}>{money(x.premium)}</td>
          </tr>)}</tbody>
        </table>
        <div className="row" style={{ padding: '11px 16px', gap: 12, borderTop: '1px solid var(--line)', fontSize: 12.5, color: 'var(--text-muted)' }}>
          <span>{m.workload.reduce((t, x) => t + x.total, 0)} renewals across {m.workload.length} owners</span><span className="dim">·</span>
          <span>{m.open.length} open</span><span className="dim">·</span><span>{m.terminal.length} concluded</span>{m.offPath.length ? <><span className="dim">·</span><span>{m.offPath.length} off path</span></> : null}
          <span className="grow" /><span className="dim">Median days to close is measured from the start of the working cycle</span>
        </div>
      </div>
    </div>

    {/* stalled */}
    <div className="secwrap">
      <div className="sec"><span className="st">Stalled</span><span className="sn">{m.stalled.length}</span><span className="snote">No stage change in over 14 days — not the same as overdue</span></div>
      <div className="btbl-wrap">
        <table className="btbl dense">
          <thead><tr><th>Account</th><th>Advisor</th><th>Stage</th><th style={{ textAlign: 'right' }}>Days in stage</th><th>Owner</th><th>Deadline</th></tr></thead>
          <tbody>{m.stalled.slice(0, 25).map(({ r, days }) => {
            const a = window.AG.advOf(r); const c = window.AG.prim(r);
            const o = MD.staff.find((s) => s.id === r.owner);
            return <tr key={r.id} onClick={() => app.nav('#/renewal/' + r.id)}>
              <td><b>{r.account}</b><div className="xs dim">{r.policy}</div></td>
              <td className="mut">{a ? a.name : <span className="pill t-amber">None</span>}</td>
              <td className="mut">{r.stage}</td>
              <td style={{ textAlign: 'right' }} className={days > 25 ? 'hot' : ''}>{days}d</td>
              <td className="mut">{o ? o.name : '—'}</td>
              <td className={c && c.daysTo < 0 ? 'hot' : 'mut'}>{c ? (c.daysTo < 0 ? Math.abs(c.daysTo) + 'd over' : c.daysTo + 'd') : '—'}</td>
            </tr>;
          })}</tbody>
        </table>
        {m.stalled.length > 25 ? <div className="row" style={{ padding: '11px 16px', borderTop: '1px solid var(--line)', fontSize: 12.5, color: 'var(--text-muted)' }}>
          <span>Showing the 25 longest-stalled of {m.stalled.length}</span>
        </div> : null}
      </div>
    </div>

    {/* where the book is stuck */}
    <div className="secwrap">
      <div className="sec"><span className="st">Where the book is stuck</span><span className="snote">Sorted by days lost, not by count</span></div>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.5fr) minmax(0,1fr)', gap: 12, alignItems: 'start' }}>
        <div className="btbl-wrap">
          <table className="btbl dense">
            <thead><tr><th>Cause</th><th style={{ textAlign: 'right' }}>Renewals</th><th style={{ textAlign: 'right' }}>Days lost</th><th style={{ textAlign: 'right' }}>Median age</th><th>Share</th></tr></thead>
            <tbody>{m.causes.map((c) => <tr key={c.cat} onClick={() => app.nav('#/book')}>
              <td><span className="row" style={{ gap: 7 }}><span className="bcode">{c.cat}</span><span className="trunc" style={{ maxWidth: 190 }}>{c.label}</span></span></td>
              <td style={{ textAlign: 'right' }}>{c.n}</td>
              <td style={{ textAlign: 'right' }} className={c.days > m.totalStuckDays * 0.2 ? 'hot' : ''}>{c.days}</td>
              <td style={{ textAlign: 'right' }}>{c.medianAge}d</td>
              <td><span className="row" style={{ gap: 7 }}><span style={{ width: 52 }}><Bar pct={pctOf(c.days, m.totalStuckDays)} h={5} tone={c.days > m.totalStuckDays * 0.2 ? 'var(--red)' : 'var(--line-strong)'} /></span><span className="xs dim">{pctOf(c.days, m.totalStuckDays)}%</span></span></td>
            </tr>)}</tbody>
          </table>
        </div>
        <div>
          {m.parties.map((p) => <div className="dbar" key={p.party} style={{ border: '1px solid var(--line)', borderRadius: 'var(--r-md)', marginBottom: 8, display: 'block', padding: '12px 14px' }}>
            <div className="row" style={{ gap: 9 }}>
              <b style={{ fontSize: 13.5 }}>{p.party}</b>
              <span className="grow" />
              <span className="tab" style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>{pctOf(p.days, m.totalStuckDays)}%</span>
            </div>
            <div style={{ marginTop: 8 }}><Bar pct={pctOf(p.days, m.totalStuckDays)} tone={p.party === 'Internal' ? 'var(--amber)' : 'var(--blue)'} /></div>
            <div className="xs dim" style={{ marginTop: 5 }}>{p.n} conditions · {p.days} days lost</div>
          </div>)}
          <div className="sm mut" style={{ marginTop: 10, lineHeight: 1.55 }}>
            {m.parties[0].party === 'Internal'
              ? <span><b>Internal is the largest bucket.</b> That is a capacity conversation, not a chasing one — no amount of outreach moves work sitting with us.</span>
              : <span><b>{m.parties[0].party} is the largest bucket</b> at {pctOf(m.parties[0].days, m.totalStuckDays)}% of days lost. Internal holds {pctOf((m.parties.find((p) => p.party === 'Internal') || { days: 0 }).days, m.totalStuckDays)}% — if that grows, it becomes a staffing conversation rather than a chasing one.</span>}
          </div>
        </div>
      </div>
    </div>

    {/* repeated delays */}
    <div className="secwrap">
      <div className="sec"><span className="st">Repeated delays</span>{rd.enabled ? <span className="sn">{offenders.length}</span> : null}
        <span className="snote">{rd.enabled ? rd.threshold + '+ delays inside ' + rd.window + ' days · the threshold set in Settings' : 'alerts are switched off'}</span></div>
      {!rd.enabled
        ? <div className="note"><Icon n="ban" s={15} style={{ color: 'var(--text-muted)' }} /><span>Repeated-delay alerts are switched off in <button className="reasoning-link" onClick={() => app.nav('#/settings')}>Settings</button>, so no pattern is being tracked across renewals. Individual delays still show on their own renewals.</span></div>
        : offenders.length ? <div className="mon">{offenders.slice(0, 8).map((o) => <div className="mrow" key={o.who} style={{ cursor: 'default' }}>
          <span className="mw">{o.who}</span>
          <span className="mx">{o.accounts.slice(0, 3).join(' · ')}{o.accounts.length > 3 ? ' · +' + (o.accounts.length - 3) + ' more' : ''}
            <div className="xs dim" style={{ marginTop: 2 }}>oldest condition {o.oldest} days</div></span>
          <span className="mt" style={{ flex: '0 0 110px' }}><span className="pill t-amber">{o.n} renewals</span></span>
        </div>)}</div>
        : <div className="note"><Icon n="check" s={15} style={{ color: 'var(--green-deep)' }} /><span>Nobody meets the configured pattern — no single party is behind on {rd.threshold} or more renewals inside {rd.window} days.</span></div>}
    </div>

    {/* is the agent running */}
    <div className="secwrap">
      <div className="sec"><span className="st">Is the agent running</span><span className="snote">If it quietly stopped, this is where you would find out</span></div>
      <div className="mon">
        <div className="mrow" style={{ cursor: 'default' }}>
          <span className="mw">Salesforce<div className="xs dim" style={{ marginTop: 2 }}>system of record</div></span>
          <span className="mx">Last read {rel(MD.health.salesforce.lastRead)} · last write {rel(MD.health.salesforce.lastWrite)}
            <div className="xs dim" style={{ marginTop: 2 }}>{MD.health.salesforce.queued ? MD.health.salesforce.queued + ' writes queued' : 'no queued writes'}</div></span>
          <span className="mt" style={{ flex: '0 0 120px' }}><span className={'pill ' + (MD.health.salesforce.freshness === 'current' ? 't-green' : 't-amber')}>{MD.health.salesforce.freshness === 'current' ? 'Current' : 'Stale'}</span></span>
        </div>
        {MD.health.email.groups.map((g) => <div className="mrow" key={g.name} style={{ cursor: 'default' }}>
          <span className="mw">{g.name.replace(' shared mailbox', '')}<div className="xs dim" style={{ marginTop: 2 }}>shared mailbox</div></span>
          <span className="mx">Last observed {rel(g.last)}
            {g.state === 'stale' ? <div className="xs" style={{ marginTop: 2, color: 'var(--amber-deep)', fontWeight: 600 }}>absence of a reply is not being treated as fact here</div> : <div className="xs dim" style={{ marginTop: 2 }}>observation current</div>}</span>
          <span className="mt" style={{ flex: '0 0 120px' }}><span className={'pill ' + (g.state === 'stale' ? 't-amber' : 't-green')}>{g.state === 'stale' ? 'Stale' : 'Current'}</span></span>
        </div>)}
        <div className="mrow" style={{ cursor: 'default' }}>
          <span className="mw">Full-book evaluation<div className="xs dim" style={{ marginTop: 2 }}>the agent's own run</div></span>
          <span className="mx">Ran {rel(MD.health.engine.lastFull)} across {MD.health.engine.evaluated} renewals
            <div className="xs dim" style={{ marginTop: 2 }}>{MD.health.engine.failed24 ? MD.health.engine.failed24 + ' failed evaluations in 24h' : 'no failures in 24h'} · queue {MD.health.engine.queue}</div></span>
          <span className="mt" style={{ flex: '0 0 120px' }}><span className={'pill ' + (MD.health.engine.failed24 ? 't-amber' : 't-green')}>{MD.health.engine.failed24 ? MD.health.engine.failed24 + ' failed' : 'Clean'}</span></span>
        </div>
        <div className="mrow" style={{ cursor: 'default' }} onClick={() => app.nav('#/outreach')}>
          <span className="mw">Drafts in review<div className="xs dim" style={{ marginTop: 2 }}>waiting on a person</div></span>
          <span className="mx">{m.agent.inReview} prepared and unsent
            <div className="xs dim" style={{ marginTop: 2 }}>{m.agent.stale ? m.agent.stale + ' sitting longer than a day' : 'none older than a day'}</div></span>
          <span className="mt" style={{ flex: '0 0 120px' }}><span className={'pill ' + (m.agent.stale ? 't-amber' : 't-slate')}>{m.agent.stale ? m.agent.stale + ' ageing' : 'Fresh'}</span></span>
        </div>
      </div>
      <div className="sm mut" style={{ marginTop: 9 }}>No send failures recorded this cycle. Sends are blocked outright rather than retried when Salesforce cannot accept the audit write, so a failure would appear as a queued write above.</div>
    </div>

    {/* what the agent did */}
    <div className="secwrap">
      <div className="sec"><span className="st">What the agent did</span><span className="snote">This cycle</span></div>
      <div className="profile-strip">
        <div className="ps-cell"><div className="ps-l">Drafts prepared</div><div className="ps-v">{m.agent.prepared}</div></div>
        <div className="ps-cell"><div className="ps-l">Sent</div><div className="ps-v">{m.agent.sent}</div></div>
        <div className="ps-cell"><div className="ps-l">Consolidated into briefs</div><div className="ps-v">{m.agent.consolidated}</div></div>
        <div className="ps-cell"><div className="ps-l">Progressed with no person</div><div className="ps-v">{m.agent.noPerson}</div></div>
        <div className="ps-cell"><div className="ps-l">Hours to first contact</div><div className="ps-v">{m.agent.medianFirstContact}</div><div className="xs dim" style={{ marginTop: 2 }}>median</div></div>
      </div>
    </div>
  </div>;
}

/* ---------------- CHARTS ---------------- */
const GROUP_TONES = { info: '#8A92A3', underwriting: '#2267EA', pricing: '#9355D1', onboarding: '#E79800', offpath: '#A3ABBC', closed: 'var(--green-deep)' };
const STAGE_TONES = ['#A3ABBC', '#8A92A3', '#6E7481', '#5C6373', '#2267EA', '#1b59cf', '#E79800', '#9355D1', '#09BD87', '#0B8A64', '#0d6b52'];
function ChartsView({ m }) {
  const MD = window.MD;
  const maxWeek = Math.max.apply(null, m.weekly.map((w) => w.n)) || 1;
  const reqOff = m.requiredPerWeek > maxWeek;
  const plot = 96;
  const reqY = plot - Math.min(m.requiredPerWeek / maxWeek, 1) * plot;
  const maxParty = Math.max.apply(null, m.parties.map((p) => p.days)) || 1;
  const maxAdvPrem = Math.max.apply(null, m.top15.map((x) => x.premium)) || 1;
  const totalStages = m.groupDist.reduce((t, s) => t + s.n, 0) || 1;

  /* Two per row where width allows — minmax(360px) admitted a third column at 1180. */
  return <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(min(100%,460px),1fr))', gap: 12 }}>
    {/* 1 — closes per week */}
    <ChartCard title="Closes per week" caption={'Bars are renewals concluded each week. The dashed line is the ' + m.requiredPerWeek.toFixed(1) + ' a week needed to clear ' + m.open.length + ' open renewals by ' + MD.dl('payment') + '. The gap is the message.'}>
      <div style={{ position: 'relative', height: plot + 26 }}>
        <div style={{ position: 'absolute', left: 0, right: 0, top: Math.max(0, reqY), borderTop: '1px dashed var(--red)' }}>
          <span className="xs" style={{ position: 'absolute', right: 0, top: -14, color: 'var(--red-deep)', fontWeight: 700 }}>{m.requiredPerWeek.toFixed(1)}/wk needed{reqOff ? ' — above every week' : ''}</span>
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 5, height: plot, position: 'relative' }}>
          {m.weekly.map((w) => <div key={w.label} style={{ flex: 1, minWidth: 0 }} title={w.label + ': ' + w.n}>
            <div style={{ height: Math.max(2, (w.n / maxWeek) * plot), background: w.n >= m.requiredPerWeek ? 'var(--green-deep)' : 'var(--line-strong)', borderRadius: '2px 2px 0 0' }} />
          </div>)}
        </div>
        <div style={{ display: 'flex', gap: 5, marginTop: 5 }}>
          {m.weekly.map((w, i) => <div key={w.label} className="xs dim" style={{ flex: 1, minWidth: 0, textAlign: 'center' }}>{i % 2 === 0 ? i + 1 : ''}</div>)}
        </div>
      </div>
    </ChartCard>

    {/* 2 — retention against target */}
    <ChartCard title="Retention against target" caption={'Each bar runs 0–100%. The marker is the ' + Math.round(MD.CYCLE.target * 100) + '% goal. Projected assumes every overdue renewal lapses.'}>
      <div style={{ position: 'relative' }}>
        {[['Count-weighted', m.retCount, 'var(--blue)'], ['Premium-weighted', m.retPremium, 'var(--green-deep)'], ['Last cycle', MD.CYCLE.priorRate, 'var(--line-strong)'], ['Projected', m.retProjected, 'var(--red)']].map((x) => <div key={x[0]} style={{ marginBottom: 11 }}>
          <div className="row" style={{ gap: 8, marginBottom: 4 }}>
            <span className="xs" style={{ color: 'var(--text-2)', flex: 1 }}>{x[0]}</span>
            <span className="xs tab" style={{ fontWeight: 700, color: 'var(--text)' }}>{(x[1] * 100).toFixed(1)}%</span>
          </div>
          <Bar pct={x[1] * 100} tone={x[2]} h={9} />
        </div>)}
        <div style={{ position: 'absolute', left: Math.round(MD.CYCLE.target * 100) + '%', top: 14, bottom: 22, width: 1, background: 'var(--night)' }}>
          <span className="xs" style={{ position: 'absolute', bottom: -18, left: -14, color: 'var(--text)', fontWeight: 700 }}>{Math.round(MD.CYCLE.target * 100)}%</span>
        </div>
      </div>
    </ChartCard>

    {/* 3 — the book by stage */}
    <ChartCard wide title="The book by phase" caption={'All ' + m.all.length + ' renewals across the six phases. Eighteen stages is unreadable in a bar, so the chart groups them; the table on The season lists stages individually.'}>
      <div style={{ display: 'flex', height: 30, borderRadius: 'var(--r-sm)', overflow: 'hidden', border: '1px solid var(--line)' }}>
        {m.groupDist.map((s, i) => <div key={s.id} title={s.label + ': ' + s.n}
          style={{ width: (s.n / totalStages) * 100 + '%', background: GROUP_TONES[s.id] || STAGE_TONES[i % STAGE_TONES.length], borderRight: '1px solid rgba(255,255,255,.35)' }} />)}
      </div>
      <div className="wrap" style={{ marginTop: 12, gap: '6px 14px' }}>
        {m.groupDist.map((s, i) => <span key={s.id} className="row" style={{ gap: 6 }}>
          <i style={{ width: 9, height: 9, borderRadius: 2, background: GROUP_TONES[s.id] || STAGE_TONES[i % STAGE_TONES.length], display: 'block' }} />
          <span className="xs" style={{ color: 'var(--text-2)' }}>{s.label}</span>
          <span className="xs dim tab">{s.n}</span>
        </span>)}
      </div>
    </ChartCard>

    {/* 4 — days lost by owing party */}
    <ChartCard title="Days lost by owing party" caption="Total days every open condition has been waiting, grouped by who owes the next move. Internal means the delay is ours.">
      {m.parties.map((p) => <div key={p.party} style={{ marginBottom: 13 }}>
        <div className="row" style={{ gap: 8, marginBottom: 4 }}>
          <span className="xs" style={{ color: 'var(--text-2)', flex: 1 }}>{p.party}</span>
          <span className="xs dim">{p.n} conditions</span>
          <span className="xs tab" style={{ fontWeight: 700, color: 'var(--text)' }}>{p.days}d</span>
        </div>
        <Bar pct={(p.days / maxParty) * 100} tone={p.party === 'Internal' ? 'var(--amber)' : 'var(--blue)'} h={11} />
        <div className="xs dim" style={{ marginTop: 3 }}>{pctOf(p.days, m.totalStuckDays)}% of all days lost</div>
      </div>)}
    </ChartCard>

    {/* 5 — premium concentration */}
    <ChartCard wide title="Premium concentration by advisor" caption="Top fifteen advisors by prior-year book premium. The darker marker on each bar is SAP AUM on the same scale, so the two can be compared directly.">
      {m.top15.map((x) => {
        const maxSap = Math.max.apply(null, m.top15.map((y) => y.sap)) || 1;
        const scale = Math.max(maxAdvPrem, maxSap);
        return <div key={x.a.id} style={{ display: 'grid', gridTemplateColumns: '150px minmax(0,1fr) 96px 96px', gap: 10, alignItems: 'center', padding: '5px 0' }}>
          <span className="xs trunc" style={{ color: 'var(--text-2)' }}>{x.a.name}</span>
          <span style={{ position: 'relative', display: 'block' }}>
            <Bar pct={(x.premium / scale) * 100} tone="var(--blue)" h={11} />
            <i style={{ position: 'absolute', left: 'calc(' + (x.sap / scale) * 100 + '% - 1px)', top: -2, width: 2, height: 15, background: 'var(--night)', display: 'block' }} title={'SAP AUM ' + money(x.sap)} />
          </span>
          <span className="xs tab dim" style={{ textAlign: 'right' }}>{money(x.premium)}</span>
          <span className="xs tab dim" style={{ textAlign: 'right' }}>{money(x.sap)}</span>
        </div>;
      })}
      <div className="row" style={{ gap: 16, marginTop: 10 }}>
        <span className="row" style={{ gap: 6 }}><i style={{ width: 14, height: 8, borderRadius: 2, background: 'var(--blue)', display: 'block' }} /><span className="xs dim">Book premium</span></span>
        <span className="row" style={{ gap: 6 }}><i style={{ width: 2, height: 12, background: 'var(--night)', display: 'block' }} /><span className="xs dim">SAP AUM</span></span>
      </div>
    </ChartCard>
  </div>;
}

/* ---------------- shell ---------------- */
const M_TABS = [
  { id: 'book', label: 'The book', note: 'Outcome, retention and advisors — for the president' },
  { id: 'season', label: 'The season', note: 'Work, blockers, people and health — for the operations lead' },
  { id: 'charts', label: 'Charts', note: 'Visual summaries of both' },
];
function ManagementScreen() {
  const MD = window.MD;
  const [tab, setTab] = mS('book');
  const m = cycleModel();
  const active = M_TABS.find((t) => t.id === tab) || M_TABS[0];

  return <div className="ag-canvas wide">
    <div className="ph"><div className="ph-row"><div className="grow">
      <h1>Cycle overview</h1>
      <p className="sub">{MD.CYCLE.label} · {m.daysLeft} days to the payment deadline on {MD.dl('payment')}. The only screen on the cycle clock rather than today's.</p>
    </div><span className="ph-badge"><Icon n="building" s={14} />{m.all.length} renewals in cycle</span></div></div>

    <div className="row" style={{ gap: 8, marginBottom: 6, flexWrap: 'wrap' }}>
      {M_TABS.map((t) => <button key={t.id} className={'tabbtn' + (tab === t.id ? ' on' : '')} onClick={() => setTab(t.id)}>{t.label}</button>)}
    </div>
    <div className="sm mut" style={{ marginBottom: 20 }}>{active.note}</div>

    {tab === 'book' ? <BookView m={m} /> : tab === 'season' ? <SeasonView m={m} /> : <ChartsView m={m} />}
  </div>;
}

Object.assign(window, { ManagementScreen, BookView, SeasonView, ChartsView, cycleModel });
