create-pmos 0.124.0 → 0.126.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/briefing-cards.md +40 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/needs-overdue.js +41 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +109 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +413 -0
- package/workspace/public/pm/briefing-model.js +211 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +185 -0
- package/workspace/public/pm/c3-daypart.js +176 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +674 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +230 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1014 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1547 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +895 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/* pm/c3-cal-strategy.js — the Calendar's Quarter and Year panes of /c3.html, built on the console3 mockup's own markup and CSS
|
|
2
|
+
* (console3.html: strat 3065, calDeck 3174, calCards 3143, cf 3085, nsChart 2833, nsMini 2851, krCard 2856, sigCard 2884, synCard 2888,
|
|
3
|
+
* matCard 2894, betsCard 2907, progSVG 2716, stratCase 2919). The Quarter and the Year are two different panes:
|
|
4
|
+
* Quarter a deck of cards (the direction card first, which flips to its case file), the quarter's outcomes, the early signals, the
|
|
5
|
+
* north star, synergy and coverage, and delivery against promise
|
|
6
|
+
* Year the deck, maturity by pillar, bets and their checkpoints, the north star, the early signals, synergy and coverage
|
|
7
|
+
* mountQuarter(host, ctx) and mountYear(host, ctx) fill `host`; dispose(host) tears its case slides down. ctx = { data, today, goInit(id, title) }, where
|
|
8
|
+
* `data` is the GET /api/calendar-views answer the page already holds (its gantt rows feed Delivery against promise).
|
|
9
|
+
*
|
|
10
|
+
* DATA. GET /api/c3-strategy (workspace/lib/routes-c3-strategy.js) reads the KR metric, bet and hypothesis records and the signal rules. Demo mode
|
|
11
|
+
* (?demo=1) answers it from c3-demo-cal3.js with the mockup's own sample, and adds a deck, a lens line and a maturity table that no record holds.
|
|
12
|
+
* In real mode a card whose source is empty says what is missing and what would fill it; nothing is invented. The direction card's case file
|
|
13
|
+
* is built from the rules the repository already has (the 10 percent early-warning line, the three-check pivot rule), with no confidence figure,
|
|
14
|
+
* because none is calibrated; its slide stays disabled, because this page has no route that records a strategy decision. */
|
|
15
|
+
import { el } from './dom.js';
|
|
16
|
+
import { pmGet } from './http.js';
|
|
17
|
+
import { ensureC3Css, c3ic, c3ring } from './pm-work-view.js';
|
|
18
|
+
import { renderCase } from './c3-case.js';
|
|
19
|
+
import { makeXp, attachExpand } from './c3-expand.js';
|
|
20
|
+
import { toast } from './c3-toast.js';
|
|
21
|
+
import { TAG_STYLE, swipeDir } from './briefing-model.js';
|
|
22
|
+
|
|
23
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
24
|
+
const sv = (tag, attrs, ...kids) => { const n = document.createElementNS(NS, tag); for (const k of Object.keys(attrs || {})) if (attrs[k] != null) n.setAttribute(k, attrs[k]); for (const c of kids) if (c) n.appendChild(c); return n; };
|
|
25
|
+
const tx = (x, y, s, attrs) => { const n = sv('text', { x, y, ...(attrs || {}) }); n.textContent = s; return n; };
|
|
26
|
+
const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
+
const MN = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
28
|
+
const DAY = 864e5;
|
|
29
|
+
const utc = (s) => Date.parse(String(s).slice(0, 10) + 'T00:00:00Z');
|
|
30
|
+
const dstr = (t) => new Date(t).toISOString().slice(0, 10);
|
|
31
|
+
const keyLab = (k) => MN[Number(k.slice(5, 7)) - 1] + " '" + k.slice(2, 4);
|
|
32
|
+
const fmt = (v) => (v == null ? '' : Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
|
|
33
|
+
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
34
|
+
const isPhone = () => typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches;
|
|
35
|
+
/* On a phone the chart rows are 17px tall (a 640px chart in 366px), so each row that opens an initiative is also a 44px button under the chart (as calendar-views.js does). */
|
|
36
|
+
const phoneRows = (rows, label, go) => el('ul', { class: 'list', role: 'list', style: 'margin:0;list-style:none;padding:0', 'data-cs': 'phone-rows' }, rows.map((r) => el('li', null,
|
|
37
|
+
el('button', { type: 'button', class: 'nrow', style: 'width:100%;min-height:44px;border:0;background:none;text-align:left;display:flex;justify-content:space-between;gap:8px;align-items:center;cursor:pointer', 'aria-label': 'Open ' + r.title, onClick: () => go(r) },
|
|
38
|
+
el('b', null, r.title), el('span', { class: 'mono', style: 'font-size:var(--fs-2);color:' + (r.c || MFG) }, r.text)))));
|
|
39
|
+
const head3 = (icon, title, line) => el('h3', null, c3ic(icon), title + ' ', line ? el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, line) : null);
|
|
40
|
+
const OK = 'var(--success)'; const WARN = 'var(--warning)'; const BAD = 'var(--destructive)'; const ACC = 'var(--acc)'; const MFG = 'var(--muted-foreground)'; const PUR = 'var(--purple)';
|
|
41
|
+
|
|
42
|
+
/* ── the data: one read, kept for a minute ─────────────────────────────────────────────────────────────────────── */
|
|
43
|
+
let cache = null; let inflight = null;
|
|
44
|
+
const fresh = () => cache && Date.now() - cache.at < 60000;
|
|
45
|
+
function readModel(real) {
|
|
46
|
+
if (fresh()) return Promise.resolve(cache.model);
|
|
47
|
+
if (!inflight) {
|
|
48
|
+
inflight = Promise.all([pmGet('/api/c3-strategy'), real ? pmGet('/api/pm-items').catch(() => null) : null]).then(([s, it]) => {
|
|
49
|
+
inflight = null;
|
|
50
|
+
const model = s && s.ok ? { ...s, items: it && it.ok ? it : null } : { ok: false, error: (s && s.error) || 'not available' };
|
|
51
|
+
if (model.ok) cache = { at: Date.now(), model };
|
|
52
|
+
return model;
|
|
53
|
+
}, (e) => { inflight = null; return { ok: false, error: String(e && e.message || e) }; });
|
|
54
|
+
}
|
|
55
|
+
return inflight;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── what is decided on this page (kept in memory, as the mockup keeps S.cdone) ─────────────────────────────────── */
|
|
59
|
+
const decided = new Map();
|
|
60
|
+
const cases = new Set();
|
|
61
|
+
|
|
62
|
+
/* ── the north star: nsChart (2833) and nsMini (2851) ─────────────────────────────────────────────────────────────── */
|
|
63
|
+
function nsInfo(N) {
|
|
64
|
+
if (!N || !N.points || N.points.length < 2) return null;
|
|
65
|
+
const P = N.points; const nowI = Math.max(0, P.findIndex((p) => p.at === N.now));
|
|
66
|
+
const a = P[nowI] && P[nowI].actual; const pl = P[nowI] && P[nowI].plan;
|
|
67
|
+
const up = P[P.length - 1].plan >= P[0].plan; // an increasing target: behind means below the plan
|
|
68
|
+
const gap = (i) => { const p = P[i]; return p.proj == null || !p.plan ? null : (p.plan - p.proj) / Math.abs(p.plan) * (up ? 1 : -1); };
|
|
69
|
+
const first = (f) => { for (let i = nowI + 1; i < P.length; i++) { const g = gap(i); if (g !== null && g >= f) return i; } return -1; };
|
|
70
|
+
return { P, nowI, var: a != null && pl ? Math.round((a / pl - 1) * 100) : null, leaves: first(0.08), pivot: first(0.15), unit: N.unit || '' };
|
|
71
|
+
}
|
|
72
|
+
const nice = (range) => { const raw = range / 3; const m = Math.pow(10, Math.floor(Math.log10(raw || 1))); const f = raw / m; return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10) * m; };
|
|
73
|
+
function window_(I, view, today) {
|
|
74
|
+
const last = I.P.length - 1;
|
|
75
|
+
if (view === 'year') return [clamp(I.nowI - 6, 0, last), clamp(I.nowI + 11, 0, last)];
|
|
76
|
+
const q = Math.floor((Number(today.slice(5, 7)) - 1) / 3); const qe = today.slice(0, 4) + '-' + String(q * 3 + 3).padStart(2, '0');
|
|
77
|
+
const ei = I.P.findIndex((p) => p.at === qe);
|
|
78
|
+
return [clamp(I.nowI - 3, 0, last), ei >= 0 ? ei : clamp(I.nowI + 2, 0, last)];
|
|
79
|
+
}
|
|
80
|
+
function nsChart(N, I, i0, i1, H) {
|
|
81
|
+
H = H || 190; const W = 560; const x0 = 34; const x1 = W - 14; const yt = 14; const yb = H - 26;
|
|
82
|
+
const vals = []; for (let i = i0; i <= i1; i++) { const p = I.P[i]; [p.plan && p.plan * 1.08, p.plan && p.plan * 0.92, p.actual, p.proj].forEach((v) => { if (v != null) vals.push(v); }); }
|
|
83
|
+
let mn = Math.min(...vals); let mx = Math.max(...vals); const pad = (mx - mn) * 0.06 || 1; mn -= pad; mx += pad;
|
|
84
|
+
const step = nice(mx - mn); let ticks = []; for (let v = Math.ceil(mn / step) * step; v <= mx + 1e-9; v += step) ticks.push(Math.round(v * 100) / 100);
|
|
85
|
+
const ax = N.axis && N.axis[i1 - i0 < 8 ? 'quarter' : 'year']; if (ax) { mn = ax.min; mx = ax.max; ticks = ax.ticks; } // the demo's sample carries the mockup's own axis
|
|
86
|
+
const X = (i) => x0 + (i - i0) / Math.max(1, i1 - i0) * (x1 - x0); const Y = (v) => yb - (v - mn) / (mx - mn) * (yb - yt);
|
|
87
|
+
const s = sv('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': (N.label || 'North star') + ': actual, plan with a tolerance band, and projection at the current slope' });
|
|
88
|
+
ticks.forEach((v) => s.append(sv('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' }), tx(x0 - 5, Y(v) + 4, fmt(v) + I.unit, { 'text-anchor': 'end' })));
|
|
89
|
+
const idx = []; for (let i = i0; i <= i1; i++) if (I.P[i].plan != null) idx.push(i);
|
|
90
|
+
s.append(sv('polygon', { points: idx.map((i) => X(i).toFixed(1) + ',' + Y(I.P[i].plan * 1.08).toFixed(1)).concat(idx.slice().reverse().map((i) => X(i).toFixed(1) + ',' + Y(I.P[i].plan * 0.92).toFixed(1))).join(' '), fill: 'var(--accw)' }),
|
|
91
|
+
sv('polyline', { points: idx.map((i) => X(i).toFixed(1) + ',' + Y(I.P[i].plan).toFixed(1)).join(' '), fill: 'none', stroke: 'var(--ring)', 'stroke-width': 2, 'stroke-dasharray': '5 4' }));
|
|
92
|
+
const act = []; for (let i = i0; i <= Math.min(I.nowI, i1); i++) if (I.P[i].actual != null) act.push(X(i).toFixed(1) + ',' + Y(I.P[i].actual).toFixed(1));
|
|
93
|
+
if (act.length > 1) s.append(sv('polyline', { class: 'draw', pathLength: 1, points: act.join(' '), fill: 'none', stroke: ACC, 'stroke-width': 3, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
|
|
94
|
+
const pj = []; for (let i = Math.max(i0, I.nowI); i <= i1; i++) if (I.P[i].proj != null) pj.push(X(i).toFixed(1) + ',' + Y(I.P[i].proj).toFixed(1));
|
|
95
|
+
if (pj.length > 1) s.append(sv('polyline', { points: pj.join(' '), fill: 'none', stroke: WARN, 'stroke-width': 3, 'stroke-dasharray': '2 5', 'stroke-linecap': 'round' }));
|
|
96
|
+
if (I.nowI >= i0 && I.nowI <= i1) {
|
|
97
|
+
s.append(sv('line', { x1: X(I.nowI), x2: X(I.nowI), y1: yt, y2: yb, stroke: 'var(--foreground)', 'stroke-width': 1.5 }));
|
|
98
|
+
if (I.P[I.nowI].actual != null) s.append(sv('circle', { cx: X(I.nowI), cy: Y(I.P[I.nowI].actual), r: 5, fill: ACC, stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
99
|
+
s.append(tx(X(I.nowI) - 6, yt + 8, 'now', { 'text-anchor': 'end', class: 't' }));
|
|
100
|
+
}
|
|
101
|
+
if (I.leaves > 0 && I.leaves <= i1) s.append(sv('circle', { cx: X(I.leaves), cy: Y(I.P[I.leaves].proj), r: 6, fill: WARN, stroke: 'var(--card)', 'stroke-width': 2 }), tx(X(I.leaves) - 8, Y(I.P[I.leaves].proj) + 22, 'leaves band', { 'text-anchor': X(I.leaves) > x1 - 60 ? 'end' : 'middle', style: 'fill:' + WARN }));
|
|
102
|
+
if (I.pivot > 0 && I.pivot <= i1) s.append(sv('circle', { cx: X(I.pivot), cy: Y(I.P[I.pivot].proj), r: 6, fill: BAD, stroke: 'var(--card)', 'stroke-width': 2 }), tx(X(I.pivot), Y(I.P[I.pivot].proj) + 22, 'pivot line', { 'text-anchor': 'middle', style: 'fill:' + BAD }));
|
|
103
|
+
const lstep = i1 - i0 > 8 ? 3 : 1; for (let i = i0; i <= i1; i += lstep) s.append(tx(X(i), H - 8, keyLab(I.P[i].at), { 'text-anchor': 'middle' }));
|
|
104
|
+
return [s, el('div', { class: 'pgl' }, el('span', { style: 'color:' + ACC }, '▬ actual'), el('span', null, '┅ plan, ±8% band'), el('span', { style: 'color:' + WARN }, '┅ if the slope holds'), el('span', { style: 'color:' + BAD }, '● pivot line, 15% behind'))];
|
|
105
|
+
}
|
|
106
|
+
function nsMini(I) {
|
|
107
|
+
const W = 340; const H = 96; const x0 = 6; const x1 = W - 6; const [i0, i1] = [Math.max(0, I.nowI - 6), Math.min(I.P.length - 1, I.nowI + 11)];
|
|
108
|
+
const vs = []; for (let i = i0; i <= i1; i++) [I.P[i].plan, I.P[i].actual, I.P[i].proj].forEach((v) => { if (v != null) vs.push(v); });
|
|
109
|
+
const mn = Math.min(...vs); const mx = Math.max(...vs) || 1;
|
|
110
|
+
const X = (i) => x0 + (i - i0) / Math.max(1, i1 - i0) * (x1 - x0); const Y = (v) => H - 14 - (v - mn) / (mx - mn || 1) * (H - 26);
|
|
111
|
+
const line = (f, a, b) => { const o = []; for (let i = a; i <= b; i++) { const v = f(I.P[i]); if (v != null) o.push(X(i) + ',' + Y(v)); } return o.join(' '); };
|
|
112
|
+
const s = sv('svg', { class: 'nsm', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'North star against plan' },
|
|
113
|
+
sv('polyline', { points: line((p) => p.plan, i0, i1), fill: 'none', stroke: 'var(--ring)', 'stroke-width': 2, 'stroke-dasharray': '4 3' }),
|
|
114
|
+
sv('polyline', { points: line((p) => p.actual, i0, I.nowI), fill: 'none', stroke: ACC, 'stroke-width': 3, 'stroke-linecap': 'round' }),
|
|
115
|
+
sv('polyline', { points: line((p) => p.proj, I.nowI, i1), fill: 'none', stroke: WARN, 'stroke-width': 3, 'stroke-dasharray': '2 5', 'stroke-linecap': 'round' }));
|
|
116
|
+
if (I.P[I.nowI].actual != null) s.append(sv('circle', { cx: X(I.nowI), cy: Y(I.P[I.nowI].actual), r: 5, fill: ACC, stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
117
|
+
const lp = I.P[i1]; s.append(tx(X(I.nowI) - 6, H - 2, 'now', { 'text-anchor': 'end' }));
|
|
118
|
+
if (lp.plan != null) s.append(tx(x1, Y(lp.plan) - 6, 'plan', { 'text-anchor': 'end' }));
|
|
119
|
+
if (lp.proj != null) s.append(tx(x1, Y(lp.proj) + 14, "at today's pace", { 'text-anchor': 'end', style: 'fill:' + WARN }));
|
|
120
|
+
return s;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── empty states: the card is drawn, says what is missing and what fills it ─────────────────────────────────────── */
|
|
124
|
+
const empty = (icon, title, line, why, fill, key) => el('section', { class: 'card cs-empty', 'data-cs': key, 'data-cs-empty': 'true' }, head3(icon, title, line), el('div', { class: 'fold' }, c3ic('moon', 'sm'), el('span', null, why + ' ' + fill)));
|
|
125
|
+
|
|
126
|
+
/* ── cards ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
127
|
+
const KST = { ok: { l: 'On course', c: OK }, drift: { l: 'Drifting', c: WARN }, off: { l: 'Off course', c: BAD }, unscored: { l: 'Not scored', c: MFG } };
|
|
128
|
+
|
|
129
|
+
function northCard(M, view, today, bare) {
|
|
130
|
+
const I = nsInfo(M.north);
|
|
131
|
+
if (!I) return empty('up', 'North star', '', 'No north-star metric is recorded: no KR metric record is marked north_star with readings.', 'Add a KR metric in Initiatives > Signals, mark it as the north star and record its readings; the plan, the tolerance band and the slope are drawn from them.', 'north');
|
|
132
|
+
const [i0, i1] = window_(I, view, today);
|
|
133
|
+
const [s, leg] = i1 - i0 < 1 ? [el('div', { class: 'fold' }, c3ic('info', 'sm'), 'Fewer than two months of plan are in the window.'), null] : nsChart(M.north, I, i0, i1);
|
|
134
|
+
return el('section', { class: 'card', 'data-cs': 'north' }, bare ? head3('up', 'North star') : head3('up', 'North star: ' + (M.north.label || '').toLowerCase(), I.var == null ? 'no reading yet' : I.var + '% against plan today'), s, leg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function outcomesCard(M) {
|
|
138
|
+
if (!M.outcomes.length) return empty('target', 'Quarter outcomes', 'where we are, where we should be, where we land', 'No KR metric is recorded, so no outcome is scored.', 'Add one in Initiatives > Signals: a metric, a baseline, a target and a by date, then a reading.', 'outcomes');
|
|
139
|
+
const pos = (k, v) => clamp((v - k.start) / (k.target - k.start) * 100, 0, 104);
|
|
140
|
+
const rows = M.outcomes.map((k) => {
|
|
141
|
+
const S = KST[k.status] || KST.unscored; const u = k.unit || '';
|
|
142
|
+
if (k.status === 'unscored') return el('div', { class: 'krg', style: '--c:' + S.c, 'data-cs-kr': k.id, 'data-status': 'unscored' }, el('div', null, el('b', null, k.name), el('small', null, k.pillar || 'not scored')), el('div', null, el('small', null, k.reason || 'Not scored.')), el('div', { class: 'chip-s', style: '--c:' + S.c }, S.l));
|
|
143
|
+
return el('div', { class: 'krg', style: '--c:' + S.c, 'data-cs-kr': k.id, 'data-status': k.status },
|
|
144
|
+
el('div', null, el('b', null, k.name), el('small', null, k.pillar || '')),
|
|
145
|
+
el('div', null, el('div', { class: 'krt' }, el('i', { style: 'width:' + pos(k, k.now) + '%' }), el('u', { style: 'left:' + pos(k, k.expected) + '%', title: 'Where it should be today' }), k.forecast != null ? el('s', { style: 'left:' + pos(k, k.forecast) + '%', title: 'Forecast landing' }) : null),
|
|
146
|
+
el('small', null, `now ${fmt(k.now)}${u} · should be ${fmt(k.expected)}${u} · target ${fmt(k.target)}${u}`), k.lead ? el('small', null, k.lead) : null),
|
|
147
|
+
el('div', { class: 'chip-s', style: '--c:' + S.c + ';flex-direction:column;align-items:flex-end' }, S.l, el('small', null, k.forecast != null ? 'lands ' + fmt(k.forecast) + u : 'no forecast yet')));
|
|
148
|
+
});
|
|
149
|
+
return el('section', { class: 'card', 'data-cs': 'outcomes' }, head3('target', 'Quarter outcomes', 'where we are, where we should be, where we land'), el('div', null, rows),
|
|
150
|
+
el('div', { class: 'pgl' }, el('span', null, '▬ now'), el('span', null, '| should be today'), el('span', null, '◌ forecast landing')));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const SEV = { watch: { l: 'Watch', c: ACC, i: 'eye' }, drift: { l: 'Drift', c: WARN, i: 'clock' }, pivot: { l: 'Pivot', c: BAD, i: 'alert' }, up: { l: 'Upside', c: OK, i: 'up' } };
|
|
154
|
+
const SEVRANK = ['pivot', 'drift', 'watch', 'up'];
|
|
155
|
+
function spark(a, c) {
|
|
156
|
+
if (!a || a.length < 2) return null;
|
|
157
|
+
const W = 64; const H = 26; const mn = Math.min(...a); const mx = Math.max(...a);
|
|
158
|
+
const X = (i) => 3 + i * (W - 6) / (a.length - 1); const Y = (v) => H - 4 - (v - mn) / (mx - mn || 1) * (H - 8);
|
|
159
|
+
return sv('svg', { class: 'spk', viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true', style: 'width:64px;height:26px' }, sv('polyline', { points: a.map((v, i) => X(i) + ',' + Y(v)).join(' '), fill: 'none', stroke: c, 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }), sv('circle', { cx: X(a.length - 1), cy: Y(a[a.length - 1]), r: 3, fill: c }));
|
|
160
|
+
}
|
|
161
|
+
/* the case file of a signal: the mockup's own options in demo; in real mode the rule's evidence and no option, because no option is recorded */
|
|
162
|
+
function caseOfSignal(s, live) {
|
|
163
|
+
if (s.case) return s.case;
|
|
164
|
+
return { sit: s.ev, chg: [['target', s.thr, s.source || 'rule', s.sev === 'up' ? 'ok' : 'warn'], s.ttt ? ['clock', s.ttt, 'timing'] : null].filter(Boolean), opts: [],
|
|
165
|
+
unsure: live ? 'No option is recorded for this signal. Decide in your terminal, then record what you did.' : '' };
|
|
166
|
+
}
|
|
167
|
+
function signalsCard(M, n, ctx, live) {
|
|
168
|
+
const L = M.signalsAll.slice().sort((a, b) => SEVRANK.indexOf(a.sev) - SEVRANK.indexOf(b.sev)).slice(0, n);
|
|
169
|
+
if (!L.length) return empty('alert', 'Early signals', 'before the numbers say it', 'No signal is open: no signal rule has tripped, no KR metric is behind or ahead of its line, and no initiative is late.', 'Set a rule in Initiatives > Signals, or record a KR metric reading.', 'signals');
|
|
170
|
+
const rows = L.map((s) => {
|
|
171
|
+
const V = SEV[s.sev] || SEV.watch; const k = 'sig:' + s.id; const done = decided.get(k);
|
|
172
|
+
const xp = makeXp(caseNode(k, caseOfSignal(s, live), live, ctx));
|
|
173
|
+
const row = el('li', { class: 'sgr', style: '--c:' + V.c + (done ? ';opacity:.55' : ''), 'data-cs-sig': s.id, 'data-sev': s.sev },
|
|
174
|
+
el('span', { class: 'ki', style: 'color:' + V.c }, c3ic(V.i)),
|
|
175
|
+
el('div', { style: 'min-width:0' }, el('div', { class: 'st1' }, s.t), el('div', { class: 'st2' }, s.ev),
|
|
176
|
+
el('div', { class: 'st3' }, el('span', { class: 'sv' }, V.l), el('span', null, c3ic('target', 'sm'), s.thr), s.ttt ? el('span', null, c3ic('clock', 'sm'), s.ttt) : null), xp), spark(s.sp, V.c));
|
|
177
|
+
attachExpand(row, xp, { key: 'sg-' + s.id });
|
|
178
|
+
return row;
|
|
179
|
+
});
|
|
180
|
+
return el('section', { class: 'card', 'data-cs': 'signals' }, head3('alert', 'Early signals', 'before the numbers say it'), el('ul', { class: 'sgl', style: 'list-style:none;margin:0;padding:0' }, rows));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function synergyCard(M) {
|
|
184
|
+
const S = M.synergy;
|
|
185
|
+
if (!S || !S.rows.length) return empty('init', 'Synergy and coverage', 'do the bets add up to the strategy', 'No bet is recorded, so there is nothing to cover the outcomes with.', 'Add a bet in Initiatives > Roadmap and name the KR metric it serves (parent_kr).', 'synergy');
|
|
186
|
+
if (!S.columns.length) return empty('init', 'Synergy and coverage', 'do the bets add up to the strategy', S.rows.length + ' bet' + (S.rows.length === 1 ? ' is' : 's are') + ' recorded, but no KR metric exists to measure them against.', 'Add a KR metric in Initiatives > Signals.', 'synergy');
|
|
187
|
+
const cov = S.columns.map((_, j) => S.rows.filter((r) => r.weights[j] > 0 && !r.parked).length);
|
|
188
|
+
const cells = [el('span'), ...S.columns.map((p) => el('span', { class: 'h', title: p.n }, c3ic(p.icon || 'target', 'sm'), p.n))];
|
|
189
|
+
S.rows.forEach((r) => {
|
|
190
|
+
cells.push(el('span', { class: 'l' }, r.title, r.parked ? [' ', el('span', { class: 'dim' }, 'parked')] : null));
|
|
191
|
+
r.weights.forEach((v) => cells.push(el('span', { class: 'c' }, Array.from({ length: Math.min(3, v) }, () => el('i', { class: r.parked ? 'o' : '' })))));
|
|
192
|
+
});
|
|
193
|
+
cells.push(el('span', { class: 'l dim' }, 'Active bets'), ...cov.map((n) => el('span', { class: 'c' + (n ? '' : ' gap') }, n ? n + ' active' : 'no bet')));
|
|
194
|
+
const notes = (S.notes || []).map((r) => el('li', { class: r.tone || '' }, c3ic(r.icon || 'info', 'sm'), el('span', null, r.text)));
|
|
195
|
+
return el('section', { class: 'card', 'data-cs': 'synergy' }, head3('init', 'Synergy and coverage', 'do the bets add up to the strategy'),
|
|
196
|
+
el('div', { class: 'syn', style: 'grid-template-columns:minmax(0,1.3fr) repeat(' + S.columns.length + ',minmax(0,1fr))' }, cells), notes.length ? el('ul', { class: 'chgl' }, notes) : null);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function maturityCard(M) {
|
|
200
|
+
const T = M.maturity;
|
|
201
|
+
if (!T) return empty('flag', 'Maturity by pillar', 'where each pillar stands', 'No pillar maturity is recorded: this repository holds no record of pillars, their stages or their criteria.', 'It fills when a maturity model is recorded (stage criteria per pillar and a target date).', 'maturity');
|
|
202
|
+
const W = 560; const LW = 96; const RW = 150; const CW = W - LW - RW; const rh = 48; const top = 26; const H = top + T.pillars.length * rh + 6; const X = (n) => LW + (n - 1) / 4 * CW;
|
|
203
|
+
const s = sv('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Product maturity by pillar: now, target by ' + T.target_label + ', and projection at the current pace' });
|
|
204
|
+
T.stages.forEach((n, i) => s.append(tx(X(i + 1), 14, n, { 'text-anchor': 'middle' })));
|
|
205
|
+
T.pillars.forEach((p, r) => {
|
|
206
|
+
const y = top + r * rh + rh / 2; const gap = p.proj < p.tgt; const c = gap ? (p.proj < p.now + 1 && p.tgt - p.proj > 1 ? BAD : WARN) : OK;
|
|
207
|
+
s.append(tx(0, y + 4, p.n, { class: 't' }), sv('line', { x1: X(1), x2: X(5), y1: y, y2: y, stroke: 'var(--border)', 'stroke-width': 3, 'stroke-linecap': 'round' }), sv('line', { x1: X(1), x2: X(p.now), y1: y, y2: y, stroke: ACC, 'stroke-width': 3, 'stroke-linecap': 'round' }));
|
|
208
|
+
if (p.proj > p.now) s.append(sv('line', { x1: X(p.now), x2: X(p.proj), y1: y, y2: y, stroke: c, 'stroke-width': 3, 'stroke-dasharray': '2 5', 'stroke-linecap': 'round' }));
|
|
209
|
+
for (let k = 1; k <= 5; k++) s.append(sv('circle', { cx: X(k), cy: y, r: k === p.now ? 7 : 4, fill: k <= p.now ? ACC : 'var(--card)', stroke: k <= p.now ? ACC : 'var(--ring)', 'stroke-width': 2 }));
|
|
210
|
+
s.append(sv('circle', { cx: X(p.tgt), cy: y, r: 11, fill: 'none', stroke: 'var(--foreground)', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' }), sv('path', { d: `M${X(p.proj)} ${y - 9} l7 9 -7 9 -7 -9z`, fill: c, stroke: 'var(--card)', 'stroke-width': 2 }),
|
|
211
|
+
tx(W - RW + 14, y - 3, gap ? 'Short by ' + (p.tgt - p.proj) + ' stage' + (p.tgt - p.proj > 1 ? 's' : '') : 'On course', { style: 'fill:' + c + ';font-weight:500' }), tx(W - RW + 14, y + 13, p.ev));
|
|
212
|
+
});
|
|
213
|
+
return el('section', { class: 'card', 'data-cs': 'maturity' }, head3('flag', 'Maturity by pillar', 'where each pillar stands against ' + T.target_label), s,
|
|
214
|
+
el('div', { class: 'pgl' }, el('span', { style: 'color:' + ACC }, '● reached'), el('span', null, '◌ target, ' + T.target_label), el('span', null, "◆ projected at today's pace")),
|
|
215
|
+
el('ul', { class: 'chgl' }, T.pillars.filter((p) => p.proj < p.tgt).map((p) => el('li', { class: 'warn' }, c3ic('target', 'sm'), el('span', null, p.n + ': next stage needs ' + p.nx + '.')))));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const BCOL = { late: BAD, risk: WARN, park: MFG, prop: PUR, ok: OK, none: ACC };
|
|
219
|
+
function betsCard(M, ctx) {
|
|
220
|
+
const T = M.timeline;
|
|
221
|
+
if (!T || !T.rows.length) return empty('flag', 'Bets and their checkpoints', 'each bet has a date to persevere or pivot', T && T.checkpoints.length ? 'No bet is recorded, so no bar is drawn.' : 'No bet and no checkpoint is recorded.', 'Add a bet in Initiatives > Roadmap (now, next or later), and a hypothesis with a review_by date for each checkpoint.', 'bets');
|
|
222
|
+
const W = 560; const LW = 170; const CW = (W - LW) / 4; const rh = 32; const top = 36; const H = top + T.rows.length * rh + 28;
|
|
223
|
+
const s = sv('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Bets by quarter with their status and review points' });
|
|
224
|
+
T.quarters.forEach((q, i) => s.append(tx(LW + i * CW + CW / 2, 14, q, { 'text-anchor': 'middle', class: i === 0 ? 't' : '' }), sv('line', { x1: LW + i * CW, x2: LW + i * CW, y1: 22, y2: H - 26, stroke: 'var(--border)' })));
|
|
225
|
+
const gantt = (ctx.data && ctx.data.gantt) || [];
|
|
226
|
+
T.rows.forEach((b, r) => {
|
|
227
|
+
const y = top + r * rh; const g = b.initiative && gantt.find((x) => x.id === b.initiative);
|
|
228
|
+
const st = g ? ({ 'on time': 'ok', 'at risk': 'risk', delayed: 'late', overdue: 'late' }[g.status] || b.st || 'none') : (b.st || 'none'); const col = BCOL[st] || ACC;
|
|
229
|
+
const t = b.title.length > 22 ? b.title.slice(0, 21) + '…' : b.title;
|
|
230
|
+
const grp = sv('g', b.initiative && !isPhone() ? { role: 'button', tabindex: '0', style: 'cursor:pointer', 'aria-label': 'Open ' + b.title, 'data-cs-bet': b.id } : { 'data-cs-bet': b.id });
|
|
231
|
+
grp.append(sv('rect', { x: 0, y, width: W, height: rh, fill: 'transparent' }), tx(0, y + 14, t, { class: 't' }), tx(0, y + 27, b.sub || '', { style: 'font-size:var(--fs-1)' }));
|
|
232
|
+
b.cells.forEach((v, i) => {
|
|
233
|
+
if (!v) return; const x = LW + i * CW + 4; const w = CW - 8;
|
|
234
|
+
if (v === 'c') grp.append(sv('rect', { x, y: y + 6, width: w, height: 16, rx: 8, fill: col, 'fill-opacity': i === 0 ? 0.95 : 0.55 }));
|
|
235
|
+
else grp.append(sv('rect', { x, y: y + 6, width: w, height: 16, rx: 8, fill: col, 'fill-opacity': 0.1, stroke: col, 'stroke-dasharray': '4 3' }), tx(x + w / 2, y + 18, v === 'a' ? 'proposed' : v === 'l' ? 'later' : 'parked', { 'text-anchor': 'middle', style: v === 'a' ? 'fill:' + PUR : '' }));
|
|
236
|
+
});
|
|
237
|
+
if (b.initiative && !isPhone()) { const open = () => ctx.goInit(b.initiative, b.title); grp.addEventListener('click', open); grp.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }); }
|
|
238
|
+
s.append(grp);
|
|
239
|
+
});
|
|
240
|
+
T.checkpoints.forEach((c) => { const x = LW + c.q * CW + c.f * CW; const y = H - 16; s.append(sv('path', { d: `M${x} ${y - 8} l6 7 -6 7 -6 -7z`, fill: 'var(--card)', stroke: 'var(--foreground)', 'stroke-width': 2 }, sv('title', {}))); s.lastChild.firstChild.textContent = c.label; });
|
|
241
|
+
const cps = T.checkpoints;
|
|
242
|
+
return el('section', { class: 'card', 'data-cs': 'bets' }, head3('flag', 'Bets and their checkpoints', 'each bet has a date to persevere or pivot'), s,
|
|
243
|
+
isPhone() ? phoneRows(T.rows.filter((b) => b.initiative).map((b) => ({ title: b.title, text: b.sub || '', id: b.initiative })), 'Open', (r) => ctx.goInit(r.id, r.title)) : null,
|
|
244
|
+
el('div', { class: 'pgl' }, cps.length ? cps.map((c, i) => el('span', null, (i === 0 ? '◇ checkpoints to persevere or pivot: ' : '') + c.label)) : el('span', null, '◇ no checkpoint is recorded: a hypothesis with a review_by date adds one')));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ── delivery against promise: progSVG (2716), quarter mode, from the day numbers of the range ───────────────────────── */
|
|
248
|
+
const ST = { done: { l: 'Done', c: OK }, ok: { l: 'On time', c: OK }, risk: { l: 'At risk', c: WARN }, late: { l: 'Delayed', c: BAD }, over: { l: 'Overdue', c: BAD }, park: { l: 'Parked', c: MFG } };
|
|
249
|
+
const SEVR = { over: 0, late: 1, risk: 2, ok: 3, park: 4, done: 5 };
|
|
250
|
+
const STKEY = { 'on time': 'ok', 'at risk': 'risk', delayed: 'late', overdue: 'over' };
|
|
251
|
+
function deliveryRows(M, ctx, range, live) {
|
|
252
|
+
const day = (s) => Math.round((utc(s) - utc(range.start)) / DAY) + 1;
|
|
253
|
+
const today = ctx.today || dstr(Date.now());
|
|
254
|
+
if (M.delivery && M.delivery.rows) return M.delivery.rows.map((r) => ({ ...r, start: r.start ? day(r.start) : null, due: day(r.due), fc: r.fc ? day(r.fc) : null }));
|
|
255
|
+
const out = [];
|
|
256
|
+
((ctx.data && ctx.data.gantt) || []).forEach((g) => {
|
|
257
|
+
const st = STKEY[g.status]; if (!st || !g.promised) return;
|
|
258
|
+
const f = g.forecast && (g.forecast.end || g.forecast.start); const p = g.planned && g.planned.start;
|
|
259
|
+
out.push({ id: g.id, n: g.title || g.id, kind: 'init', start: day(p || g.promised), due: day(g.promised), fc: f ? day(f) : day(g.promised), pct: typeof g.progress === 'number' ? g.progress : null, st,
|
|
260
|
+
why: (g.title || g.id) + ': ' + g.status + (g.slip_days > 0 ? ', ' + g.slip_days + ' days past the plan' : ''), late: st === 'over' ? Math.max(1, Math.round((utc(today) - utc(g.promised)) / DAY)) : 0, go: g.id });
|
|
261
|
+
});
|
|
262
|
+
if (live && M.items && M.items.lists) {
|
|
263
|
+
const seen = new Set();
|
|
264
|
+
Object.values(M.items.lists).forEach((l) => (l || []).forEach((i) => {
|
|
265
|
+
if (!i || i.done || seen.has(i.id) || !/^\d{4}-\d{2}-\d{2}/.test(String(i.due || ''))) return; seen.add(i.id);
|
|
266
|
+
const d = String(i.due).slice(0, 10); const over = d < today;
|
|
267
|
+
out.push({ id: 'item-' + i.id, n: i.title, kind: 'task', due: day(d), st: over ? 'over' : 'ok', why: i.title + (over ? ', overdue' : ', due ' + d), late: over ? Math.round((utc(today) - utc(d)) / DAY) : 0 });
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
function deliveryCard(M, ctx, live) {
|
|
273
|
+
const demo = !live;
|
|
274
|
+
const range = ctx.data && ctx.data.range;
|
|
275
|
+
const head = head3('target', 'Delivery against promise');
|
|
276
|
+
if (!range) return el('section', { class: 'card', 'data-cs': 'delivery' }, head, el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The calendar range is not read yet.'));
|
|
277
|
+
const a = 1; const b = Math.round((utc(range.end) - utc(range.start)) / DAY) + 1;
|
|
278
|
+
const today = ctx.today || dstr(Date.now()); const TD = Math.round((utc(today) - utc(range.start)) / DAY) + 1;
|
|
279
|
+
const all = deliveryRows(M, ctx, range, live).filter((c) => { const lo = c.kind === 'init' ? c.start : c.due; const hi = Math.max(c.due, c.fc || c.due); return (c.st === 'over' && TD >= a && TD <= b) || (hi >= a && lo <= b); })
|
|
280
|
+
.sort((x, y) => SEVR[x.st] - SEVR[y.st] || x.due - y.due);
|
|
281
|
+
if (!all.length) return el('section', { class: 'card cs-empty', 'data-cs': 'delivery', 'data-cs-empty': 'true' }, head, el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date in this quarter. Give an initiative a promised date and a plan, and it appears here with its forecast.'));
|
|
282
|
+
const R = all.slice(0, 8); const phone = isPhone();
|
|
283
|
+
const W = 640; const LW = 130; const SW = 96; const CW = W - LW - SW; const N = b - a + 1; const dw = CW / N; const rh = 30; const top = 28; const H = top + R.length * rh + 8;
|
|
284
|
+
const Xl = (n) => LW + (n - a) * dw; const Xc = (n) => Xl(n) + dw / 2;
|
|
285
|
+
const dlab = (n) => { const d = new Date(utc(range.start) + (n - 1) * DAY); return d.getUTCDate() + ' ' + MN[d.getUTCMonth()]; };
|
|
286
|
+
const s = sv('svg', { class: 'pg', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Progress of commitments: on time, at risk, delayed and overdue' }, sv('defs', {}, sv('clipPath', { id: 'csc' + range.start }, sv('rect', { x: LW, y: 18, width: CW, height: H - 18 }))));
|
|
287
|
+
for (let n = a; n <= b; n++) { const d = new Date(utc(range.start) + (n - 1) * DAY); if (d.getUTCDate() === 1) s.append(sv('line', { x1: Xl(n), x2: Xl(n), y1: 18, y2: H - 4, stroke: 'var(--border)' }), tx(Xl(n) + 3, 14, MN[d.getUTCMonth()])); }
|
|
288
|
+
if (TD >= a && TD <= b) { const now = new Date(); const fr = clamp((now.getHours() + now.getMinutes() / 60 - 8) / 10, 0, 1); const tX = Xl(TD) + dw * fr; s.append(sv('rect', { x: Xl(TD), y: 20, width: dw, height: H - 24, fill: 'var(--accw)', rx: 6 }), sv('line', { x1: tX, x2: tX, y1: 20, y2: H - 4, stroke: 'var(--foreground)', 'stroke-width': 2 })); }
|
|
289
|
+
const stat = (c) => (c.st === 'over' ? 'Overdue ' + (c.late || TD - c.due) + ' d' : c.st === 'late' ? 'Delayed +' + (c.fc - c.due) + ' d' : ST[c.st].l);
|
|
290
|
+
R.forEach((c, i) => {
|
|
291
|
+
const y = top + i * rh; const cy = y + rh / 2; const S = ST[c.st]; const nm = c.n.length > 17 ? c.n.slice(0, 16) + '…' : c.n; const dim = c.st === 'done' || c.st === 'park';
|
|
292
|
+
const g = sv('g', { opacity: dim ? 0.6 : 1, 'data-cs-row': c.id, 'data-status': c.st, ...(c.go && !phone ? { role: 'button', tabindex: '0', 'data-a': 'goinit', 'data-k': String(c.go), 'aria-label': 'Open ' + c.n + ': ' + stat(c), style: 'cursor:pointer' } : {}) });
|
|
293
|
+
g.append(sv('rect', { x: 0, y, width: W, height: rh, fill: 'transparent' }), tx(0, cy + 4, nm, { class: 't' }));
|
|
294
|
+
if (c.kind === 'init') {
|
|
295
|
+
const xs = Xl(c.start); const xe = Xc(c.due); const has = c.pct != null; const xf = xs + (xe - xs) * (c.pct || 0); const ex = clamp((TD - c.start) / Math.max(1, c.due - c.start), 0, 1); const xt = xs + (xe - xs) * ex;
|
|
296
|
+
const bars = sv('g', { 'clip-path': `url(#csc${range.start})` }, sv('rect', { x: xs, y: cy - 5, width: Math.max(2, xe - xs), height: 10, rx: 5, fill: S.c, 'fill-opacity': 0.2 }));
|
|
297
|
+
if (has) bars.append(sv('rect', { x: xs, y: cy - 5, width: Math.max(2, xf - xs), height: 10, rx: 5, fill: S.c }));
|
|
298
|
+
if (c.st !== 'park') bars.append(sv('line', { x1: xt, x2: xt, y1: cy - 9, y2: cy + 9, stroke: 'var(--foreground)', 'stroke-width': 2 }));
|
|
299
|
+
if (c.fc > c.due) bars.append(sv('rect', { x: xe, y: cy - 2.5, width: Xc(c.fc) - xe, height: 5, rx: 2.5, fill: BAD, 'fill-opacity': 0.6 }), sv('circle', { cx: Xc(c.fc), cy, r: 6, fill: BAD, stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
300
|
+
bars.append(sv('path', { d: `M${xe} ${cy - 9} l8 9 -8 9 -8 -9z`, fill: 'var(--card)', stroke: S.c, 'stroke-width': 2 }));
|
|
301
|
+
g.append(bars);
|
|
302
|
+
if (c.due > b) g.append(tx(Xl(b + 1) - 4, cy - 12, 'due ' + dlab(c.due) + (c.fc > c.due ? ' → ' + dlab(c.fc) : ''), { 'text-anchor': 'end', style: 'font-size:var(--fs-1)' }));
|
|
303
|
+
} else {
|
|
304
|
+
if (c.st === 'over') g.append(sv('line', { x1: Xc(Math.max(a, c.due)), x2: Xc(Math.min(b, TD)), y1: cy, y2: cy, stroke: BAD, 'stroke-width': 3, 'stroke-dasharray': '5 4', 'stroke-linecap': 'round' }));
|
|
305
|
+
if (c.due >= a && c.due <= b) { if (c.st === 'done') g.append(sv('circle', { cx: Xc(c.due), cy, r: 8, fill: OK, 'fill-opacity': 0.18 }), sv('path', { d: `M${Xc(c.due) - 4} ${cy} l3 3 5 -6`, fill: 'none', stroke: OK, 'stroke-width': 2, 'stroke-linecap': 'round' })); else g.append(sv('path', { d: `M${Xc(c.due)} ${cy - 8} l7 8 -7 8 -7 -8z`, fill: c.st === 'over' ? S.c : 'var(--card)', stroke: S.c, 'stroke-width': 2 })); }
|
|
306
|
+
}
|
|
307
|
+
g.append(tx(W - SW + 10, cy + 4, stat(c), { style: 'fill:' + S.c + ';font-weight:500' }));
|
|
308
|
+
const t = sv('title', {}); t.textContent = c.why || c.n; g.append(t);
|
|
309
|
+
if (c.go && !phone) { const open = () => ctx.goInit(c.go, c.n); g.addEventListener('click', open); g.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }); }
|
|
310
|
+
s.append(g);
|
|
311
|
+
});
|
|
312
|
+
const hasFill = R.some((c) => c.kind === 'init' && c.pct != null);
|
|
313
|
+
return el('section', { class: 'card', 'data-cs': 'delivery' }, head, s, phone ? phoneRows(R.filter((c) => c.go).map((c) => ({ title: c.n, text: stat(c), c: ST[c.st].c, id: c.go })), 'Open', (r) => ctx.goInit(r.id, r.title)) : null, !demo && all.length > R.length ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, '+' + (all.length - R.length) + ' more commitments not drawn') : null,
|
|
314
|
+
el('div', { class: 'pgl' }, el('span', null, '◇ due'), el('span', { style: 'color:' + BAD }, '● forecast'), hasFill ? el('span', null, '▬ actual progress') : el('span', null, '▬ planned span (no progress is recorded)'), el('span', null, '| where it should be today'), el('span', { style: 'color:' + BAD }, '┅ overdue'),
|
|
315
|
+
el('span', { style: 'color:' + OK }, '■ on time'), el('span', { style: 'color:' + WARN }, '■ at risk'), el('span', { style: 'color:' + BAD }, '■ delayed')));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ── the case file: the shared one, with its slide bound to what this page can do ─────────────────────────────────── */
|
|
319
|
+
const NO_ROUTE = 'This page has no route that records a decision yet, so it stays a proposal. Nothing is written.';
|
|
320
|
+
function caseNode(key, model, live, ctx, onDecided) {
|
|
321
|
+
const box = el('div', { 'data-cs-case': key });
|
|
322
|
+
const build = () => {
|
|
323
|
+
const done = decided.get(key); const m0 = { ...model, id: key };
|
|
324
|
+
if (!live && m0.conf == null) m0.conf = null;
|
|
325
|
+
const m = live ? { ...m0, outward: true, outboxReason: NO_ROUTE } : m0;
|
|
326
|
+
const node = renderCase(done ? { ...m, opts: [] } : m, { outbox: false, onConfirm: ({ option }) => {
|
|
327
|
+
const o = (model.opts || []).find((x) => x.id === option.id) || {};
|
|
328
|
+
decided.set(key, option.id); refresh(); if (onDecided) onDecided(option.id, true);
|
|
329
|
+
toast({ text: o.msg || 'Done. Nothing outside changed.', actionLabel: 'Undo', onAction: () => { decided.delete(key); refresh(); if (onDecided) onDecided(null, false); } });
|
|
330
|
+
} });
|
|
331
|
+
cases.add(node);
|
|
332
|
+
// no confidence is computed in real mode: the recommended option says only "Recommended", and no bar is drawn
|
|
333
|
+
if (m.conf == null) { const rk = node.querySelector('.opt.rec .rk'); if (rk) rk.lastChild.textContent = 'Recommended'; const cf = node.querySelector('.opt.rec .conf'); if (cf) cf.remove(); }
|
|
334
|
+
if (done) {
|
|
335
|
+
const o = (model.opts || []).find((x) => x.id === done); const chips = el('div', { class: 'chips2', 'data-cs': 'done' }, el('span', { class: 'st ok' }, c3ic('check', 'sm'), o ? o.l : 'Done'),
|
|
336
|
+
el('button', { type: 'button', class: 'chip2 lk', style: 'min-height:44px', onclick: (e) => { e.stopPropagation(); decided.delete(key); refresh(); if (onDecided) onDecided(null, false); } }, c3ic('undo', 'sm'), 'undo'));
|
|
337
|
+
const ft = node.querySelector('.cft'); if (ft) ft.before(chips); else node.append(chips);
|
|
338
|
+
}
|
|
339
|
+
return node;
|
|
340
|
+
};
|
|
341
|
+
const refresh = () => { const old = box.firstChild; if (old && old.c3case) old.c3case.dispose(); cases.delete(old); box.replaceChildren(build()); };
|
|
342
|
+
box.append(build());
|
|
343
|
+
box.addEventListener('click', (e) => e.stopPropagation());
|
|
344
|
+
return box;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ── the deck: the mockup's cardHtml (2324) and cf (3085), flip, dots and keys as briefing-deck.js does them ────────── */
|
|
348
|
+
const TAGS = { critical: 'Critical', opp: 'Opportunity', big: 'Big win', small: 'Small win', justin: 'Just in' };
|
|
349
|
+
const eyebrow = (c) => { const T = c.tag && TAG_STYLE[TAGS[c.tag]]; return T ? el('span', { class: 'tg' + (T.filled ? ' f' : ''), title: T.q }, c3ic(T.icon, 'sm'), TAGS[c.tag]) : el('span', { class: 'tg', style: '--tc:' + MFG }, c3ic(c.ei || 'target', 'sm'), c.el || ''); };
|
|
350
|
+
const hrows = (a) => el('div', { class: 'hrw' }, a.map((r) => el('div', { class: 'hrr ' + (r[2] || '') }, c3ic(r[0], 'sm'), el('span', null, r[1]), r[3] ? el('i', null, r[3]) : null)));
|
|
351
|
+
function hnum(h) {
|
|
352
|
+
const arr = h.arr; const W = 340; const H = 60; const has = h.target != null;
|
|
353
|
+
const mn = Math.min(...arr, ...(has ? [h.target] : [])); const mx = Math.max(...arr, ...(has ? [h.target] : []));
|
|
354
|
+
const X = (i) => 4 + i * (W - 8) / (arr.length - 1); const Y = (v) => H - 6 - (v - mn) / (mx - mn || 1) * (H - 26); const p = arr.map((v, i) => X(i) + ',' + Y(v)).join(' ');
|
|
355
|
+
const s = sv('svg', { viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Trend' });
|
|
356
|
+
if (has) s.append(sv('line', { x1: 4, x2: W - 4, y1: Y(h.target), y2: Y(h.target), stroke: 'var(--ring)', 'stroke-dasharray': '4 4' }), tx(W - 4, Y(h.target) - 4, 'target ' + h.target + h.unit, { 'text-anchor': 'end' }));
|
|
357
|
+
s.append(sv('polygon', { points: `4,${H - 4} ${p} ${W - 4},${H - 4}`, fill: h.tone, 'fill-opacity': 0.1 }), sv('polyline', { class: 'draw', pathLength: 1, points: p, fill: 'none', stroke: h.tone, 'stroke-width': 3, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }), sv('circle', { cx: X(arr.length - 1), cy: Y(arr[arr.length - 1]), r: 4.5, fill: h.tone, stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
358
|
+
return el('div', { class: 'hn' }, el('div', { class: 'a' }, el('b', null, h.big), el('span', null, h.unit)), el('div', { class: 'd', style: 'color:' + h.tone }, h.delta), s);
|
|
359
|
+
}
|
|
360
|
+
function hero(c, M, I) {
|
|
361
|
+
const h = c.hero || {};
|
|
362
|
+
if (h.kind === 'ns' && I) return nsMini(I);
|
|
363
|
+
if (h.kind === 'krs') return el('div', { class: 'hbs' }, M.outcomes.filter((k) => k.status !== 'unscored').map((k, i) => { const S = KST[k.status]; const u = k.unit || ''; return el('div', { class: 'hbr', style: '--c:' + S.c }, el('div', { class: 'l' }, el('span', null, k.name), el('i', null, S.l + (k.forecast != null ? ' · lands ' + fmt(k.forecast) + u : ''))), el('div', { class: 'tk' }, el('u', { style: 'width:' + Math.max(2, clamp((k.now - k.start) / (k.target - k.start) * 100, 0, 100)) + '%;animation-delay:' + i * 90 + 'ms' }))); }));
|
|
364
|
+
if (h.kind === 'ladder' && M.maturity) return el('div', { class: 'hbs' }, M.maturity.pillars.map((p) => { const gap = p.proj < p.tgt; const S = M.maturity.stages; return el('div', { class: 'hbr', style: '--c:' + (gap ? WARN : OK) }, el('div', { class: 'l' }, el('span', null, p.n), el('i', null, S[p.now - 1] + ' → ' + S[p.proj - 1] + (gap ? ' · target ' + S[p.tgt - 1] : ''))), el('div', { class: 'tk' }, el('u', { style: 'width:' + Math.min(100, p.proj / p.tgt * 100) + '%' }))); }));
|
|
365
|
+
if (h.kind === 'num' || h.kind === 'spark') return hnum(h);
|
|
366
|
+
if (h.kind === 'ring') return el('div', { class: 'hr' }, c3ring(h.done, Math.max(1, h.total), 96), el('div', { class: 'c' }, el('b', null, h.cap), el('span', null, h.sub || '')));
|
|
367
|
+
if (h.kind === 'rows') return hrows(h.rows);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function kpisOf(C) {
|
|
371
|
+
const o = C.opts || []; const a = [];
|
|
372
|
+
if (o.length) { if (C.conf != null) a.push([C.conf + '%', 'confidence', ACC]); a.push([o.length, 'options', PUR]); a.push([(C.chg || []).length, 'evidence', OK]); } else a.push([(C.chg || []).length, 'facts', ACC]);
|
|
373
|
+
return a.slice(0, 3).map((k) => el('div', { class: 'kp', style: '--c:' + k[2] }, el('b', null, String(k[0])), el('span', null, k[1])));
|
|
374
|
+
}
|
|
375
|
+
function recLine(c) {
|
|
376
|
+
if (decided.get('card:' + c.k)) return el('span', { class: 'cfr' }, c3ic('check', 'sm'), 'Done');
|
|
377
|
+
const C = c.case; if (!C || !(C.opts || []).length) return el('span', { class: 'cfr n' }, 'Tap to see the story ', c3ic('next', 'sm'));
|
|
378
|
+
const o = C.opts.find((r) => r.rec) || C.opts[0]; const l = o.l.length > 46 ? o.l.slice(0, 45) + '…' : o.l;
|
|
379
|
+
return el('span', { class: 'cfr' }, c3ic('spark', 'sm'), 'Suggested: ' + l + (C.conf != null ? ' · ' + C.conf + '%' : ''));
|
|
380
|
+
}
|
|
381
|
+
function cardEl(c, i, n, M, I, live, ctx, S) {
|
|
382
|
+
const T = c.tag && TAG_STYLE[TAGS[c.tag]]; const key = 'card:' + c.k;
|
|
383
|
+
const art = el('article', { class: 'fc' + (i === 0 ? ' on' : '') + (decided.get(key) ? ' ok' : ''), tabindex: '0', role: 'button', 'aria-expanded': 'false', 'aria-label': (c.el || TAGS[c.tag] || 'Card') + ': ' + c.h + ', card ' + (i + 1) + ' of ' + n,
|
|
384
|
+
'data-cs-card': c.k, 'data-tag': c.tag || '', style: '--tc:' + (T ? T.color : MFG) });
|
|
385
|
+
const front = el('div', { class: 'ff' }, el('div', null, eyebrow(c)), el('div', { class: 'cf' },
|
|
386
|
+
el('div', { class: 'cfa' }, el('h2', null, c.h), el('div', { class: 'cfs' }, c.sub)), el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': (c.hero || {}).kind, 'aria-label': c.dir ? 'Direction: ' + (decided.get(key) || S.state || 'not scored') : null }, hero(c, M, I))),
|
|
387
|
+
el('div', { class: 'cfk' }, kpisOf(c.case || {})), el('div', { class: 'cff' }, el('div', { class: 'cfx' }, (c.facts || []).map((f) => el('span', null, c3ic(f[0], 'sm'), f[1]))), recLine(c))));
|
|
388
|
+
const mark = () => { const on = !!decided.get(key); art.classList.toggle('ok', on); const r = art.querySelector(':scope > .rsv'); if (on && !r) art.querySelector('.flipg').before(el('span', { class: 'rsv' }, c3ic('check', 'sm'), 'decided')); else if (!on && r) r.remove();
|
|
389
|
+
const rl = art.querySelector('.cff'); if (rl) rl.lastChild.replaceWith(recLine(c)); };
|
|
390
|
+
const back = el('div', { class: 'fb' }, el('div', { class: 'fh', title: 'Flip back (Esc)' }, eyebrow(c), el('span', { class: 'why2' }, T ? T.q.split(': ')[1] : 'The case, the options and how sure we are', c3ic('undo', 'sm'))),
|
|
391
|
+
caseNode(key, c.case || { sit: c.sub, chg: [], opts: [] }, live, ctx, () => mark()));
|
|
392
|
+
art.append(front, ...(decided.get(key) ? [el('span', { class: 'rsv' }, c3ic('check', 'sm'), 'decided')] : []), el('span', { class: 'flipg', 'aria-hidden': 'true' }, c3ic('undo', 'sm')), back);
|
|
393
|
+
return art;
|
|
394
|
+
}
|
|
395
|
+
function flipTo(S, c, force) {
|
|
396
|
+
const deck = c.parentNode;
|
|
397
|
+
if (!c.classList.contains('on') && !c.classList.contains('back')) { deck.scrollTo({ left: c.offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' }); return; }
|
|
398
|
+
const toBack = force !== undefined ? force : !c.classList.contains('back');
|
|
399
|
+
deck.querySelectorAll('.fc.back').forEach((o) => { if (o !== c) { o.classList.remove('back'); o.setAttribute('aria-expanded', 'false'); } });
|
|
400
|
+
const sw = () => { c.classList.toggle('back', toBack); c.setAttribute('aria-expanded', String(toBack)); };
|
|
401
|
+
if (reduced()) { sw(); return; }
|
|
402
|
+
c.classList.add('fl-out'); setTimeout(() => { c.classList.remove('fl-out'); sw(); c.classList.add('fl-in'); setTimeout(() => c.classList.remove('fl-in'), 320); }, 170);
|
|
403
|
+
}
|
|
404
|
+
function deckNode(list, M, I, live, ctx, S) {
|
|
405
|
+
const deck = el('div', { class: 'deck', tabindex: '-1', role: 'group', 'aria-roledescription': 'card deck', 'aria-label': 'Cards' }, list.map((c, i) => cardEl(c, i, list.length, M, I, live, ctx, S)));
|
|
406
|
+
const dots = el('div', { class: 'dk', role: 'group', 'aria-label': 'Cards' }, list.map((c, i) => el('button', { type: 'button', class: 'dkd', 'aria-label': 'Card ' + (i + 1) + ': ' + c.h, 'aria-current': String(i === 0),
|
|
407
|
+
onClick: () => { const k = deck.children[i]; deck.scrollTo({ left: k.offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' }); } })));
|
|
408
|
+
let cur = 0;
|
|
409
|
+
const sync = () => { const f = deck.children[0]; if (!f) return; const w = f.offsetWidth + 12; cur = clamp(Math.round(deck.scrollLeft / w), 0, deck.children.length - 1); [].forEach.call(deck.children, (k, j) => k.classList.toggle('on', j === cur)); dots.querySelectorAll('.dkd').forEach((x, j) => x.setAttribute('aria-current', String(j === cur))); };
|
|
410
|
+
deck.addEventListener('scroll', sync);
|
|
411
|
+
let y0 = null;
|
|
412
|
+
deck.addEventListener('pointerdown', (e) => { y0 = e.target.closest('button,.fb .case') ? null : e.clientY; });
|
|
413
|
+
deck.addEventListener('pointerup', (e) => { if (y0 === null) return; const d = swipeDir(e.clientY - y0); y0 = null; const c = deck.children[cur]; if (d && c) flipTo(S, c, d > 0); });
|
|
414
|
+
deck.addEventListener('pointercancel', () => { y0 = null; });
|
|
415
|
+
deck.addEventListener('click', (e) => { const c = e.target.closest('.fc'); if (!c || e.target.closest('button')) return; if (c.classList.contains('back') && !e.target.closest('.fh')) return; flipTo(S, c); });
|
|
416
|
+
deck.addEventListener('keydown', (e) => {
|
|
417
|
+
const c = e.target.closest && e.target.closest('.fc');
|
|
418
|
+
if (e.key === 'Escape') { const b = deck.querySelector('.fc.back'); if (b) { e.stopPropagation(); flipTo(S, b, false); } return; }
|
|
419
|
+
if (!c) return;
|
|
420
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.target === c) { e.preventDefault(); flipTo(S, c); return; }
|
|
421
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault(); const j = clamp(cur + (e.key === 'ArrowRight' ? 1 : -1), 0, deck.children.length - 1); deck.scrollTo({ left: deck.children[j].offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' }); deck.children[j].focus({ preventScroll: true }); }
|
|
422
|
+
});
|
|
423
|
+
return el('section', { class: 'cdeck', 'data-cs': 'deck' }, deck, list.length > 1 ? dots : null);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* ── the real model: what the cards say when the repository holds the records ─────────────────────────────────────── */
|
|
427
|
+
function derive(M, ctx, live) {
|
|
428
|
+
const sigs = M.signals.map((s) => ({ ...s }));
|
|
429
|
+
if (live) {
|
|
430
|
+
((ctx.data && ctx.data.gantt) || []).forEach((g) => {
|
|
431
|
+
if (g.status !== 'delayed' && g.status !== 'overdue') return;
|
|
432
|
+
sigs.push({ id: 'ini:' + g.id, sev: 'drift', t: (g.title || g.id) + (g.status === 'overdue' ? ' is past its promised date' : ' is running late'), ev: 'Promised ' + (g.promised || 'no date') + (g.forecast && (g.forecast.end || g.forecast.start) ? ', forecast ' + (g.forecast.end || g.forecast.start) : '') + (g.slip_days > 0 ? ', ' + g.slip_days + ' days past the plan.' : '.'),
|
|
433
|
+
thr: 'Slip rule: beyond twice the tolerance reads as delayed', ttt: g.status === 'overdue' ? 'overdue' : 'delayed', sp: [], source: 'initiative' });
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
M.signalsAll = sigs;
|
|
437
|
+
const scored = M.outcomes.filter((k) => k.status !== 'unscored');
|
|
438
|
+
const okN = scored.filter((k) => k.status === 'ok').length;
|
|
439
|
+
const drift = sigs.filter((s) => s.sev === 'drift').length;
|
|
440
|
+
const pivot = sigs.some((s) => s.sev === 'pivot') || !!(ctx.data && ctx.data.strategy && ctx.data.strategy.pivot && ctx.data.strategy.pivot.signal);
|
|
441
|
+
const state = M.direction && M.direction.state ? M.direction.state : pivot ? 'pivot' : drift >= 2 || scored.some((k) => k.status === 'off') ? 'adjust' : scored.length || sigs.length || nsInfo(M.north) ? 'persevere' : null;
|
|
442
|
+
return { sigs, scored, okN, drift, pivot, state };
|
|
443
|
+
}
|
|
444
|
+
const plain = (sit, chg, extra) => ({ sit, chg, opts: [], ...(extra || {}) });
|
|
445
|
+
function realDirection(M, D, I, view) {
|
|
446
|
+
const A = view === 'quarter'; const n = D.scored.length;
|
|
447
|
+
const rows = [];
|
|
448
|
+
D.sigs.slice().sort((a, b) => SEVRANK.indexOf(a.sev) - SEVRANK.indexOf(b.sev)).slice(0, 3).forEach((s) => rows.push([(SEV[s.sev] || SEV.watch).i, s.t, s.source || 'signal', s.sev === 'up' ? 'ok' : s.sev === 'pivot' ? 'bad' : s.sev === 'drift' ? 'warn' : '']));
|
|
449
|
+
if (I && I.var != null) rows.push([I.var < 0 ? 'clock' : 'up', 'North star ' + Math.abs(I.var) + '% ' + (I.var < 0 ? 'behind' : 'ahead of') + ' plan today', 'KR metric', I.var <= -8 ? 'bad' : I.var < 0 ? 'warn' : 'ok']);
|
|
450
|
+
const head = { pivot: 'The pivot rule has tripped', adjust: 'Adjust course early', persevere: 'On course: hold the plan', null: 'Direction is not scored' }[D.state];
|
|
451
|
+
const sit = { pivot: 'A pivot rule has tripped: a KR metric has been behind its line at three readings in a row and the gap is not closing, or a critical signal rule is open. Decide whether to pivot the bet it belongs to.',
|
|
452
|
+
adjust: D.drift + ' signal' + (D.drift === 1 ? ' is' : 's are') + ' drifting or an outcome is off course, but no pivot rule has tripped. Fix the cause before changing the bet.',
|
|
453
|
+
persevere: 'No outcome is behind its line, no signal rule is open and nothing is late. Hold the plan.',
|
|
454
|
+
null: 'Nothing is scored: no KR metric has a reading, no signal rule is set and no initiative has a promised date. The direction needs at least one of them.' }[D.state];
|
|
455
|
+
const opts = D.state ? [
|
|
456
|
+
{ id: 'adjust', l: 'Adjust: act on the cause of the drifting signals', rec: D.state === 'adjust', msg: '', fx: [['check', 'Tests the cause before changing the bet', 'ok']] },
|
|
457
|
+
{ id: 'persevere', l: 'Persevere: hold the plan', rec: D.state === 'persevere', msg: '', fx: [['alert', 'The signals stay open', D.drift ? 'warn' : '']] },
|
|
458
|
+
{ id: 'pivot', l: 'Pivot the bet', rec: D.state === 'pivot', msg: '', fx: [['alert', 'The pivot rule needs 3 readings behind the line and not closing', 'bad']] },
|
|
459
|
+
] : [];
|
|
460
|
+
return { h: A ? head : (I && I.var != null ? 'North star ' + Math.abs(I.var) + '% ' + (I.var < 0 ? 'behind' : 'ahead of') + ' plan' : head),
|
|
461
|
+
sub: n ? D.okN + ' of ' + n + ' outcome' + (n === 1 ? '' : 's') + ' on course.' : 'No outcome is scored yet.',
|
|
462
|
+
case: { tc: 'var(--warning)', sit, chg: rows, opts, cta: 'Slide to decide', unsure: 'The direction follows the 10 percent early-warning line and the three-reading pivot rule. It is a rule, not a forecast, and no confidence figure is computed.', ign: '' } };
|
|
463
|
+
}
|
|
464
|
+
function realDeck(M, D, I, view, ctx) {
|
|
465
|
+
const dir = realDirection(M, D, I, view); const A = view === 'quarter';
|
|
466
|
+
const facts = [['clock', D.sigs.filter((s) => s.sev === 'drift').length + ' drifting'], ['eye', D.sigs.filter((s) => s.sev === 'watch').length + ' on watch'], ['up', D.sigs.filter((s) => s.sev === 'up').length + ' upside']];
|
|
467
|
+
const L = [{ k: A ? 'qdir' : 'ydir', ei: 'target', el: A ? 'Quarter direction' : 'Year direction', h: dir.h, sub: dir.sub, hero: I ? { kind: 'ns' } : null, facts, case: dir.case, dir: true }];
|
|
468
|
+
if (A && D.scored.length) {
|
|
469
|
+
const bad = D.scored.filter((k) => k.status !== 'ok');
|
|
470
|
+
L.push({ k: 'qkr', ei: 'target', el: 'Outcomes', h: D.okN + ' of ' + D.scored.length + ' outcomes on course', sub: bad.length ? bad.map((k) => k.name).join(', ') + (bad.length === 1 ? ' is' : ' are') + ' behind the line.' : 'Every scored outcome is on its line.', hero: { kind: 'krs' }, facts: [['target', M.outcomes.length + ' KR metrics']],
|
|
471
|
+
case: plain('Each outcome is compared with the straight line from its baseline to its target.', D.scored.slice(0, 4).map((k) => [k.status === 'ok' ? 'check' : 'clock', k.name + ': now ' + fmt(k.now) + (k.unit || '') + ', ' + fmt(k.expected) + (k.unit || '') + ' expected today', 'KR metric', k.status === 'ok' ? 'ok' : k.status === 'off' ? 'bad' : 'warn']), { unsure: 'A line from baseline to target assumes steady progress.' }) });
|
|
472
|
+
}
|
|
473
|
+
const top = D.sigs.slice().sort((a, b) => SEVRANK.indexOf(a.sev) - SEVRANK.indexOf(b.sev))[0];
|
|
474
|
+
if (top) L.push({ k: A ? 'qsig' : 'ysig', tag: top.sev === 'up' ? 'big' : top.sev === 'watch' ? 'opp' : 'critical', h: top.t, sub: top.thr, hero: top.sp && top.sp.length > 1 ? { kind: 'spark', big: fmt(top.sp[top.sp.length - 1]), unit: '', delta: top.ttt || 'latest reading', tone: (SEV[top.sev] || SEV.watch).c, arr: top.sp, target: null } : { kind: 'rows', rows: [[(SEV[top.sev] || SEV.watch).i, top.ev, '', '']] },
|
|
475
|
+
facts: [['target', top.source || 'signal']], case: caseOfSignal(top, true) });
|
|
476
|
+
if (I) L.push({ k: A ? 'qns' : 'yns', ei: 'up', el: 'North star', h: 'North star is ' + (I.var == null ? 'not read' : Math.abs(I.var) + '% ' + (I.var < 0 ? 'behind' : 'ahead of') + ' plan'), sub: I.leaves > 0 ? 'At this slope it leaves tolerance in ' + keyLab(I.P[I.leaves].at) + (I.pivot > 0 ? ' and reaches the pivot line in ' + keyLab(I.P[I.pivot].at) + '.' : '.') : 'At this slope it stays inside the tolerance band.', hero: { kind: 'ns' },
|
|
477
|
+
facts: [['clock', M.north.label]], case: plain('The north star is drawn against a straight plan from its baseline to its target, with a band of 8 percent either side.', [['up', 'Now ' + fmt(I.P[I.nowI].actual) + I.unit + ' against ' + fmt(I.P[I.nowI].plan) + I.unit + ' planned', 'KR metric']]) });
|
|
478
|
+
if (!A && M.timeline && M.timeline.rows.length) L.push({ k: 'ybets', ei: 'flag', el: 'Bets and checkpoints', h: M.timeline.rows.length + ' bet' + (M.timeline.rows.length === 1 ? '' : 's') + ' on the roadmap', sub: M.timeline.checkpoints.length ? 'Next checkpoint: ' + M.timeline.checkpoints[0].label + '.' : 'No checkpoint date is recorded.',
|
|
479
|
+
hero: { kind: 'rows', rows: M.timeline.checkpoints.slice(0, 3).map((c) => ['flag', c.label, '', '']).concat(M.timeline.checkpoints.length ? [] : [['info', 'No hypothesis has a review date', '', '']]) }, facts: [['target', M.timeline.rows.length + ' bets']], case: plain('Each bet carries a horizon, and each hypothesis a review date.', M.timeline.checkpoints.slice(0, 3).map((c) => ['flag', c.label, 'hypothesis'])) });
|
|
480
|
+
return { cards: L, head: { i: A ? 'target' : 'flag', h: dir.h, s: dir.sub + (I && I.leaves > 0 && !A ? ' It leaves tolerance in ' + keyLab(I.P[I.leaves].at) + '.' : ''), ring: [D.okN, Math.max(1, D.scored.length)] } };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* ── mount ──────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
484
|
+
function lensHead(h) { return el('div', { class: 'lh', 'data-cs': 'lens' }, el('span', { class: 'ki' }, c3ic(h.i || 'target')), el('div', { style: 'min-width:0' }, el('b', null, h.h), el('div', { class: 'dim' }, h.s)), c3ring(h.ring[0], Math.max(1, h.ring[1]), 44)); }
|
|
485
|
+
|
|
486
|
+
function draw(host, ctx, view, M0) {
|
|
487
|
+
const live = !(M0 && M0.demo);
|
|
488
|
+
if (!M0 || !M0.ok) { host.replaceChildren(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-cs': 'error' }, 'The strategy view could not be read: ' + ((M0 && M0.error) || 'not available'))); return; }
|
|
489
|
+
const M = { ...M0 }; const today = ctx.today || (M.today || dstr(Date.now()));
|
|
490
|
+
const D = derive(M, ctx, live); const I = nsInfo(M.north);
|
|
491
|
+
const S = { state: D.state };
|
|
492
|
+
let deck; let head;
|
|
493
|
+
if (M.deck && M.deck[view]) { deck = M.deck[view]; head = M.head && M.head[view]; } else { const r = realDeck(M, D, I, view, ctx); deck = r.cards; head = r.head; }
|
|
494
|
+
const dk = deckNode(deck, M, I, live, ctx, S);
|
|
495
|
+
const left = view === 'quarter' ? [dk, outcomesCard(M), signalsCard(M, 6, ctx, live)] : [dk, maturityCard(M), betsCard(M, ctx)];
|
|
496
|
+
const right = view === 'quarter' ? [northCard(M, view, today), synergyCard(M), deliveryCard(M, ctx, live)] : [northCard(M, view, today), signalsCard(M, 4, ctx, live), synergyCard(M)];
|
|
497
|
+
host.replaceChildren(lensHead(head || { i: 'target', h: 'Direction', s: '', ring: [0, 1] }), el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, ...left), el('div', { class: 'bcol r' }, ...right))));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const live_ = new WeakMap();
|
|
501
|
+
function mount(host, ctx, view) {
|
|
502
|
+
if (!host) return host;
|
|
503
|
+
ensureC3Css();
|
|
504
|
+
dispose(host);
|
|
505
|
+
const goInit = (id, title) => { if (ctx && ctx.goInit) ctx.goInit(id, title); };
|
|
506
|
+
const c = { ...(ctx || {}), goInit };
|
|
507
|
+
live_.set(host, true);
|
|
508
|
+
host.setAttribute('data-cs-view', view); host.style.cssText = 'display:grid;gap:var(--sp-4)';
|
|
509
|
+
const demo = typeof window !== 'undefined' && !!window.__c3demo;
|
|
510
|
+
if (fresh()) draw(host, c, view, cache.model);
|
|
511
|
+
else {
|
|
512
|
+
host.replaceChildren(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-cs': 'loading' }, 'Reading the repository'));
|
|
513
|
+
readModel(!demo).then((m) => { if (live_.get(host)) draw(host, c, view, m); });
|
|
514
|
+
}
|
|
515
|
+
return host;
|
|
516
|
+
}
|
|
517
|
+
export const mountQuarter = (host, ctx) => mount(host, ctx, 'quarter');
|
|
518
|
+
export const mountYear = (host, ctx) => mount(host, ctx, 'year');
|
|
519
|
+
export function dispose(host) {
|
|
520
|
+
if (!host) return;
|
|
521
|
+
live_.delete(host);
|
|
522
|
+
host.querySelectorAll('.case').forEach((n) => { if (n.c3case) n.c3case.dispose(); cases.delete(n); });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* The panel builders, for the other places the mockup draws the same panels (Initiatives > Lifecycle > Strategize, Reports > Product): see c3-strategy-panels.js. */
|
|
526
|
+
export const PANEL_BUILDERS = { north: northCard, outcomes: outcomesCard, signals: signalsCard, synergy: synergyCard, maturity: maturityCard, bets: betsCard };
|
|
527
|
+
export { readModel, fresh as modelFresh, dstr as todayStr };
|
|
528
|
+
export const cachedModel = () => (fresh() ? cache.model : null);
|