create-pmos 0.129.0 → 0.131.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 +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-one-home-test.js +8 -3
- package/workspace/c3-work-test.js +200 -0
- package/workspace/fixtures/one-home-baseline.json +0 -91
- package/workspace/guide/briefing.md +60 -0
- package/workspace/guide/calendar-popup.md +46 -0
- package/workspace/guide/console-calendar.md +65 -0
- package/workspace/guide/index.md +4 -0
- package/workspace/guide/layout.md +1 -0
- package/workspace/guide/my-work.md +76 -1
- package/workspace/lib/calendar-layers.js +372 -0
- package/workspace/lib/pm-items.js +71 -6
- package/workspace/lib/report-needs.js +1 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-brief-page.js +187 -0
- package/workspace/lib/routes-calendar-views.js +112 -3
- package/workspace/lib/routes-pm-items.js +123 -13
- package/workspace/lib/work-focus.js +106 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-brief-data.js +11 -0
- package/workspace/public/pm/c3-brief-needs.js +145 -0
- package/workspace/public/pm/c3-brief-pulse.js +70 -0
- package/workspace/public/pm/c3-brief-today.js +44 -0
- package/workspace/public/pm/c3-calendar.js +392 -0
- package/workspace/public/pm/c3-calpop.js +103 -0
- package/workspace/public/pm/c3-demo-brief-page.js +54 -0
- package/workspace/public/pm/c3-demo-cal4.js +114 -0
- package/workspace/public/pm/c3-demo-data.js +79 -77
- package/workspace/public/pm/c3-demo-misc2.js +4 -16
- package/workspace/public/pm/c3-demo-work-ix.js +19 -67
- package/workspace/public/pm/c3-demo.js +11 -4
- package/workspace/public/pm/c3-shell.js +27 -149
- package/workspace/public/pm/calendar-views.js +11 -3
- package/workspace/public/pm/components.css +1 -0
- package/workspace/public/pm/console3.css +198 -0
- package/workspace/public/pm/pm-work-view.js +364 -172
- package/workspace/public/pm/work-place.js +58 -0
- package/workspace/test-files.json +7 -1
- package/workspace/test-lists/calendar-layers.json +5 -0
- package/workspace/test-lists/calendar-time-page.json +5 -0
- package/workspace/test-lists/calpop.json +4 -0
- package/workspace/test-lists/console3-work-page.json +9 -0
- package/workspace/public/pm/c3-demo-work2.js +0 -13
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* work-focus.js — Focus on the Work page: up to three jobs PMOS suggests for today (console3 PRD R4).
|
|
3
|
+
*
|
|
4
|
+
* Rules first, no model call. Pure functions; the route layer reads and writes focus.jsonl.
|
|
5
|
+
*
|
|
6
|
+
* focusSuggestions(workToday, jobs, state, todayIso)
|
|
7
|
+
* Groups the Today items by their job (an item with no job is its own group, key 'item:<id>'; a job group's key
|
|
8
|
+
* is 'job:<jobId>'), scores each group, and returns the best three as { key, title, why, item_ids, job }.
|
|
9
|
+
* Score per item: overdue 3 per day (at most 9), due today 2, flagged 2, P1 2, offline 1. A group scores the
|
|
10
|
+
* sum of its items. A key the PM accepted or dismissed today is not suggested again.
|
|
11
|
+
*
|
|
12
|
+
* foldFocus(lines, todayIso)
|
|
13
|
+
* The focus.jsonl events 'accepted', 'dismissed' and 'cleared' carry `day` (YYYY-MM-DD, set by the route from the
|
|
14
|
+
* server date). Only today's events count, so Focus starts empty each day. Returns { accepted, dismissed }.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_SUGGESTIONS = 3;
|
|
17
|
+
const DAY_MS = 86400000;
|
|
18
|
+
|
|
19
|
+
const dayNumber = (isoDay) => Math.floor(Date.parse(`${isoDay}T00:00:00Z`) / DAY_MS);
|
|
20
|
+
|
|
21
|
+
function parts(item, todayIso) {
|
|
22
|
+
const overdueDays = item.due && item.due < todayIso ? dayNumber(todayIso) - dayNumber(item.due) : 0;
|
|
23
|
+
return {
|
|
24
|
+
overdueDays,
|
|
25
|
+
dueToday: item.due === todayIso,
|
|
26
|
+
flagged: item.flagged === true,
|
|
27
|
+
p1: item.priority === 'P1',
|
|
28
|
+
offline: item.kind === 'offline',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function itemScore(p) {
|
|
33
|
+
return Math.min(p.overdueDays * 3, 9) + (p.dueToday ? 2 : 0) + (p.flagged ? 2 : 0) + (p.p1 ? 2 : 0) + (p.offline ? 1 : 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Plain words, no dashes: "2 items overdue, 1 flagged".
|
|
37
|
+
function whyText(list) {
|
|
38
|
+
const n = (f) => list.filter(f).length;
|
|
39
|
+
const overdue = n((p) => p.overdueDays > 0);
|
|
40
|
+
const bits = [];
|
|
41
|
+
if (overdue) bits.push(`${overdue} ${overdue === 1 ? 'item' : 'items'} overdue`);
|
|
42
|
+
if (n((p) => p.dueToday)) bits.push(`${n((p) => p.dueToday)} due today`);
|
|
43
|
+
if (n((p) => p.flagged)) bits.push(`${n((p) => p.flagged)} flagged`);
|
|
44
|
+
if (n((p) => p.p1)) bits.push(`${n((p) => p.p1)} at P1`);
|
|
45
|
+
if (n((p) => p.offline)) bits.push(`${n((p) => p.offline)} outside PMOS`);
|
|
46
|
+
return bits.length ? bits.join(', ') : 'Planned for today';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function jobTitle(job) {
|
|
50
|
+
const t = job && (job.want || job.when || job.so);
|
|
51
|
+
return t ? String(t).trim() : 'A standing job';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function focusSuggestions(workToday, jobs, state, todayIso) {
|
|
55
|
+
const blocked = new Set([...(state && state.accepted ? state.accepted.map((a) => a.key) : []), ...(state && state.dismissed ? state.dismissed : [])]);
|
|
56
|
+
const jobById = new Map((jobs || []).map((j) => [j.id, j]));
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
for (const item of workToday || []) {
|
|
59
|
+
const job = item.job && jobById.has(item.job) ? item.job : null;
|
|
60
|
+
const key = job ? `job:${job}` : `item:${item.id}`;
|
|
61
|
+
if (!groups.has(key)) groups.set(key, { key, job, members: [] });
|
|
62
|
+
const p = parts(item, todayIso);
|
|
63
|
+
groups.get(key).members.push({ item, p, score: itemScore(p) });
|
|
64
|
+
}
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const g of groups.values()) {
|
|
67
|
+
if (blocked.has(g.key)) continue;
|
|
68
|
+
g.members.sort((a, b) => b.score - a.score);
|
|
69
|
+
out.push({
|
|
70
|
+
key: g.key,
|
|
71
|
+
score: g.members.reduce((sum, m) => sum + m.score, 0),
|
|
72
|
+
earliest: g.members.map((m) => m.item.due || '9999').sort()[0],
|
|
73
|
+
suggestion: {
|
|
74
|
+
key: g.key,
|
|
75
|
+
title: g.job ? jobTitle(jobById.get(g.job)) : g.members[0].item.title,
|
|
76
|
+
why: whyText(g.members.map((m) => m.p)),
|
|
77
|
+
item_ids: g.members.map((m) => m.item.id),
|
|
78
|
+
job: g.job,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
83
|
+
out.sort((a, b) => b.score - a.score || cmp(a.earliest, b.earliest) || cmp(a.key, b.key));
|
|
84
|
+
return out.slice(0, MAX_SUGGESTIONS).map((o) => o.suggestion);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function foldFocus(lines, todayIso) {
|
|
88
|
+
const accepted = new Map();
|
|
89
|
+
const dismissed = new Set();
|
|
90
|
+
for (const ev of lines || []) {
|
|
91
|
+
if (!ev || !ev.key || ev.day !== todayIso) continue;
|
|
92
|
+
if (ev.event === 'accepted') {
|
|
93
|
+
dismissed.delete(ev.key);
|
|
94
|
+
accepted.set(ev.key, { key: ev.key, title: ev.title, why: ev.why, item_ids: ev.item_ids || [], job: ev.job || null });
|
|
95
|
+
} else if (ev.event === 'dismissed') {
|
|
96
|
+
accepted.delete(ev.key);
|
|
97
|
+
dismissed.add(ev.key);
|
|
98
|
+
} else if (ev.event === 'cleared') {
|
|
99
|
+
accepted.delete(ev.key);
|
|
100
|
+
dismissed.delete(ev.key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { accepted: [...accepted.values()], dismissed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { MAX_SUGGESTIONS, focusSuggestions, foldFocus };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.131.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.131.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* pm/c3-brief-data.js — the one read behind the Briefing's Today agenda and the own-item rows of Needs you: GET /api/brief-page
|
|
2
|
+
* (lib/routes-brief-page.js). Both sections subscribe; one request answers both. A failed read is kept as { ok:false, error } so each
|
|
3
|
+
* section can say so; nothing here invents a row. */
|
|
4
|
+
import { pmGet } from './http.js';
|
|
5
|
+
|
|
6
|
+
let cache = null; const subs = new Set();
|
|
7
|
+
export function onBriefPage(fn) { subs.add(fn); if (cache) fn(cache); return () => subs.delete(fn); }
|
|
8
|
+
export function loadBriefPage() {
|
|
9
|
+
return pmGet('/api/brief-page').then((j) => (j && j.ok !== false ? j : { ok: false, error: (j && j.error) || 'unreadable' }), (e) => ({ ok: false, error: String((e && e.message) || e) }))
|
|
10
|
+
.then((j) => { cache = j; subs.forEach((f) => f(j)); return j; });
|
|
11
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* pm/c3-brief-needs.js — the Briefing's "Needs you" (PRD briefing-brief R3, R4, R6).
|
|
2
|
+
* needsSection(S) the section: own items first (overdue, most late first; then due today; then outside PMOS), then the decisions only the PM
|
|
3
|
+
* can make (the briefing's needs in the demo, the engine cards that carry a move or are Critical in real mode: c3-brief.js needRow).
|
|
4
|
+
* itemRow(row, ctx) a row of an own item. It opens in place (c3-expand). Its actions write to the item's home record, through the pm-items routes:
|
|
5
|
+
* Done POST /api/pm-items/done { id, outcome? }
|
|
6
|
+
* Not yet POST /api/pm-items/notyet { id, when: 'later-today' | 'tomorrow' | 'next-week' } the item returns on that date
|
|
7
|
+
* Drop POST /api/pm-items/remove { id } (Undo: POST /api/pm-items/undo)
|
|
8
|
+
* An outside-PMOS item (talk to, meet, call, ask) takes an optional one-line outcome. When PMOS holds evidence ("a meeting with Ana is
|
|
9
|
+
* booked Tue 10:00") it asks "Looks done: ... Confirm?". It never marks the item done itself: only a press on Done or Confirm does.
|
|
10
|
+
* The own-item rows are built again only when their keys change, so a re-read does not close an open row. An open row is kept in BX.open. */
|
|
11
|
+
import { el } from './dom.js';
|
|
12
|
+
import { c3ic } from './pm-work-view.js';
|
|
13
|
+
import { pmPost } from './http.js';
|
|
14
|
+
import { makeXp, attachExpand } from './c3-expand.js';
|
|
15
|
+
import { needRow, belowLine, BX, toast } from './c3-brief.js';
|
|
16
|
+
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
17
|
+
import { cardView, TAG_STYLE } from './briefing-model.js';
|
|
18
|
+
import { onBriefPage } from './c3-brief-data.js';
|
|
19
|
+
import { button, ensureComponentsCss } from './primitives.js';
|
|
20
|
+
|
|
21
|
+
const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
const post = (path, body) => pmPost(path, body).catch(() => ({ ok: false }));
|
|
23
|
+
export const WHEN = [['later-today', 'Later today'], ['tomorrow', 'Tomorrow'], ['next-week', 'Next week']];
|
|
24
|
+
const WHEN_TOAST = { 'later-today': 'Back later today', tomorrow: 'Back tomorrow', 'next-week': 'Back next week' };
|
|
25
|
+
|
|
26
|
+
function leave(row, then) {
|
|
27
|
+
if (reduced()) { then(); return; }
|
|
28
|
+
row.classList.add('leave'); setTimeout(then, 340);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function itemRow(r, ctx = {}) {
|
|
32
|
+
const key = r.key; const offline = !!r.offline;
|
|
33
|
+
const tone = r.kind === 'overdue' ? 't-bad' : offline ? 't-ink' : 't-warn';
|
|
34
|
+
ensureComponentsCss();
|
|
35
|
+
const late = r.days_late > 0 ? (r.days_late === 1 ? '1 day late' : r.days_late + ' days late') : null;
|
|
36
|
+
let row = null; let busy = false;
|
|
37
|
+
const out = el('input', { type: 'text', class: 'ioutcome', 'data-outcome': '', maxlength: '160', placeholder: offline ? 'What happened? One line, optional' : 'Note, optional', 'aria-label': 'Outcome, optional' });
|
|
38
|
+
|
|
39
|
+
const write = (call, okText, undo) => {
|
|
40
|
+
if (busy) return; busy = true;
|
|
41
|
+
call().then((res) => {
|
|
42
|
+
busy = false;
|
|
43
|
+
if (!res || res.ok === false) { toast('That did not go through' + (res && res.error ? ': ' + res.error : '.')); return; }
|
|
44
|
+
BX.open.delete(key);
|
|
45
|
+
leave(row, () => { row.remove(); if (ctx.changed) ctx.changed(); });
|
|
46
|
+
toast(okText, undo ? () => { post(undo.path, undo.body).then(() => { if (ctx.reload) ctx.reload(); }); } : undefined);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const done = (why) => {
|
|
50
|
+
const o = out.value.trim() || why || '';
|
|
51
|
+
write(() => post('/api/pm-items/done', { id: r.id, ...(o ? { outcome: o } : {}) }), 'Marked done', { path: '/api/pm-items/reopen', body: { id: r.id } });
|
|
52
|
+
};
|
|
53
|
+
const notYet = (when) => write(() => post('/api/pm-items/notyet', { id: r.id, when }), WHEN_TOAST[when]);
|
|
54
|
+
const drop = () => write(() => post('/api/pm-items/remove', { id: r.id }), 'Dropped', { path: '/api/pm-items/undo', body: { id: r.id } });
|
|
55
|
+
|
|
56
|
+
const btn = (label, act, fn, extra) => button({ label, onClick: (e) => { e.stopPropagation(); fn(); }, attrs: { 'data-act': act, ...(extra || {}) } });
|
|
57
|
+
const panel = el('div', { class: 'ip', 'data-item-panel': r.id },
|
|
58
|
+
r.evidence ? el('div', { class: 'looks', 'data-evidence': '', role: 'note' }, c3ic('eye', 'sm'), el('span', null, 'Looks done: ' + r.evidence.text + '. Confirm?'),
|
|
59
|
+
btn('Yes, it is done', 'confirm', () => done('Confirmed: ' + r.evidence.text))) : null,
|
|
60
|
+
out,
|
|
61
|
+
el('div', { class: 'iacts' },
|
|
62
|
+
btn('Done', 'done', () => done('')),
|
|
63
|
+
el('span', { class: 'ny', role: 'group', 'aria-label': 'Not yet: ask again' }, el('span', { class: 'dim c3s2' }, 'Not yet, ask again:'),
|
|
64
|
+
...WHEN.map(([w, l]) => btn(l, 'notyet', () => notYet(w), { 'data-when': w }))),
|
|
65
|
+
btn('Drop', 'drop', drop)));
|
|
66
|
+
const xp = makeXp(panel);
|
|
67
|
+
const chips = [late ? el('span', { class: 'st warn', 'data-late': String(r.days_late) }, c3ic('clock', 'sm'), late) : null,
|
|
68
|
+
r.kind === 'due-today' ? el('span', { class: 'chip2' }, c3ic('clock', 'sm'), 'Due today') : null,
|
|
69
|
+
offline ? el('span', { class: 'tag', 'data-offline': '' }, 'Outside PMOS') : el('span', { class: 'tag' }, 'Yours'),
|
|
70
|
+
r.person ? el('span', { class: 'chip2' }, c3ic('user', 'sm'), r.person) : null,
|
|
71
|
+
r.priority ? el('span', { class: 'fl ' + String(r.priority).toLowerCase() }, r.priority) : null,
|
|
72
|
+
r.evidence ? el('span', { class: 'st ok', 'data-looks-done': '' }, c3ic('eye', 'sm'), 'Looks done') : null];
|
|
73
|
+
row = el('li', { class: 'nrow', 'data-row': '', 'data-id': key, 'data-needs-row': key, 'data-item-id': r.id, 'data-ref-row': r.id, 'data-kind': r.kind, 'data-a': 'expand' },
|
|
74
|
+
el('span', { class: 'ki ' + tone + (r.kind === 'overdue' ? ' hot' : ''), title: r.kind }, c3ic(r.kind === 'overdue' ? 'alert' : offline ? 'user' : 'clock')),
|
|
75
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, r.headline), el('div', { class: 'chips' }, ...chips.filter(Boolean)), xp));
|
|
76
|
+
attachExpand(row, xp, { key: 'item-' + key, initial: BX.open.has(key), onToggle: (on) => { if (on) BX.open.add(key); else BX.open.delete(key); } });
|
|
77
|
+
return row;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function needsSection(S) {
|
|
81
|
+
const count = el('span', { class: 'bdg g' }, '0');
|
|
82
|
+
const quick = el('span', { class: 'chips2 dim mono c3s2' });
|
|
83
|
+
const own = el('ul', { class: 'list', role: 'list', 'data-needs-own': '' });
|
|
84
|
+
const ownNote = el('div');
|
|
85
|
+
const list = el('ul', { class: 'list', role: 'list' });
|
|
86
|
+
const extra = el('div');
|
|
87
|
+
const alertHost = el('div');
|
|
88
|
+
const box = el('section', { class: 'card sect', 'data-c3pg': 'needs', 'aria-label': 'Needs you' },
|
|
89
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('inbox', 'sm'), 'Needs you ', count)), alertHost, ownNote, own, list, extra);
|
|
90
|
+
let sig = ''; let last = null; let ownSig = ''; let page = null;
|
|
91
|
+
const nothing = () => el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you.');
|
|
92
|
+
const recount = () => {
|
|
93
|
+
const n = box.querySelectorAll('.nrow[data-needs-row]').length;
|
|
94
|
+
count.textContent = String(n);
|
|
95
|
+
const f = list.querySelector('.fold');
|
|
96
|
+
if (!n && !f) list.replaceChildren(nothing());
|
|
97
|
+
else if (n && f && list.children.length === 1 && f.textContent.startsWith('Nothing needs')) f.remove();
|
|
98
|
+
};
|
|
99
|
+
const ctx = { changed: recount };
|
|
100
|
+
|
|
101
|
+
const fillOwn = (j) => {
|
|
102
|
+
page = j;
|
|
103
|
+
if (!j || j.ok === false) { ownNote.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status' }, 'Your own items could not be read: ' + ((j && j.error) || 'unreadable'))); return; }
|
|
104
|
+
ownNote.replaceChildren();
|
|
105
|
+
const rows = (j.needs || []).filter((r) => r.kind !== 'decision');
|
|
106
|
+
const g = JSON.stringify(rows.map((r) => [r.key, r.headline, r.days_late, !!r.evidence]));
|
|
107
|
+
if (g === ownSig) return; ownSig = g;
|
|
108
|
+
own.replaceChildren(...rows.map((r) => itemRow(r, { ...ctx, reload: () => import('./c3-brief-data.js').then((m) => m.loadBriefPage()) })));
|
|
109
|
+
recount();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const fill = (b) => {
|
|
113
|
+
last = b;
|
|
114
|
+
const inc = !!S.incident;
|
|
115
|
+
let needs = b && Array.isArray(b.needs) && b.needs.length ? b.needs.filter((n) => !BX.gone[n.key] && (!inc || n.hard)) : null;
|
|
116
|
+
const decide = ((b && b.cards) || []).filter((c) => c && (c.move || c.tag === 'Critical'));
|
|
117
|
+
const key = needs ? needs.map((n) => n.key + n.headline) : decide.map((c) => (c.key || '') + c.headline);
|
|
118
|
+
if (needs && S.pinNeed) { const at = needs.findIndex((n) => n.key === S.pinNeed); if (at > 0) needs.unshift(...needs.splice(at, 1)); }
|
|
119
|
+
const g = JSON.stringify([key, inc, !!(b && b.kept_out), S.pinNeed || '']);
|
|
120
|
+
if (g === sig) return; sig = g;
|
|
121
|
+
alertHost.replaceChildren(...(inc ? [el('div', { class: 'incident', role: 'status', 'data-c3pg': 'incident' }, c3ic('alert', 'lg'), el('div', null, el('b', null, 'Card payments failing since 07:40'), el('div', { class: 'dim c3s3' }, '12% of checkouts. Sam on call. Only deadlines shown.')))] : []));
|
|
122
|
+
if (needs) {
|
|
123
|
+
if (!quick.isConnected) box.querySelector('.sh2').append(quick);
|
|
124
|
+
quick.textContent = ''; quick.append(c3ic('bolt', 'sm'), needs.filter((n) => n.quick).length + ' under 2 min');
|
|
125
|
+
list.replaceChildren(...needs.slice(0, 5).map((n, i) => needRow(n, i, ctx)));
|
|
126
|
+
extra.replaceChildren(...(inc ? [] : belowLine(needs.slice(5), (n, i) => needRow(n, i + 5, ctx), b.kept_out)));
|
|
127
|
+
recount();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const cards = decide.map(cardView);
|
|
131
|
+
quick.remove();
|
|
132
|
+
list.replaceChildren(...cards.map((v, i) => {
|
|
133
|
+
const T = TAG_STYLE[v.tag] || TAG_STYLE['Just in'];
|
|
134
|
+
return needRow({ key: v.key || String(i), headline: v.headline, kind: v.tag, icon: T.icon, color: T.color, time: v.deadline && v.deadline.text ? v.deadline.text : null, src: v.tag, view: v }, i, ctx);
|
|
135
|
+
}));
|
|
136
|
+
extra.replaceChildren();
|
|
137
|
+
recount();
|
|
138
|
+
};
|
|
139
|
+
S.refreshNeeds = () => { sig = ''; if (last) fill(last); };
|
|
140
|
+
S.needsFill = fill;
|
|
141
|
+
onBriefing((b) => { S.brief = b; fill(b); });
|
|
142
|
+
onBriefPage(fillOwn);
|
|
143
|
+
loadBriefing();
|
|
144
|
+
return box;
|
|
145
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* pm/c3-brief-pulse.js — the Briefing's "Portfolio pulse" (PRD briefing-brief R5, R6).
|
|
2
|
+
* One status bar and a row per initiative (the Calendar's own gantt rows, GET /api/calendar-views quarter, c3-plan.js readQuarter; the demo's pulse rows
|
|
3
|
+
* carry their own colour and reason). A row opens in place with the case component the Needs you rows use (c3-expand + c3-case): why it is where it
|
|
4
|
+
* is, promised against forecast. A press on a row does NOT change the tab. "Open initiative" is an explicit link inside the opened row and is the only
|
|
5
|
+
* thing here that does. Rows are references (data-ref-row): nothing in them changes a record. */
|
|
6
|
+
import { el } from './dom.js';
|
|
7
|
+
import { c3ic } from './pm-work-view.js';
|
|
8
|
+
import { makeXp, attachExpand } from './c3-expand.js';
|
|
9
|
+
import { caseFile } from './c3-brief.js';
|
|
10
|
+
import { readQuarter } from './c3-plan.js';
|
|
11
|
+
import { row, signal, ensureComponentsCss } from './primitives.js';
|
|
12
|
+
|
|
13
|
+
/* tone: ok is neutral, warn is the warning token, bad the destructive one (R28-12) */
|
|
14
|
+
const PST = { 'on time': ['ok', 'On time'], 'at risk': ['warn', 'At risk'], delayed: ['bad', 'Delayed'], overdue: ['bad', 'Overdue'], 'no plan': ['', 'No plan'] };
|
|
15
|
+
const RAG = { red: ['bad', 'Off track'], amber: ['warn', 'At risk'], green: ['ok', 'On track'], park: ['', 'Parked'] };
|
|
16
|
+
const SIG = { bad: 'destructive', warn: 'warning' };
|
|
17
|
+
const end = (o) => (o && (o.end || o.start)) || null;
|
|
18
|
+
|
|
19
|
+
/** One gantt row as the pulse draws it: { id, title, cls, word, color, why, evidence }. Pure. */
|
|
20
|
+
export function pulseRow(g) {
|
|
21
|
+
if (g.pulse && RAG[g.pulse.c]) {
|
|
22
|
+
const [cls, word] = RAG[g.pulse.c];
|
|
23
|
+
const ev = [];
|
|
24
|
+
if (g.promised) ev.push(['target', 'Promised ' + g.promised, 'Plan', cls === 'bad' ? 'bad' : cls === 'warn' ? 'warn' : '']);
|
|
25
|
+
return { id: g.id, title: g.title || g.id, cls, word, why: g.pulse.why, evidence: ev };
|
|
26
|
+
}
|
|
27
|
+
const k = PST[g.status] ? g.status : 'no plan'; const f = end(g.forecast); const p = g.promised || null;
|
|
28
|
+
const why = p ? 'Forecast ' + (f || 'not set') + ' vs promised ' + p : 'No promised date' + (f ? '; forecast ' + f : '');
|
|
29
|
+
const ev = [];
|
|
30
|
+
if (p) ev.push(['target', 'Promised ' + p, 'Plan']);
|
|
31
|
+
if (f) ev.push(['clock', 'Forecast ' + f, 'Plan', PST[k][0] === 'bad' ? 'bad' : PST[k][0] === 'warn' ? 'warn' : '']);
|
|
32
|
+
if (g.slip_days > 0) ev.push(['alert', 'Slipping by ' + g.slip_days + ' d', 'Plan', 'warn']);
|
|
33
|
+
return { id: g.id, title: g.title || g.id, cls: PST[k][0], word: PST[k][1] + (g.slip_days > 0 ? ' +' + g.slip_days + ' d' : ''), why, evidence: ev };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pulseCase(r) {
|
|
37
|
+
return { id: 'pulse:' + r.id, tc: r.cls === 'bad' ? 'var(--destructive)' : r.cls === 'warn' ? 'var(--warning)' : 'var(--muted-foreground)', sit: r.why, chg: r.evidence, opts: [], go: ['goinit', r.id, 'Open initiative'], cta: '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function pulse(S) {
|
|
41
|
+
ensureComponentsCss();
|
|
42
|
+
const body = el('div', null, el('p', { class: 'dim mono c3s2' }, 'Reading the initiatives.'));
|
|
43
|
+
const box = el('section', { class: 'card pulsecard', 'data-c3pg': 'pulse' },
|
|
44
|
+
el('div', { class: 'pulsehead' },
|
|
45
|
+
el('h3', null, c3ic('init', 'sm'), 'Portfolio pulse'), el('span', { class: 'mono dim c3s2' }, 'promised vs forecast')), body);
|
|
46
|
+
const note = (t, ic) => el('div', { class: 'fold' }, c3ic(ic || 'moon', 'sm'), t);
|
|
47
|
+
const draw = (rows, err) => {
|
|
48
|
+
if (err) { body.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status' }, 'The initiatives could not be read: ' + err)); return; }
|
|
49
|
+
if (!rows.length) { body.replaceChildren(note('No initiative has a promised date yet, so there is nothing to compare with the forecast.')); return; }
|
|
50
|
+
const noPlan = rows.filter((g) => !g.pulse && g.status === 'no plan').length;
|
|
51
|
+
rows = rows.filter((g) => g.pulse || g.status !== 'no plan');
|
|
52
|
+
if (!rows.length) { body.replaceChildren(note('No initiative has a promised date yet, so there is nothing to compare with the forecast.'), ...(noPlan ? [el('p', { class: 'dim mono c3s2' }, noPlan + ' initiatives have no plan date.')] : [])); return; }
|
|
53
|
+
const pr = rows.map(pulseRow);
|
|
54
|
+
const by = new Map(); pr.forEach((r) => { const k = r.word.replace(/ \+.*/, ''); const o = by.get(k) || { n: 0, cls: r.cls }; o.n += 1; by.set(k, o); });
|
|
55
|
+
const bar = el('div', { class: 'pbs', role: 'group', 'aria-label': 'Status of initiatives' },
|
|
56
|
+
...[...by.entries()].map(([k, v]) => { const seg = el('span', { class: 'pb-seg pb-' + (v.cls || 'mute'), title: v.n + ' ' + k.toLowerCase(), 'aria-label': v.n + ' ' + k.toLowerCase() }); seg.style.setProperty('--fr-grow', String(v.n)); return seg; }));
|
|
57
|
+
const rowEl = (r) => {
|
|
58
|
+
const xp = makeXp(caseFile(pulseCase(r)));
|
|
59
|
+
let ex = null;
|
|
60
|
+
const head = row({ title: r.title, meta: [r.why], signals: [signal({ text: r.word, tone: SIG[r.cls] || 'muted' })], openLabel: r.title + ': ' + r.word, onOpen: () => ex.toggle(), openAttrs: { 'data-c3pg-prow': r.id } });
|
|
61
|
+
ex = attachExpand(head, xp, { key: 'pulse-' + r.id });
|
|
62
|
+
return el('div', { class: 'pwrap', 'data-ref-row': '', 'data-pulse-row': r.id }, head, xp);
|
|
63
|
+
};
|
|
64
|
+
body.replaceChildren(...[bar, el('div', null, ...pr.slice(0, 8).map(rowEl)), pr.length > 8 ? el('div', { class: 'dim mono c3s2' }, '+' + (pr.length - 8) + ' more in the Calendar tab') : null, noPlan ? el('div', { class: 'dim mono c3s2' }, noPlan + ' more have no plan date.') : null].filter(Boolean));
|
|
65
|
+
};
|
|
66
|
+
readQuarter().then((j) => {
|
|
67
|
+
if (!j || j.ok === false) draw([], (j && j.error) || 'unreadable'); else { S.today = j.today || ''; draw(j.gantt || []); }
|
|
68
|
+
}).catch((e) => draw([], String((e && e.message) || e)));
|
|
69
|
+
return box;
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* pm/c3-brief-today.js — the Briefing's "Today": the agenda of today's meetings (PRD briefing-brief R2).
|
|
2
|
+
* One row per meeting, in time order, from GET /api/brief-page: its time and length, the people, the initiative, what PMOS holds as PREPARED
|
|
3
|
+
* (documents, notes) and what the PM must do BEFORE the meeting (own items that name it). A meeting with nothing held says "Nothing prepared
|
|
4
|
+
* yet". A meeting that is over is dimmed; the one under way is marked Now, the next one Next.
|
|
5
|
+
* Rows are REFERENCES (data-ref-row): they carry no control that changes a record. The record's actions are in Needs you and on its own page. */
|
|
6
|
+
import { el } from './dom.js';
|
|
7
|
+
import { c3ic } from './pm-work-view.js';
|
|
8
|
+
import { onBriefPage } from './c3-brief-data.js';
|
|
9
|
+
|
|
10
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
11
|
+
const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
|
|
12
|
+
|
|
13
|
+
export function meetingRow(m) {
|
|
14
|
+
const marks = { now: 'Now', next: 'Next' };
|
|
15
|
+
const prep = m.prepared && m.prepared.length
|
|
16
|
+
? el('ul', { class: 'tl', role: 'list', 'aria-label': 'Prepared' }, ...m.prepared.map((x) => el('li', { 'data-prep': x.kind }, c3ic(x.kind === 'prep' ? 'spark' : 'doc', 'sm'), x.text)))
|
|
17
|
+
: el('p', { class: 'dim c3s2', 'data-prep': 'none' }, c3ic('moon', 'sm'), 'Nothing prepared yet');
|
|
18
|
+
const bef = m.before && m.before.length
|
|
19
|
+
? el('ul', { class: 'tl', role: 'list', 'aria-label': 'Before the meeting' }, ...m.before.map((x) => el('li', { 'data-before': x.id }, c3ic('todo', 'sm'), x.title, x.due ? el('span', { class: 'mono dim' }, ' · due ' + x.due) : null)))
|
|
20
|
+
: el('p', { class: 'dim c3s2', 'data-before': 'none' }, 'Nothing to do before it.');
|
|
21
|
+
return el('li', { class: 'trow ' + m.state, 'data-ref-row': '', 'data-today-row': m.id, 'data-state': m.state },
|
|
22
|
+
el('div', { class: 'tm mono' }, hm(m.start), el('div', { class: 'dim c3s2' }, m.minutes ? m.minutes + ' min' : 'no end')),
|
|
23
|
+
el('div', { class: 'tb' },
|
|
24
|
+
el('div', { class: 'chips2' }, el('b', { class: 'tt' }, m.title), marks[m.state] ? el('span', { class: 'st ' + (m.state === 'now' ? 'ok' : ''), 'data-mark': m.state }, marks[m.state]) : null),
|
|
25
|
+
el('div', { class: 'chips2 dim c3s2' },
|
|
26
|
+
m.people && m.people.length ? el('span', { class: 'chip2', 'data-people': '' }, c3ic('user', 'sm'), m.people.join(', ')) : null,
|
|
27
|
+
m.initiative ? el('span', { class: 'chip2', 'data-init': '' }, c3ic('init', 'sm'), m.initiative) : null),
|
|
28
|
+
el('div', { class: 'tcols' }, el('div', null, el('div', { class: 'lab mono' }, 'Prepared'), prep), el('div', null, el('div', { class: 'lab mono' }, 'Before the meeting'), bef))));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function todaySection() {
|
|
32
|
+
const list = el('ul', { class: 'list', role: 'list' });
|
|
33
|
+
const count = el('span', { class: 'bdg g' }, '0');
|
|
34
|
+
const box = el('section', { class: 'card sect', 'data-c3pg': 'today', 'aria-label': 'Today' },
|
|
35
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('cal', 'sm'), 'Today ', count)), list);
|
|
36
|
+
list.replaceChildren(el('li', { class: 'fold', 'data-today-state': 'loading' }, c3ic('clock', 'sm'), 'Reading today\'s meetings.'));
|
|
37
|
+
onBriefPage((j) => {
|
|
38
|
+
if (!j || j.ok === false) { count.textContent = '0'; list.replaceChildren(el('li', { class: 'fold', role: 'status', 'data-today-state': 'error' }, c3ic('alert', 'sm'), 'Today\'s meetings could not be read: ' + ((j && j.error) || 'unreadable'))); return; }
|
|
39
|
+
const t = j.today || [];
|
|
40
|
+
count.textContent = String(t.length);
|
|
41
|
+
list.replaceChildren(...(t.length ? t.map(meetingRow) : [el('li', { class: 'fold', 'data-today-state': 'empty' }, c3ic('moon', 'sm'), 'No meetings today in the calendar PMOS holds.')]));
|
|
42
|
+
});
|
|
43
|
+
return box;
|
|
44
|
+
}
|