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,240 @@
|
|
|
1
|
+
/* pm/c3-breakdown.js — the Breakdown sub-view of Initiatives on /c3.html, on the mockup's own markup and CSS (console3.html wbTree 3986, wbRow 3990 and
|
|
2
|
+
* patch 25 at line 4406: wbEd, capCard, the wform; wbsRight 3997). PMOS-ONLY non-development work in three phases (Discovery, Design, Launch); the
|
|
3
|
+
* engineering tickets are in the Tickets view and are never mixed in here.
|
|
4
|
+
* Left: the tree (initiative, phase, item). An item shows its phase, source and owner chips, who added it and how often it changed, and on tap its
|
|
5
|
+
* history and an editor: phase, owner, points, the Jira tickets it waits on, delete (with an 8 second undo). Right: Capacity for PMOS work, Who
|
|
6
|
+
* shaped the plan, Latest changes, Rules.
|
|
7
|
+
* REAL: GET /api/c3-init/breakdown (items, people, capacity per person over two weeks, the events of the ledger: who and why) and the writes
|
|
8
|
+
* through pmPost to /api/roadmap-ops/breakdown/{add,edit,delete,undo,dependency} and /people. A change to a field Jira owns comes back as a proposal and
|
|
9
|
+
* is shown as one. Nothing is invented: with no items the page says so. */
|
|
10
|
+
import { el } from './dom.js';
|
|
11
|
+
import { pmGet, pmPost } from './http.js';
|
|
12
|
+
import { c3ic, c3ib } from './pm-work-view.js';
|
|
13
|
+
import { makeView, frame, hBars, card, emptyRow, dayLabel, say } from './c3-init-extras.js';
|
|
14
|
+
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
15
|
+
import { attachExpand } from './c3-expand.js';
|
|
16
|
+
|
|
17
|
+
const PHASES = ['discovery', 'design', 'launch'];
|
|
18
|
+
const PL = { discovery: 'Discovery', design: 'Design', launch: 'Launch' };
|
|
19
|
+
const PC = { discovery: 'var(--purple)', design: 'var(--acc)', launch: 'var(--warning)' };
|
|
20
|
+
const SRC = { local: 'Local', figma: 'Figma', calendar: 'Calendar', gmail: 'Gmail', notes: 'Notes' };
|
|
21
|
+
const AGENTISH = /^(agent|claude)/i;
|
|
22
|
+
|
|
23
|
+
/** who a ledger `by` is: { l: label, c: colour, i: icon }, as the mockup's orig(). */
|
|
24
|
+
function orig(by) {
|
|
25
|
+
const b = String(by || '');
|
|
26
|
+
if (AGENTISH.test(b) || /^claude-code/.test(b)) return { l: 'Agent', c: 'var(--purple)', i: 'spark' };
|
|
27
|
+
if (/^human:wawan$/i.test(b) || b === 'You' || b === '') return { l: 'You', c: 'var(--acc)', i: 'user' };
|
|
28
|
+
return { l: b.replace(/^human:/, ''), c: 'var(--muted-foreground)', i: 'user' };
|
|
29
|
+
}
|
|
30
|
+
const bucket = (by) => { const l = orig(by).l; return l === 'You' || l === 'Agent' ? l : 'Team'; };
|
|
31
|
+
const evOf = (V, rid) => ((V.data.bd && V.data.bd.events) || []).filter((e) => e.rid === rid);
|
|
32
|
+
const isOwnerPerson = (p) => p && !AGENTISH.test(p);
|
|
33
|
+
|
|
34
|
+
async function load(V) {
|
|
35
|
+
const bd = await pmGet('/api/c3-init/breakdown').catch(() => null);
|
|
36
|
+
if (V.disposed) return;
|
|
37
|
+
V.data.bd = bd && bd.ok ? bd : null; V.data.err = bd && bd.ok ? null : (bd && bd.error) || 'The breakdown could not be read.';
|
|
38
|
+
V.render();
|
|
39
|
+
if (V.sheet) V.sheet.draw();
|
|
40
|
+
}
|
|
41
|
+
async function write(V, path, body, note, at) {
|
|
42
|
+
const r = await pmPost(path, body);
|
|
43
|
+
if (V.disposed) return r;
|
|
44
|
+
if (r && r.ok) { V.err = ''; V.note = r.proposal ? (r.note || 'Jira owns those fields: the change is a proposal, nothing was applied.') : note || ''; await load(V); return r; }
|
|
45
|
+
V.err = (r && r.error) || 'Not recorded.'; V.errAt = at || null; V.render(); return r;
|
|
46
|
+
}
|
|
47
|
+
const iniName = (V, id) => { const r = (V.ctx.rows || []).find((x) => x.id === id); return r ? r.title : id || 'No initiative'; };
|
|
48
|
+
|
|
49
|
+
/** the tree: initiatives (in the order of first appearance), each with the phases that hold items. */
|
|
50
|
+
function tree(V) {
|
|
51
|
+
const items = ((V.data.bd && V.data.bd.items) || []).filter((x) => !V.scope || x.initiative === V.scope);
|
|
52
|
+
const ids = []; items.forEach((x) => { const k = x.initiative || ''; if (!ids.includes(k)) ids.push(k); });
|
|
53
|
+
return ids.map((id, ii) => {
|
|
54
|
+
const mine = items.filter((x) => (x.initiative || '') === id);
|
|
55
|
+
const firsts = mine.map((x) => evOf(V, x.rid)[0]).filter(Boolean); const youN = firsts.filter((e) => orig(e.by).l === 'You').length;
|
|
56
|
+
const first = firsts.length ? (youN * 2 >= firsts.length ? firsts.find((e) => orig(e.by).l === 'You') || firsts[0] : firsts[0]) : null; // who added most of it
|
|
57
|
+
const pts = (l) => [l.filter((x) => x.done).reduce((n, x) => n + (x.points || 1), 0), l.reduce((n, x) => n + (x.points || 1), 0)];
|
|
58
|
+
return { id: 'i-' + id, type: 'ini', code: String(ii + 1), name: iniName(V, id), by: first && first.by, pts: pts(mine), kids: PHASES.map((ph, pi) => {
|
|
59
|
+
const l = mine.filter((x) => x.phase === ph); if (!l.length) return null;
|
|
60
|
+
return { id: 'p-' + id + '-' + ph, type: 'ph', code: (ii + 1) + '.' + (pi + 1), name: PL[ph], pts: pts(l), kids: l.map((x, xi) => ({ id: x.rid, type: 'it', code: (ii + 1) + '.' + (pi + 1) + '.' + (xi + 1), name: x.title, item: x, pts: [x.done ? x.points || 1 : 0, x.points || 1], kids: [] })) };
|
|
61
|
+
}).filter(Boolean) };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const collapsed = (V, n) => ((V.col = V.col || {})[n.id] != null ? V.col[n.id] : n.type === 'ini' && !V.scope);
|
|
66
|
+
const pill = (label, on, attrs, fn) => el('button', { type: 'button', class: 'pill', 'aria-pressed': String(on), onClick: fn, ...(attrs || {}) }, label);
|
|
67
|
+
|
|
68
|
+
function history(V, rid) {
|
|
69
|
+
const ev = evOf(V, rid).slice().reverse();
|
|
70
|
+
if (!ev.length) return el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'No history is recorded for this item.');
|
|
71
|
+
const KI = { add: 'plus', edit: 'notes', dependency: 'org', delete: 'x' };
|
|
72
|
+
return el('ul', { class: 'hx' }, ev.map((e) => { const o = orig(e.by); return el('li', null, el('span', { class: 'chip2', style: 'color:' + o.c }, c3ic(o.i, 'sm'), o.l),
|
|
73
|
+
el('span', null, c3ic(KI[e.kind] || 'notes', 'sm'), ' ' + (e.kind === 'add' ? 'Added. ' : ''), e.why, ''), el('span', { class: 'w' }, dayLabel(String(e.at).slice(0, 10)))); }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function editor(V, x) {
|
|
77
|
+
const ppl = ((V.data.bd && V.data.bd.people) || []).map((p) => p.id);
|
|
78
|
+
const owners = [...new Set([...ppl, ...(x.owner && !ppl.includes(x.owner) ? [x.owner] : []), 'Agent'])];
|
|
79
|
+
/* an edit saves at once and keeps an undo: the toast puts the old values back (a page press on the same route) */
|
|
80
|
+
const edit = async (fields, note) => {
|
|
81
|
+
const prev = {}; Object.keys(fields).forEach((k) => { prev[k] = x[k]; });
|
|
82
|
+
const r = await write(V, '/api/roadmap-ops/breakdown/edit', { rid: x.rid, fields }, note, x.rid);
|
|
83
|
+
if (r && r.ok && !r.proposal) say(note || 'Saved.', () => write(V, '/api/roadmap-ops/breakdown/edit', { rid: x.rid, fields: prev }, 'Undone.', x.rid));
|
|
84
|
+
return r;
|
|
85
|
+
};
|
|
86
|
+
const title = el('input', { type: 'text', value: x.title, 'aria-label': 'Title', onChange: (e) => { const v = e.target.value.trim(); if (v && v !== x.title) edit({ title: v }, 'Renamed.'); } });
|
|
87
|
+
const dep = el('input', { type: 'text', placeholder: 'Jira key, such as CHK-144', 'aria-label': 'Add a dependency on a Jira ticket', 'data-bd-dep': x.rid });
|
|
88
|
+
return el('div', { class: 'wbed', onClick: (e) => e.stopPropagation() },
|
|
89
|
+
el('div', { class: 'chips2' }, el('span', { class: 'lb' }, 'Title'), title),
|
|
90
|
+
el('div', { class: 'chips2' }, el('span', { class: 'lb' }, 'Phase'), PHASES.map((p) => pill(PL[p], x.phase === p, { 'data-bd-phase': p }, () => x.phase !== p && edit({ phase: p }, 'Phase is now ' + PL[p] + '.')))),
|
|
91
|
+
el('div', { class: 'chips2' }, el('span', { class: 'lb' }, 'Owner'), owners.map((o) => pill(o, x.owner === o, { 'data-bd-owner': o }, () => x.owner !== o && edit({ owner: o }, 'Owner is now ' + o + '.')))),
|
|
92
|
+
el('div', { class: 'chips2' }, el('span', { class: 'lb' }, 'Points'),
|
|
93
|
+
c3ib('Fewer points', 'prev', { 'data-bd-pts': '-1', onClick: () => (x.points || 1) > 1 && edit({ points: (x.points || 1) - 1 }) }, 'sm'), el('b', null, String(x.points || 1)),
|
|
94
|
+
c3ib('More points', 'next', { 'data-bd-pts': '1', onClick: () => edit({ points: (x.points || 1) + 1 }) }, 'sm')),
|
|
95
|
+
el('div', { class: 'chips2' }, el('span', { class: 'lb' }, 'Waits on'),
|
|
96
|
+
(x.deps || []).length ? (x.deps || []).map((k) => el('span', { class: 'chip2', title: 'Waits on ' + k }, c3ic('lock', 'sm'), k,
|
|
97
|
+
el('button', { type: 'button', class: 'chv', 'aria-label': 'Stop waiting on ' + k, 'data-bd-undep': k, onClick: async () => { const r = await write(V, '/api/roadmap-ops/breakdown/dependency', { rid: x.rid, jira: k, remove: true }, 'No longer waits on ' + k + '.', x.rid); if (r && r.ok && !r.proposal) say('No longer waits on ' + k + '.', () => write(V, '/api/roadmap-ops/breakdown/dependency', { rid: x.rid, jira: k }, 'Waits on ' + k + ' again.', x.rid)); } }, c3ic('x', 'sm')))) : el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Nothing'),
|
|
98
|
+
dep, c3ib('Add the dependency', 'plus', { 'data-bd-adddep': x.rid, onClick: async () => { const k = dep.value.trim().toUpperCase(); if (!k) return; const r = await write(V, '/api/roadmap-ops/breakdown/dependency', { rid: x.rid, jira: k }, x.title + ' now waits on ' + k + '.', x.rid); if (r && r.ok && !r.proposal) say(x.title + ' now waits on ' + k + '.', () => write(V, '/api/roadmap-ops/breakdown/dependency', { rid: x.rid, jira: k, remove: true }, 'No longer waits on ' + k + '.', x.rid)); } }, 'sm')),
|
|
99
|
+
el('div', { class: 'chips2' },
|
|
100
|
+
el('button', { type: 'button', class: 'pill', style: 'color:var(--destructive)', 'data-bd-del': x.rid, onClick: async () => {
|
|
101
|
+
const r = await write(V, '/api/roadmap-ops/breakdown/delete', { rid: x.rid }, '', x.rid);
|
|
102
|
+
if (r && r.ok) { if (V.sheet) V.sheet.ui.close(); startUndo(V, x); say('Deleted “' + x.title + '”.', () => write(V, '/api/roadmap-ops/breakdown/undo', { rid: x.rid }, 'Brought back “' + x.title + '”.', null)); }
|
|
103
|
+
} }, c3ic('x', 'sm'), 'Delete'), el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Created here, so delete removes it. Undo brings it back for 8 seconds.')));
|
|
104
|
+
}
|
|
105
|
+
function startUndo(V, x) {
|
|
106
|
+
clearTimeout(V.undoT); V.undo = { rid: x.rid, title: x.title };
|
|
107
|
+
V.undoT = setTimeout(() => { V.undo = null; V.render(); }, 8000);
|
|
108
|
+
V.render();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The edit sheet of an item (the mockup's wbEd, console3.html line 4427, on the shared sheet): title, phase, owner, points, the Jira tickets it waits on, delete with an undo, and its history. */
|
|
112
|
+
function openItem(V, rid) {
|
|
113
|
+
const find = () => ((V.data.bd && V.data.bd.items) || []).find((x) => x.rid === rid);
|
|
114
|
+
const first = find(); if (!first) return;
|
|
115
|
+
let ui = null;
|
|
116
|
+
const draw = () => {
|
|
117
|
+
const x = find(); if (!x) { ui.close(); return; }
|
|
118
|
+
ui.body.replaceChildren(el('div', { class: 'dpn', 'data-bd-sheet': rid },
|
|
119
|
+
V.err && V.errAt === rid ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), V.err) : null,
|
|
120
|
+
V.note ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status' }, V.note) : null,
|
|
121
|
+
editor(V, x), el('h4', { style: 'margin:0;font-size:var(--fs-3)' }, 'History'), history(V, rid)));
|
|
122
|
+
};
|
|
123
|
+
ui = openSheet({ title: first.title, icon: 'tree', body: el('div'), onClose: () => { if (V.sheet && V.sheet.ui === ui) V.sheet = null; } });
|
|
124
|
+
V.sheet = { rid, ui, draw };
|
|
125
|
+
draw();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function row(V, n) {
|
|
129
|
+
const x = n.item; const open = !!V.open['bd' + n.id]; const has = n.kids.length > 0; const col = collapsed(V, n);
|
|
130
|
+
const first = x ? evOf(V, x.rid)[0] : null; const o = orig(x ? first && first.by : n.by); const edits = x ? Math.max(0, evOf(V, x.rid).length - 1) : 0;
|
|
131
|
+
const pr = n.pts[1] ? n.pts[0] / n.pts[1] : 0;
|
|
132
|
+
const chips = [el('span', { class: 'chip2 mono' }, n.code)];
|
|
133
|
+
if (n.type !== 'ph') chips.push(el('span', { class: 'chip2', style: 'color:' + o.c, title: 'Who added it' }, c3ic(o.i, 'sm'), (o.l === 'Agent' ? 'Added by agent' : o.l === 'You' ? 'Added by you' : 'Added by ' + o.l)));
|
|
134
|
+
if (edits) chips.push(el('span', { class: 'chip2', title: 'Last change' }, c3ic('clock', 'sm'), edits + ' edit' + (edits > 1 ? 's' : '') + ', last ' + orig(evOf(V, x.rid).slice(-1)[0].by).l));
|
|
135
|
+
if (x && x.owner) chips.push(el('span', { class: 'chip2' }, x.owner));
|
|
136
|
+
chips.push(el('span', { class: 'chip2' }, n.pts[1] + ' pts'));
|
|
137
|
+
if (x) {
|
|
138
|
+
chips.push(el('span', { class: 'chip2', style: 'color:' + (PC[x.phase] || 'inherit'), title: 'Phase' }, PL[x.phase] || x.phase), el('span', { class: 'chip2', title: 'Where it comes from' }, SRC[x.source] || x.source));
|
|
139
|
+
if (x.done) chips.push(el('span', { class: 'chip2', style: 'color:var(--success)' }, 'Done'));
|
|
140
|
+
(x.deps || []).forEach((k) => chips.push(el('span', { class: 'chip2', title: 'Waits on the Jira ticket ' + k }, c3ic('lock', 'sm'), k)));
|
|
141
|
+
}
|
|
142
|
+
const li = el('li', { class: 'nrow', 'data-bd': n.id, tabindex: '0', style: 'margin-left:' + (n.type === 'ini' ? 0 : n.type === 'ph' ? 18 : 36) + 'px', ...(x ? { 'aria-haspopup': 'dialog' } : {}) });
|
|
143
|
+
const xp = x ? null : el('div', { class: 'xp' }, el('div', { class: 'xi' }, el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, n.type === 'ini' ? 'Work that lives only in PMOS for this initiative.' : 'Items of this phase.')));
|
|
144
|
+
if (x) {
|
|
145
|
+
/* an item is one object: a tap or Enter opens its edit sheet */
|
|
146
|
+
const openIt = () => openItem(V, x.rid);
|
|
147
|
+
li.addEventListener('click', (e) => { if (!e.target.closest('.chv,button,a')) openIt(); });
|
|
148
|
+
li.addEventListener('keydown', (e) => { if (e.target === li && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openIt(); } });
|
|
149
|
+
} else attachExpand(li, xp, { key: 'bd' + n.id, initial: !!V.open['bd' + n.id], onToggle: (on) => { V.open['bd' + n.id] = on; } });
|
|
150
|
+
li.append(el('span', { class: 'ki' }, has ? el('button', { type: 'button', class: 'chv', 'aria-expanded': String(!col), 'aria-label': 'Show or hide ' + n.name, 'data-bd-tog': n.id, onClick: (e) => { e.stopPropagation(); V.col[n.id] = !collapsed(V, n); V.render(); } }, c3ic('next', 'sm')) : c3ic('check')),
|
|
151
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt', style: n.type !== 'it' ? 'font-weight:600' : '', tabindex: '-1' }, n.name), el('div', { class: 'chips' }, chips),
|
|
152
|
+
n.type !== 'it' ? el('div', { class: 'tk', style: 'margin-top:6px' }, el('u', { style: 'width:' + Math.max(2, pr * 100) + '%;background:var(--success)' })) : null, xp));
|
|
153
|
+
return [li, ...(has && !col ? n.kids.flatMap((k) => row(V, k)) : [])];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function addForm(V) {
|
|
157
|
+
const rows = V.ctx.rows || [];
|
|
158
|
+
const title = el('input', { type: 'text', placeholder: 'What is the work?', 'aria-label': 'What is the work', 'data-bd-new': 'title' });
|
|
159
|
+
const ini = el('select', { 'aria-label': 'Initiative', 'data-bd-new': 'initiative' }, el('option', { value: '' }, 'No initiative'), rows.map((r) => el('option', { value: r.id, selected: r.id === V.scope }, r.title)));
|
|
160
|
+
const ph = el('select', { 'aria-label': 'Phase', 'data-bd-new': 'phase' }, PHASES.map((p) => el('option', { value: p }, PL[p])));
|
|
161
|
+
const src = el('select', { 'aria-label': 'Where it comes from', 'data-bd-new': 'source' }, Object.keys(SRC).map((s) => el('option', { value: s }, SRC[s])));
|
|
162
|
+
const own = el('input', { type: 'text', placeholder: 'Owner (optional)', 'aria-label': 'Owner', 'data-bd-new': 'owner' });
|
|
163
|
+
const pts = el('input', { type: 'number', min: '1', step: '1', value: '1', 'aria-label': 'Points', 'data-bd-new': 'points' });
|
|
164
|
+
return el('div', { class: 'wform', 'data-bd-form': '' }, el('b', { style: 'font-size:var(--fs-3)' }, 'Add a work item'), title, ini, ph, src, own, pts,
|
|
165
|
+
el('div', { class: 'chips2' },
|
|
166
|
+
el('button', { type: 'button', class: 'pill', 'data-bd-save': '', onClick: () => {
|
|
167
|
+
if (!title.value.trim()) { V.err = 'Name the work first.'; V.errAt = null; V.render(); return; }
|
|
168
|
+
const body = { title: title.value.trim(), phase: ph.value, source: src.value, ...(own.value.trim() ? { owner: own.value.trim() } : {}), points: Math.max(1, Number(pts.value) || 1), ...(ini.value ? { initiative: ini.value } : {}) };
|
|
169
|
+
V.form = false; write(V, '/api/roadmap-ops/breakdown/add', body, 'Added “' + body.title + '”.', null);
|
|
170
|
+
} }, c3ic('check', 'sm'), 'Add it'),
|
|
171
|
+
el('button', { type: 'button', class: 'pill', onClick: () => { V.form = false; V.err = ''; V.render(); } }, c3ic('x', 'sm'), 'Cancel')));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function capacityCard(V) {
|
|
175
|
+
const bd = V.data.bd || {}; const people = bd.people || [];
|
|
176
|
+
const all = (bd.capacity || []).filter((r) => r.person && isOwnerPerson(r.person));
|
|
177
|
+
const rows = all.filter((r) => people.some((p) => p.id === r.person) || r.pmos_points > 0); // a row for someone with no points per week and no open work is Jira load only
|
|
178
|
+
const un = (bd.capacity || []).find((r) => r.unassigned);
|
|
179
|
+
const setPpw = (id, cur, d) => write(V, '/api/roadmap-ops/people', { id, points_per_week: Math.max(0, cur + d) }, 'Points per week for ' + id + ': ' + Math.max(0, cur + d) + '.', null);
|
|
180
|
+
const jira = all.filter((r) => r.jira_points > 0).map((r) => r.person + ' ' + r.jira_points + ' pts');
|
|
181
|
+
return card('user', 'Capacity for PMOS work', 'next 2 weeks, points open of points available',
|
|
182
|
+
...rows.map((r) => {
|
|
183
|
+
const ppw = (people.find((p) => p.id === r.person) || {}).points_per_week || r.capacity / 2; const pct = r.capacity ? Math.min(100, Math.round(r.pmos_points / r.capacity * 100)) : 0;
|
|
184
|
+
const colr = r.over ? 'var(--destructive)' : r.pmos_points > r.capacity * 0.85 ? 'var(--warning)' : 'var(--success)';
|
|
185
|
+
return el('div', { class: 'chips2', 'data-bd-cap': r.person, style: 'justify-content:space-between;gap:10px' }, el('span', { style: 'width:60px' }, r.person), el('div', { class: 'tk', style: 'flex:1' }, el('u', { style: 'width:' + Math.max(2, pct) + '%;background:' + colr })),
|
|
186
|
+
el('span', { class: 'mono dim', style: 'font-size:var(--fs-2);' + (r.over ? 'color:var(--destructive)' : '') }, r.pmos_points + ' of ' + r.capacity),
|
|
187
|
+
c3ib('Less capacity for ' + r.person, 'prev', { onClick: () => setPpw(r.person, ppw, -1) }, 'sm'), c3ib('More capacity for ' + r.person, 'next', { onClick: () => setPpw(r.person, ppw, 1) }, 'sm'));
|
|
188
|
+
}),
|
|
189
|
+
rows.length ? null : el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No one has points per week yet, so there is no capacity to compare.'),
|
|
190
|
+
rows.length ? el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Points per person per week: ' + rows.map((r) => r.person + ' ' + ((people.find((p) => p.id === r.person) || {}).points_per_week || r.capacity / 2)).join(', ') + '. Use the arrows to change them.') : null,
|
|
191
|
+
un ? el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, un.pmos_points + ' points have no owner yet.') : null,
|
|
192
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, c3ic('lock', 'sm'), ' Engineering load comes from Jira and is read-only here: ' + (jira.join(', ') || 'none') + '.'));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function render(V) {
|
|
196
|
+
const bd = V.data.bd; const items = ((bd && bd.items) || []).filter((x) => !V.scope || x.initiative === V.scope);
|
|
197
|
+
const agents = items.filter((x) => { const f = evOf(V, x.rid)[0]; return f && orig(f.by).l === 'Agent'; }).length;
|
|
198
|
+
const done = items.filter((x) => x.done).length;
|
|
199
|
+
const head = !bd ? { icon: 'tree', h: V.data.err || 'Reading the breakdown', s: 'Work that lives only in PMOS: discovery, design and launch.', ring: [0, 1] }
|
|
200
|
+
: { icon: 'tree', h: items.length ? items.length + ' work item' + (items.length === 1 ? '' : 's') + '. Agents added ' + agents + ', you added ' + (items.length - agents) : 'No work items yet',
|
|
201
|
+
s: 'Work that lives only in PMOS: discovery, design and launch. Engineering work is in Tickets.', ring: [done, items.length] };
|
|
202
|
+
const T = tree(V);
|
|
203
|
+
const list = el('ul', { class: 'list wbs' }, T.length ? T.flatMap((n) => row(V, n)) : [emptyRow('check', bd ? 'No work items yet. Add one with the plus.' : 'Reading the breakdown.')]);
|
|
204
|
+
const left = el('section', { class: 'card sect', 'data-bd-list': '' },
|
|
205
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('tree', 'sm'), 'Work breakdown ', el('span', { class: 'bdg g' }, String(items.length))),
|
|
206
|
+
el('span', { class: 'chips2' }, V.scope ? null : el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'pick an initiative to add'), c3ib('Add a work item', 'plus', { 'data-bd-add': '', 'aria-expanded': String(!!V.form), onClick: () => { V.form = !V.form; V.err = ''; V.render(); } }, 'sm'))),
|
|
207
|
+
V.form ? addForm(V) : null,
|
|
208
|
+
V.note ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-bd-note': '' }, V.note) : null,
|
|
209
|
+
V.undo ? el('div', { class: 'fold', role: 'status', 'data-bd-undo': '' }, c3ic('undo', 'sm'), 'Deleted “' + V.undo.title + '”. ',
|
|
210
|
+
el('button', { type: 'button', class: 'chip2 lk', onClick: async () => { const u = V.undo; V.undo = null; clearTimeout(V.undoT); await write(V, '/api/roadmap-ops/breakdown/undo', { rid: u.rid }, 'Brought back “' + u.title + '”.', null); } }, c3ic('undo', 'sm'), 'Undo')) : null,
|
|
211
|
+
V.err && !V.errAt ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), V.err) : null,
|
|
212
|
+
list,
|
|
213
|
+
el('div', { class: 'pgl' }, el('span', { style: 'color:var(--acc)' }, 'you'), el('span', { style: 'color:var(--purple)' }, 'agent'), el('span', null, 'teammates in grey'), el('span', null, 'tap a row for its history')));
|
|
214
|
+
|
|
215
|
+
const evs = ((bd && bd.events) || []).filter((e) => !V.scope || items.some((x) => x.rid === e.rid));
|
|
216
|
+
const adds = evs.filter((e) => e.kind === 'add' && items.some((x) => x.rid === e.rid)); const chg = evs.filter((e) => e.kind !== 'add');
|
|
217
|
+
const cnt = (l) => { const o = { You: 0, Agent: 0, Team: 0 }; l.forEach((e) => { o[bucket(e.by)]++; }); return o; };
|
|
218
|
+
const A = cnt(adds); const C = cnt(chg); const mxA = Math.max(1, adds.length); const mxC = Math.max(1, chg.length);
|
|
219
|
+
const day0 = evs.length ? String(evs[0].at).slice(0, 10) : '';
|
|
220
|
+
let feed = evs.filter((e) => e.kind !== 'add' || String(e.at).slice(0, 10) !== day0); if (!feed.length) feed = evs;
|
|
221
|
+
feed = feed.slice().reverse().slice(0, 6);
|
|
222
|
+
const right = [
|
|
223
|
+
capacityCard(V),
|
|
224
|
+
card('user', 'Who shaped the plan', '', el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Items added'),
|
|
225
|
+
hBars([['You', A.You / mxA, 'var(--acc)', A.You], ['Agent', A.Agent / mxA, 'var(--purple)', A.Agent], ['Team', A.Team / mxA, 'var(--muted-foreground)', A.Team]]),
|
|
226
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Changes made'),
|
|
227
|
+
hBars([['You', C.You / mxC, 'var(--acc)', C.You], ['Agent', C.Agent / mxC, 'var(--purple)', C.Agent], ['Team', C.Team / mxC, 'var(--muted-foreground)', C.Team]])),
|
|
228
|
+
card('clock', 'Latest changes', 'who, what and why', feed.length ? el('ul', { class: 'hx' }, feed.map((e) => { const o = orig(e.by); return el('li', null, el('span', { class: 'chip2', style: 'color:' + o.c }, c3ic(o.i, 'sm'), o.l),
|
|
229
|
+
el('span', null, (e.title || (((bd.items || []).find((x) => x.rid === e.rid) || {}).title) || e.rid) + ': ' + (e.kind === 'add' ? 'added' : e.kind === 'delete' ? 'deleted' : 'edited'), el('small', null, e.why)), el('span', { class: 'w' }, dayLabel(String(e.at).slice(0, 10)))); })) : el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No changes are recorded yet.')),
|
|
230
|
+
card('shield', 'Rules', '', el('ul', { class: 'chgl' },
|
|
231
|
+
el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, 'Every item records who changed it and why.')),
|
|
232
|
+
el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, 'Agent additions are proposals until you accept them.')),
|
|
233
|
+
el('li', { class: 'warn' }, c3ic('alert', 'sm'), el('span', null, 'A fourth scope change re-baselines the plan.')))),
|
|
234
|
+
];
|
|
235
|
+
frame(V, { head, left: [left], right });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const view = makeView('breakdown', { init: (V) => { V.dispose = () => { if (V.sheet) closeSheet(); clearTimeout(V.undoT); }; return load(V); }, render });
|
|
239
|
+
export const mount = view.mount;
|
|
240
|
+
export const dispose = view.dispose;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/* pm/c3-brief.js — the Briefing column below the deck (console3.html vBrief, line 1465 on) and what its rows DO.
|
|
2
|
+
* needRow(n, i, ctx) a "Needs you" row (needRow2, line 2345): a click opens its case file in place (the .xp panel), a swipe right
|
|
3
|
+
* or the key e takes the suggested move, a swipe left or n replies or defers, the keys j k Enter move and open.
|
|
4
|
+
* belowLine(rows, fn, kept) the "+4 below the line" fold that holds the needs past the fifth, and the "kept out" line
|
|
5
|
+
* reviewSection(r) "Review": the agent's drafts, one row each (draftRow2, line 2349): a click opens the draft and what it quotes,
|
|
6
|
+
* approving is a slide or a swipe after the draft was opened, "approve N skims" marks the low-risk ones
|
|
7
|
+
* handledRow(h) "6 handled for you": a fold with what the agent did and how to undo it (handled, hxHtml)
|
|
8
|
+
* trustCard(t) / footLine(f) "Data you can lean on" and the line under both columns
|
|
9
|
+
* caseFile(model, opts) the shared case file (c3-case.js), with the confidence figure left out when the data carries none
|
|
10
|
+
* toast(msg, undo) the mockup's toast (line 1166): 4.2 s, an Undo button
|
|
11
|
+
* Trust rules. A row acts only through a route that exists (POST /api/pm-items done or move, with the row's own item id). Anything else is
|
|
12
|
+
* a disabled control that says why. Anything outward only ever queues a draft: its slide is disabled, "no channel connected" (c3-case.js
|
|
13
|
+
* slideFor). The demo (c3-demo-brief-ix.js) turns on `BX.sim` and plugs in an agent: it simulates the other results in memory, and its
|
|
14
|
+
* writes are answered by the fetch wrapper in c3-demo.js. Real mode has neither, so a real row with no item id stays a read-only case. */
|
|
15
|
+
import { el } from './dom.js';
|
|
16
|
+
import { pmPost } from './http.js';
|
|
17
|
+
import { c3ic } from './pm-work-view.js';
|
|
18
|
+
import { toast } from './c3-toast.js';
|
|
19
|
+
import { renderCase } from './c3-case.js';
|
|
20
|
+
import { makeXp, attachExpand } from './c3-expand.js';
|
|
21
|
+
import { attachSwipe } from './c3-swipe.js';
|
|
22
|
+
import { needCaseOf, draftCaseOf, rawOption, registry, NO_ROUTE } from './briefing-model.js';
|
|
23
|
+
|
|
24
|
+
/* ── state: what is open, picked, seen, done. One store, keyed by item key, so a re-read of the briefing does not lose it. ───────────── */
|
|
25
|
+
export const BX = { open: new Set(), pick: {}, seen: {}, dst: {}, gone: {}, hundo: {}, cdone: {}, sim: false, agent: null };
|
|
26
|
+
export const setSim = (on) => { BX.sim = !!on; };
|
|
27
|
+
/** The demo plugs its agent in here: { has(key), info(key) -> { label }, state(key) -> 'idle'|'busy'|'out', mount(key, host, api), run(key, intent, text), tray(key) }. */
|
|
28
|
+
export const setAgent = (a) => { BX.agent = a || null; };
|
|
29
|
+
|
|
30
|
+
export const rowApi = new WeakMap(); // row element -> { kind, key, right(), left(), open() }
|
|
31
|
+
const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
32
|
+
const small = 'font-size:var(--fs-2)';
|
|
33
|
+
|
|
34
|
+
/* ── an avatar: a button that opens the person sheet when the person is known (the mockup's `av`, line 1058), else a plain chip ───────────── */
|
|
35
|
+
export const hooks = { person: null, go: null };
|
|
36
|
+
export function avatar(initial, name) {
|
|
37
|
+
const people = registry('people') || {};
|
|
38
|
+
const k = Object.keys(people).find((q) => (name && people[q].n === name) || (!name && people[q].n[0] === initial) || (name && name[0] === initial && people[q].n === String(name).split(' ')[0]));
|
|
39
|
+
if (!k) return el('span', { class: 'av', title: name || initial }, initial);
|
|
40
|
+
const p = people[k];
|
|
41
|
+
return el('button', { type: 'button', class: 'av', 'data-a': 'person', 'data-k': k, title: p.n + ' · ' + p.role, 'aria-label': p.n + ', ' + p.role, onclick: (e) => { e.stopPropagation(); if (hooks.person) hooks.person(k); } }, initial);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ── the toast: one shared module (c3-toast.js), re-exported so the views that import it from here keep working ── */
|
|
45
|
+
export { toast };
|
|
46
|
+
|
|
47
|
+
/* ── the case file ──────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
48
|
+
const hasConf = (m) => !!m && ((m.confidence !== undefined && m.confidence !== null) || (m.conf !== undefined && m.conf !== null));
|
|
49
|
+
function noConf(root) { // c3-case.js prints "Recommended · 0%" for a case with no figure; a card the engine sends without one says only "Recommended"
|
|
50
|
+
root.querySelectorAll('.opt.rec .rk').forEach((rk) => rk.replaceChildren(c3ic('spark', 'sm'), 'Recommended'));
|
|
51
|
+
root.querySelectorAll('.opt.rec .conf').forEach((n) => n.remove());
|
|
52
|
+
}
|
|
53
|
+
export function caseFile(model, opts = {}) {
|
|
54
|
+
const fig = hasConf(model);
|
|
55
|
+
const root = renderCase(model, { onGo: (g) => { if (hooks.go) hooks.go(g); }, ...opts, onPick: (id, o) => { if (!fig) noConf(root); if (opts.onPick) opts.onPick(id, o); } });
|
|
56
|
+
if (!fig) noConf(root);
|
|
57
|
+
return root;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const post = (path, body) => pmPost(path, body).catch(() => ({ ok: false }));
|
|
61
|
+
function leaveRow(row, then) {
|
|
62
|
+
if (reduced()) { then(); return; }
|
|
63
|
+
row.style.maxHeight = row.offsetHeight + 'px'; row.style.overflow = 'hidden';
|
|
64
|
+
row.style.transition = 'opacity .2s,transform .2s,max-height .26s .12s,padding .26s .12s,border-width .26s .12s';
|
|
65
|
+
void row.offsetHeight; row.classList.add('leave'); setTimeout(then, 340);
|
|
66
|
+
}
|
|
67
|
+
const unleave = (row) => { row.classList.remove('leave'); row.style.maxHeight = ''; row.style.overflow = ''; row.style.transition = ''; };
|
|
68
|
+
|
|
69
|
+
/* ── a "Needs you" row ──────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
70
|
+
const fuse = (left, col) => el('div', { class: 'chips2', style: 'margin-top:6px;gap:8px' }, el('div', { class: 'fuse' }, el('i', { style: 'width:' + Math.round(left.pct * 100) + '%;background:' + col })),
|
|
71
|
+
el('span', { class: 'mono', style: 'font-size:var(--fs-1);color:' + col }, left.text));
|
|
72
|
+
|
|
73
|
+
/** The case of a row once an agent move exists: the move leads as the recommended option (the mockup's agDecor, line 5126). */
|
|
74
|
+
function withAgent(base, label) {
|
|
75
|
+
const mv = { id: 'agmv', l: 'Let the agent act: ' + label, rec: 1, fx: [['spark', 'The suggested move, taken by an agent', 'ok'], ['shield', 'Inside the repo; outward only as a draft', 'ok']] };
|
|
76
|
+
const rest = (base.opts || base.options || []).map((q) => ({ ...q, rec: 0, recommended: false }));
|
|
77
|
+
return { ...base, opts: [mv, ...rest], options: undefined, cta: 'Slide to let the agent act' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function needRow(n, i, ctx = {}) {
|
|
81
|
+
const key = String(n.key || i);
|
|
82
|
+
const ag = BX.agent && BX.agent.has(key) ? BX.agent.info(key) : null;
|
|
83
|
+
const routed = !!n.itemId; // done and later go through the pm-items routes, with this id
|
|
84
|
+
const hot = n.p === 'P1'; const col = n.left ? (n.left.tone === 'warn' ? 'var(--warning)' : 'var(--acc)') : null;
|
|
85
|
+
const base0 = needCaseOf(n);
|
|
86
|
+
/* an option is carried out by a route (done or later, with the row's item id), by the demo's simulation, or by the agent; any other option is not available */
|
|
87
|
+
const gate = (m) => ({ ...m, options: undefined, opts: (m.opts || m.options || []).map((q) => ({ ...q, outward: !!q.outward || !(q.id === 'agmv' || routed || BX.sim) })) });
|
|
88
|
+
let xi = null; let row = null; let xp = null; let exp = null;
|
|
89
|
+
|
|
90
|
+
const doRoute = (act, msg) => {
|
|
91
|
+
if (!routed) { toast('Not available here: no route carries this out for this item.'); return; }
|
|
92
|
+
const gonePos = { parent: row.parentNode, next: row.nextSibling };
|
|
93
|
+
const call = act === 'done' ? post('/api/pm-items/done', { id: n.itemId }) : post('/api/pm-items/move', { id: n.itemId, list: 'later' });
|
|
94
|
+
BX.gone[key] = true;
|
|
95
|
+
leaveRow(row, () => { row.remove(); if (ctx.changed) ctx.changed(); });
|
|
96
|
+
call.then((r) => { if (r && r.ok === false) { delete BX.gone[key]; unleave(row); if (!row.isConnected && gonePos.parent) gonePos.parent.insertBefore(row, gonePos.next); if (ctx.changed) ctx.changed(); toast('That did not go through' + (r.error ? ': ' + r.error : '.')); } });
|
|
97
|
+
toast(msg || (act === 'done' ? 'Done' : 'Back Mon 09:00'), () => {
|
|
98
|
+
delete BX.gone[key];
|
|
99
|
+
post(act === 'done' ? '/api/pm-items/reopen' : '/api/pm-items/move', act === 'done' ? { id: n.itemId } : { id: n.itemId, list: 'triage' });
|
|
100
|
+
unleave(row); if (gonePos.parent && !row.isConnected) gonePos.parent.insertBefore(row, gonePos.next && gonePos.next.isConnected ? gonePos.next : null);
|
|
101
|
+
if (ctx.changed) ctx.changed();
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const right = () => {
|
|
105
|
+
if (ag) { if (BX.agent.state(key) === 'busy') toast('A session is already working on this.'); else BX.agent.run(key, 'move'); return; }
|
|
106
|
+
doRoute('done');
|
|
107
|
+
};
|
|
108
|
+
const left = () => {
|
|
109
|
+
if (ag) { if (BX.agent.state(key) === 'busy') toast('A session is already working on this.'); else BX.agent.tray(key); return; }
|
|
110
|
+
doRoute('defer');
|
|
111
|
+
};
|
|
112
|
+
const choose = ({ option }) => {
|
|
113
|
+
if (option.id === 'agmv') { if (ag && BX.agent.state(key) !== 'busy') toast('The agent is working on it. Anything outward stays a draft for your slide.'); right(); return; }
|
|
114
|
+
const raw = rawOption(base0, option.id) || {};
|
|
115
|
+
doRoute(raw.do === 'defer' ? 'defer' : 'done', raw.msg);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const api = { kind: 'need', key, right, left, open: () => exp && exp.open(), get row() { return row; }, redraw: () => build() };
|
|
119
|
+
const build = () => {
|
|
120
|
+
const st = ag ? BX.agent.state(key) : 'idle';
|
|
121
|
+
let m = gate(base0);
|
|
122
|
+
if (ag && st === 'idle') m = gate(withAgent(base0, ag.label));
|
|
123
|
+
else if (st === 'busy') m = { ...m, opts: [], options: undefined };
|
|
124
|
+
else if (ag && st === 'out') m = { ...m, opts: (m.opts || m.options || []).filter((q) => q.id !== 'agmv') };
|
|
125
|
+
const c = caseFile({ ...m, id: base0.id || 'need:' + key }, { picked: BX.pick[key], onPick: (id) => { BX.pick[key] = id; }, onConfirm: choose });
|
|
126
|
+
if (st === 'busy') c.classList.add('ag-busy');
|
|
127
|
+
if (ag) { const host = el('div', { class: 'c3ag', 'data-ag': key }); const sit = c.querySelector('.sit'); if (sit) sit.after(host); else c.prepend(host); BX.agent.mount(key, host, api); }
|
|
128
|
+
if (xi) xi.replaceChildren(c);
|
|
129
|
+
return c;
|
|
130
|
+
};
|
|
131
|
+
const c0 = build();
|
|
132
|
+
xp = makeXp(c0); xi = xp.firstChild;
|
|
133
|
+
|
|
134
|
+
const chips = [ag ? el('span', { class: 'chip2', style: 'gap:4px', title: 'Swipe right to let the agent act' }, c3ic('spark', 'sm'), 'agent move') : null,
|
|
135
|
+
n.p ? el('span', { class: 'fl ' + n.p.toLowerCase() }, n.p) : null, n.time ? el('span', { class: 'chip2' }, c3ic('clock', 'sm'), n.time) : null,
|
|
136
|
+
n.who ? avatar(n.who) : null, n.branch ? el('span', { class: 'chip2' }, c3ic('branch', 'sm'), n.branch) : null,
|
|
137
|
+
n.old ? el('span', { class: 'st warn' }, c3ic('clock', 'sm'), 'old') : null, n.src ? el('span', { class: 'tag' }, n.src) : null,
|
|
138
|
+
ag ? el('span', { class: 'st c3strip', 'data-sxs': key }) : null];
|
|
139
|
+
const act = ag || routed;
|
|
140
|
+
const sg = el('span', { class: 'sg', title: ag ? 'Drag right: the agent acts. Drag left: reply.' : routed ? 'Suggested: later (drag left)' : 'Not available here: no route carries a swipe out for this item.' },
|
|
141
|
+
el('span', { class: 'g l' + (ag ? '' : '') }, c3ic(ag ? 'reply' : 'moon')), el('span', { class: 'g r' + (ag || !routed ? ' y' : '') }, c3ic(ag ? 'spark' : 'check')));
|
|
142
|
+
row = el('li', { class: 'nrow', 'data-row': '', 'data-id': key, 'data-needs-row': key, 'data-a': 'expand' },
|
|
143
|
+
el('span', { class: 'ki' + (hot ? ' hot' : ''), title: n.kind, style: n.color ? 'color:' + n.color : null }, c3ic(n.icon || 'check')),
|
|
144
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, n.headline), n.left ? fuse(n.left, col) : null, el('div', { class: 'chips' }, ...chips.filter(Boolean)), xp), sg);
|
|
145
|
+
exp = attachExpand(row, xp, { key: 'need-' + key, initial: BX.open.has(key), onToggle: (on) => { if (on) BX.open.add(key); else BX.open.delete(key); } });
|
|
146
|
+
attachSwipe(row, { onRight: right, onLeft: left, labelRight: ag ? 'agent acts' : 'done', labelLeft: ag ? 'reply' : 'later', iconRight: ag ? 'spark' : 'check', iconLeft: ag ? 'reply' : 'moon', disabled: () => !act });
|
|
147
|
+
if (ag) { row.setAttribute('data-ag', '1'); }
|
|
148
|
+
rowApi.set(row, api);
|
|
149
|
+
return row;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── Review: a draft row and the section ───────────────────────────────────────────────────────────────────────────────────── */
|
|
153
|
+
const LEVEL = ['Read carefully', 'Read', 'Skim'];
|
|
154
|
+
function draftRow(x, ctx = {}) {
|
|
155
|
+
const id = String(x.id); const lv = x.lvl;
|
|
156
|
+
const col = lv === 0 ? 'var(--destructive)' : lv === 1 ? 'var(--warning)' : null;
|
|
157
|
+
const dots = x.n ? el('span', { class: 'src-dots', title: x.n + ' sources' + (x.old ? ', ' + x.old + ' old' : '') }, ...Array.from({ length: Math.min(x.n, 5) }, (_, q) => el('i', { class: q >= x.n - (x.old || 0) ? 'old' : '' }))) : el('span', { class: 'chip2' }, x.basis);
|
|
158
|
+
const chips = [x.out ? el('span', { class: 'chip2', title: 'Goes outside' }, c3ic('out', 'sm'), 'out') : null,
|
|
159
|
+
x.to ? avatar(x.to, x.toName ? x.toName.split(' ')[0] : null) : null,
|
|
160
|
+
...(x.docs || []).map((d) => el('span', { class: 'chip2 hide-ph', title: 'Document' }, c3ic(d, 'sm'))), dots,
|
|
161
|
+
x.conflict ? el('span', { class: 'st warn', title: 'Jira and the PRD disagree on the ship date' }, c3ic('alert', 'sm'), 'dates differ') : null,
|
|
162
|
+
x.sess ? el('span', { class: 'chip2 hide-ph', title: 'A session wrote this' }, c3ic('branch', 'sm')) : null];
|
|
163
|
+
let row = null; let exp = null; const acts = el('div', { class: 'acts', style: 'position:static' });
|
|
164
|
+
const set = (s, msg) => {
|
|
165
|
+
BX.dst[id] = s; draw();
|
|
166
|
+
toast(msg, () => { delete BX.dst[id]; draw(); if (ctx.changed) ctx.changed(); });
|
|
167
|
+
if (ctx.changed) ctx.changed();
|
|
168
|
+
};
|
|
169
|
+
/* marking a draft ready or sending it back has no route on the server; the demo simulates it, real mode cannot */
|
|
170
|
+
const ready = () => { if (!BX.sim) { toast('Not available here: no route marks a draft ready.'); return; } set('ready', 'Marked ready. Nothing was sent or changed in any tool.'); };
|
|
171
|
+
const back = () => { if (!BX.sim) { toast('Not available here: no route sends a draft back.'); return; } set('back', 'Sent back'); };
|
|
172
|
+
const right = () => { if (BX.dst[id]) return; if (!BX.seen[id]) { toast('Open the draft first. Approving without reading is how errors go out.'); return; } ready(); };
|
|
173
|
+
const left = () => { if (BX.dst[id]) return; back(); };
|
|
174
|
+
const model = draftCaseOf(x);
|
|
175
|
+
const gated = { ...model, opts: (model.opts || model.options || []).map((q) => ({ ...q, outward: q.outward || !BX.sim })), outboxReason: BX.sim ? model.outboxReason : NO_ROUTE };
|
|
176
|
+
const c = caseFile({ ...gated, id: 'draft:' + id }, { picked: BX.pick['d' + id], onPick: (o) => { BX.pick['d' + id] = o; }, onConfirm: ({ option }) => { const raw = rawOption(model, option.id) || {}; if (raw.do === 'back') back(); else ready(); } });
|
|
177
|
+
const xp = makeXp(c);
|
|
178
|
+
function draw() {
|
|
179
|
+
const s = BX.dst[id];
|
|
180
|
+
acts.replaceChildren(...(s ? [el('span', { class: 'st ok' }, c3ic('check', 'sm'), s === 'ready' ? 'ready' : 'sent back'),
|
|
181
|
+
el('span', { class: 'qa' }, el('button', { type: 'button', class: 'ib sm', title: 'Undo', 'aria-label': 'Undo', onclick: (e) => { e.stopPropagation(); delete BX.dst[id]; draw(); if (ctx.changed) ctx.changed(); } }, c3ic('undo', 'sm')))] : [c3ic('next', 'sm', 'chev')]));
|
|
182
|
+
const sgn = row && row.querySelector(':scope > .sg'); if (sgn) sgn.hidden = !!s;
|
|
183
|
+
}
|
|
184
|
+
const sg = x.rec === 'o' ? el('span', { class: 'sg', title: 'Read it first' }, el('span', { class: 'g y' }, c3ic('eye')))
|
|
185
|
+
: el('span', { class: 'sg', title: 'Suggested: approve (drag right)' }, el('span', { class: 'g l' }, c3ic('undo')), el('span', { class: 'g r y' }, c3ic('check')));
|
|
186
|
+
row = el('li', { class: 'drow', 'data-row': '', 'data-id': id, 'data-a': 'peek', 'data-draft-row': id },
|
|
187
|
+
el('span', { class: 'ki', title: LEVEL[lv], style: col ? 'color:' + col : null }, c3ic(lv === 0 ? 'alert' : 'eye')),
|
|
188
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, x.t), el('div', { class: 'chips' }, ...chips.filter(Boolean)), xp), acts, sg);
|
|
189
|
+
exp = attachExpand(row, xp, { key: 'draft-' + id, initial: BX.open.has('d' + id), onToggle: (on) => { if (on) { BX.open.add('d' + id); BX.seen[id] = 1; } else BX.open.delete('d' + id); } });
|
|
190
|
+
attachSwipe(row, { onRight: right, onLeft: left, labelRight: 'ready', labelLeft: 'send back', iconRight: 'check', iconLeft: 'undo', disabled: () => !!BX.dst[id] });
|
|
191
|
+
draw();
|
|
192
|
+
rowApi.set(row, { kind: 'draft', key: id, right, left, open: () => exp.open(), ready, set });
|
|
193
|
+
return row;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function reviewSection(r) {
|
|
197
|
+
const rows = r.rows.slice().sort((a, b) => a.lvl - b.lvl);
|
|
198
|
+
const ul = el('ul', { class: 'list', role: 'list' });
|
|
199
|
+
const skimBtn = el('button', { type: 'button', class: 'chip2 lk', 'data-a': 'skimready', title: BX.sim ? 'Mark every low-risk draft ready. Nothing is sent.' : 'Not available here: no route marks a draft ready.',
|
|
200
|
+
onclick: () => {
|
|
201
|
+
if (!BX.sim) { toast('Not available here: no route marks a draft ready.'); return; }
|
|
202
|
+
const sk = rows.filter((x) => x.lvl === 2 && !BX.dst[String(x.id)]);
|
|
203
|
+
sk.forEach((x) => { BX.dst[String(x.id)] = 'ready'; });
|
|
204
|
+
redraw();
|
|
205
|
+
toast(sk.length + ' low-risk drafts marked ready. Nothing was sent.', () => { sk.forEach((x) => { delete BX.dst[String(x.id)]; }); redraw(); });
|
|
206
|
+
} }, c3ic('eye', 'sm'), 'approve ' + r.skims + ' skims');
|
|
207
|
+
const redraw = () => ul.replaceChildren(...rows.map((x) => draftRow(x, { changed: update })));
|
|
208
|
+
const update = () => { const n = rows.filter((x) => x.lvl === 2 && !BX.dst[String(x.id)]).length; skimBtn.hidden = n < 2; skimBtn.lastChild.textContent = 'approve ' + n + ' skims'; };
|
|
209
|
+
redraw(); update();
|
|
210
|
+
return el('section', { class: 'card sect', 'data-c3pg': 'review', 'aria-label': 'Review' },
|
|
211
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), 'Review'),
|
|
212
|
+
el('span', { class: 'chips2' }, r.working ? el('span', { class: 'chip2', title: 'Agents working now' }, el('span', { class: 'dots' }, el('i'), el('i'), el('i')), r.working + ' working') : null, r.skims > 1 ? skimBtn : null)), ul);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ── folds: the mockup's `.xp` open and close, for the fold buttons (data-a=hand, more, trust) ────────────────────────────── */
|
|
216
|
+
function fold(btn, body, onToggle) {
|
|
217
|
+
const xp = makeXp(body);
|
|
218
|
+
btn.addEventListener('click', () => { const on = !xp.classList.contains('open'); xp.classList.toggle('open', on); btn.setAttribute('aria-expanded', String(on)); if (onToggle) onToggle(on); });
|
|
219
|
+
return xp;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function belowLine(rows, drawRow, kept) {
|
|
223
|
+
const out = [];
|
|
224
|
+
if (rows.length) {
|
|
225
|
+
const body = el('ul', { class: 'list', role: 'list' }, ...rows.map(drawRow));
|
|
226
|
+
const btn = el('button', { type: 'button', class: 'rowbtn', style: 'padding:8px 2px;font-size:var(--fs-3)', 'aria-expanded': 'false', 'data-a': 'more', 'data-c3pg': 'below-line' },
|
|
227
|
+
el('span', { class: 'chips2 dim' }, c3ic('next', 'sm', 'chev'), '+' + rows.length + ' below the line'));
|
|
228
|
+
out.push(btn, fold(btn, body));
|
|
229
|
+
}
|
|
230
|
+
if (kept) out.push(el('div', { class: 'chips2 dim', style: 'font-size:var(--fs-2);padding:6px 2px 0', 'data-c3pg': 'kept-out' }, c3ic('filter', 'sm'), kept));
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ── handled for you: a fold, each row opens to four lines, and an undo ────────────────────────────────────────────────────── */
|
|
235
|
+
export function handledRow(h) {
|
|
236
|
+
const total = h.rows.length;
|
|
237
|
+
const count = el('b', { style: 'font-size:var(--fs-3)' }, total + ' handled for you');
|
|
238
|
+
const recount = () => { count.textContent = h.rows.filter((_, i) => !BX.hundo[i]).length + ' handled for you'; };
|
|
239
|
+
const list = el('ul', { class: 'list', role: 'list' }, ...h.rows.map((x, i) => {
|
|
240
|
+
const d = el('div', { class: 'hx' }, ...x.d.map((t, q) => el('div', null, c3ic(['bolt', 'shield', 'check', 'undo'][q], 'sm'), el('span', null, t))));
|
|
241
|
+
const xp = makeXp(d);
|
|
242
|
+
const undo = el('button', { type: 'button', class: 'ib sm', title: x.d[3] ? 'Undo: ' + x.d[3] : 'Undo', 'aria-label': 'Undo: ' + x.t, 'data-a': 'hundo', disabled: !BX.sim,
|
|
243
|
+
onclick: (e) => {
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
BX.hundo[i] = !BX.hundo[i]; row.classList.toggle('undone', !!BX.hundo[i]); recount();
|
|
246
|
+
if (BX.hundo[i]) toast('Undone: ' + x.t, () => { BX.hundo[i] = false; row.classList.remove('undone'); recount(); });
|
|
247
|
+
} }, c3ic('undo', 'sm'));
|
|
248
|
+
const row = el('li', { class: 'nrow hrow' + (BX.hundo[i] ? ' undone' : ''), 'data-row': '', 'data-a': 'expand', 'data-handled-row': String(i) },
|
|
249
|
+
el('span', { class: 'ki', style: 'width:26px;height:26px' }, c3ic('spark', 'sm')), el('div', { class: 'nb', style: 'font-size:var(--fs-3)' }, x.t, xp), el('div', { class: 'qa' }, undo));
|
|
250
|
+
attachExpand(row, xp, { key: 'handled-' + i });
|
|
251
|
+
return row;
|
|
252
|
+
}));
|
|
253
|
+
const btn = el('button', { type: 'button', class: 'rowbtn', 'aria-expanded': 'false', 'data-a': 'hand' },
|
|
254
|
+
el('span', { class: 'chips2' }, c3ic('bolt', 'sm'), count, el('span', { class: 'dim', style: 'font-size:var(--fs-3)' }, h.note)), c3ic('next', 'sm', 'chev'));
|
|
255
|
+
recount();
|
|
256
|
+
return el('section', { class: 'card sect', 'data-c3pg': 'handled' }, btn, fold(btn, list));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function trustCard(t) {
|
|
260
|
+
const body = el('div', { style: 'padding:0 14px 12px;display:grid;gap:8px' }, el('div', { class: 'chips2 st warn', style: small }, c3ic('clock', 'sm'), t.stale),
|
|
261
|
+
...t.items.map((c) => el('div', { class: 'card', style: 'padding:10px;gap:6px;box-shadow:none' }, el('b', { style: 'font-size:var(--fs-3)' }, c.fact),
|
|
262
|
+
el('div', { class: 'chips2' }, el('span', { class: 'cmpill win' }, c.win), el('span', { class: 'mono dim' }, '≠'), el('span', { class: 'cmpill' }, c.lose)),
|
|
263
|
+
el('div', { class: 'chips2 st ok', style: small }, c3ic('check', 'sm'), 'fix drafted. It is in Review.'))));
|
|
264
|
+
const btn = el('button', { type: 'button', class: 'rowbtn', style: 'padding:12px 14px', 'aria-expanded': 'false', 'data-a': 'trust' },
|
|
265
|
+
el('span', { class: 'chips2' }, c3ic('shield'), el('b', { style: 'font-size:var(--fs-3)' }, 'Data you can lean on')),
|
|
266
|
+
el('span', { class: 'chips2' }, el('span', { class: 'chip2' }, t.fresh), el('span', { class: 'st warn' }, c3ic('alert', 'sm'), t.items.length + ' mismatches'), el('span', { class: 'st ok' }, c3ic('check', 'sm'), 'fixes drafted'), c3ic('next', 'sm', 'chev')));
|
|
267
|
+
return el('section', { class: 'card', style: 'gap:0;padding:0', 'data-c3pg': 'trust' }, btn, fold(btn, body));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export const footLine = (f) => el('div', { class: 'chips2 dim', style: 'font-size:var(--fs-2);justify-content:space-between', 'data-c3pg': 'foot' }, el('span', null, f.left), el('span', { class: 'mono hide-ph' }, f.right));
|
|
271
|
+
|
|
272
|
+
/* Reflect lens (console3.html vBrief, line 1470): the "Needs you" panel is titled "Carries over", with the same rows and count. */
|
|
273
|
+
export function setNeedsTitle(box, reflect) {
|
|
274
|
+
const h = box && box.querySelector('.sh2 h2'); if (!h) return;
|
|
275
|
+
const t = [...h.childNodes].find((n) => n.nodeType === 3);
|
|
276
|
+
if (t) t.textContent = (reflect ? 'Carries over' : 'Needs you') + ' ';
|
|
277
|
+
box.setAttribute('aria-label', reflect ? 'Carries over' : 'Needs you');
|
|
278
|
+
}
|