create-pmos 0.134.0 → 0.136.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/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +16 -1
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/records-dir.js +29 -0
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +9 -8
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +99 -15
- package/workspace/lib/routes-c3-strategy.js +7 -5
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +10 -7
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +17 -14
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +12 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +10 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -5,19 +5,22 @@
|
|
|
5
5
|
* chip and the swipe, pinch and key gestures; the helpers it passes are its own, so nothing is copied here.
|
|
6
6
|
*
|
|
7
7
|
* Top to bottom: the page line and the zoom bar (Day, Week, Month, Quarter, Year), the layer chips (Meetings, Milestones
|
|
8
|
-
* and due dates, Releases, Report dates, My due items, then Free time), the grid of the zoom with the layer
|
|
9
|
-
* and the detail column (under the grid on a narrow screen, at the right from 1100px).
|
|
8
|
+
* and due dates, Releases, Report dates, My due items, then Free time and Tidy-up), the grid of the zoom with the layer
|
|
9
|
+
* markers on it, and the detail column (under the grid on a narrow screen, at the right from 1100px).
|
|
10
10
|
*
|
|
11
11
|
* What it reads from the answer, all optional and additive: `cells` (the meetings), `layers` (the dated items, each with
|
|
12
12
|
* kind, layer, tone and a reference to its home), `free` (free blocks with a suggested use or none), `context` (what came
|
|
13
|
-
* out of a past meeting and what moved after it; what is prepared and what to prepare for a coming one)
|
|
13
|
+
* out of a past meeting and what moved after it; what is prepared and what to prepare for a coming one), `layers_note`, and
|
|
14
|
+
* `tidy`, `tidy_total`, `tidy_note` (initiative calendar-tidy-up: suggestions by fixed rules, each a proposal in words with
|
|
15
|
+
* references; the Tidy-up chip lists them and marks the meetings they name on the Day and Week timeline).
|
|
14
16
|
* A key that is missing reads as "the server sent none", in words. Nothing is invented.
|
|
15
17
|
*
|
|
16
18
|
* What it does: it selects (a meeting, a marker) and it navigates (a zoom, a day, a reference to the home of an item).
|
|
17
19
|
* It writes nothing. A marker carries no action: its detail has one reference link that opens the item where it lives. */
|
|
18
20
|
import { el } from './dom.js';
|
|
19
21
|
import { c3ic } from './pm-work-view.js';
|
|
20
|
-
import { button } from './primitives.js';
|
|
22
|
+
import { button, ensureComponentsCss } from './primitives.js';
|
|
23
|
+
import { toast } from './c3-toast.js';
|
|
21
24
|
|
|
22
25
|
const LAYERS = [
|
|
23
26
|
['meetings', 'Meetings'],
|
|
@@ -34,7 +37,19 @@ const TONE = { bad: 'Late or at risk of missing', warn: 'At risk', ok: 'On track
|
|
|
34
37
|
const REF = { initiative: 'Open the initiative', 'plan-item': 'Open the initiative', item: 'Open in Work', release: 'Open the release', report: 'Open in Reports' };
|
|
35
38
|
const CTXWORD = { decision: 'Decision', action: 'Action', moved: 'Moved', prepared: 'Prepared', todo: 'To prepare' };
|
|
36
39
|
const NO_SUGGESTION = 'Free, nothing specific to suggest';
|
|
37
|
-
|
|
40
|
+
/* the tidy-up suggestions (lib/calendar-tidy.js): the severity groups in order, and each rule in words */
|
|
41
|
+
const TIDY_GROUPS = [['bad', 'Needs a fix'], ['warn', 'At risk'], ['info', 'Worth a look']];
|
|
42
|
+
const TIDY_RULE = { overlap: 'Overlap', 'no-buffer': 'No break', 'prep-risk': 'Prep at risk', 'dead-initiative': 'Stopped initiative',
|
|
43
|
+
'review-early': 'Review too early', 'likely-delay': 'Late initiative', 'heavy-day': 'Heavy day' };
|
|
44
|
+
const TIDY_RANK = { bad: 0, warn: 1, info: 2 };
|
|
45
|
+
const PPH_PAGE = 46; // pixels per hour on the Day and Week timelines (the mockup's page grid)
|
|
46
|
+
const PPH_OVERLAY = 36; // the same grid in the overlay drawer
|
|
47
|
+
const DAY_LOAD = 6; // the scale of a day's load bar, in hours
|
|
48
|
+
const HOME = { event: 'Calendar', milestone: 'Initiatives', due: 'Initiatives', release: 'Initiatives', report: 'Reports', own: 'Work' }; // where a record lives (the home chip)
|
|
49
|
+
/* the overlay remembers the layers the PM hid, from one opening to the next (the mockup's mem.hide) */
|
|
50
|
+
const mem = { hide: [] };
|
|
51
|
+
/* the page's own Tidy-up choices: what was proposed and what was dismissed. They write nothing (D65) */
|
|
52
|
+
const proposed = new Set(); const dismissed = new Set();
|
|
38
53
|
|
|
39
54
|
const pad = (n) => String(n).padStart(2, '0');
|
|
40
55
|
const nowIso = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; };
|
|
@@ -46,7 +61,7 @@ const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name,
|
|
|
46
61
|
/* ── the page's own state on the instance: which layers are on, whether free time shows, what is selected ─────────── */
|
|
47
62
|
|
|
48
63
|
function state(r) {
|
|
49
|
-
const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0])), free: false, sel: null });
|
|
64
|
+
const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, sel: null });
|
|
50
65
|
if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
|
|
51
66
|
return st;
|
|
52
67
|
}
|
|
@@ -63,7 +78,11 @@ function model(d, h) {
|
|
|
63
78
|
const freeBy = new Map(); if (free) for (const x of free.days) freeBy.set(x.date, x.blocks || []);
|
|
64
79
|
const ctx = d.context && typeof d.context === 'object' ? d.context : null;
|
|
65
80
|
const findEvent = (id) => meetings.find((m) => m.e.id === id) || meetings.find((m) => h.baseId(m.e) === id) || null;
|
|
66
|
-
|
|
81
|
+
const tidy = Array.isArray(d.tidy) ? d.tidy.filter((s) => s && s.id && !dismissed.has(s.id)) : null;
|
|
82
|
+
const tidyBy = new Map(); // event id -> the suggestions that name it, the most severe first
|
|
83
|
+
if (tidy) for (const s of tidy) for (const x of s.refs || []) if (x && x.type === 'event' && x.id) { if (!tidyBy.has(x.id)) tidyBy.set(x.id, []); tidyBy.get(x.id).push(s); }
|
|
84
|
+
for (const list of tidyBy.values()) list.sort((a, b) => (TIDY_RANK[a.severity] ?? 3) - (TIDY_RANK[b.severity] ?? 3));
|
|
85
|
+
return { view, cells, meetingsOf, meetings, layers, free, freeBy, ctx, findEvent, tidy, tidyBy };
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
const sortMarks = (a, b) => (a.tone === 'bad' ? 0 : 1) - (b.tone === 'bad' ? 0 : 1) || (RANK[a.kind] ?? 9) - (RANK[b.kind] ?? 9) || String(a.date).localeCompare(String(b.date));
|
|
@@ -77,12 +96,11 @@ const freeIn = (M, a, b) => (M.free ? M.free.days.filter((x) => x.date >= a && x
|
|
|
77
96
|
|
|
78
97
|
/** Draw the Calendar tab of instance r into a new node. h is calendar-views.js's own helpers (see PAGE there). */
|
|
79
98
|
export function drawCalendarPage(host, r, h) {
|
|
99
|
+
ensureComponentsCss(); // the Tidy-up buttons are pmc-btn
|
|
80
100
|
const st = state(r);
|
|
81
101
|
const phone = r.root.classList.contains('ph');
|
|
82
|
-
const head = el('div', { class: 'cal3-head', 'data-cal3': 'head' }, el('h2', null, 'Calendar'),
|
|
83
|
-
el('span', { class: 'dim' }, 'What happens by when: meetings, dates and free time on one grid.'));
|
|
84
102
|
const bar = h.bar(host, r, phone);
|
|
85
|
-
const page = (...kids) => el('div', { class: 'cal3', 'data-cal3': 'page' },
|
|
103
|
+
const page = (...kids) => el('div', { class: 'cal3' + (r.ctx.overlay ? ' ov' : ''), 'data-cal3': 'page' }, r.ctx.deck || null, bar, ...kids);
|
|
86
104
|
if (r.error) return page(fold('alert', 'The calendar could not be read: ' + r.error, { 'data-cv': 'error', role: 'status' }));
|
|
87
105
|
if (!r.data) return page(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
|
|
88
106
|
|
|
@@ -114,7 +132,7 @@ export function drawCalendarPage(host, r, h) {
|
|
|
114
132
|
const body = el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') },
|
|
115
133
|
el('div', { class: 'cal3-wrap' },
|
|
116
134
|
el('div', { class: 'cal3-main' }, empty, grid),
|
|
117
|
-
el('div', { class: 'cal3-side' }, detail(ui), st.free ? freeList(ui, range) : null, lanes(ui, range))));
|
|
135
|
+
el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
|
|
118
136
|
return page(chips(ui, range), body);
|
|
119
137
|
}
|
|
120
138
|
|
|
@@ -132,10 +150,16 @@ function chips(ui, range) {
|
|
|
132
150
|
});
|
|
133
151
|
const freeN = M.free ? (typeof M.free.count === 'number' ? M.free.count : freeIn(M, range.start, range.end).length) : null;
|
|
134
152
|
return el('div', { class: 'cal3-chips', role: 'group', 'aria-label': 'Layers', 'data-cal3': 'chips' },
|
|
135
|
-
...LAYERS.map(([id, label]) => chip(id, label, count(id), st.on.has(id), () => {
|
|
153
|
+
...LAYERS.map(([id, label]) => chip(id, label, count(id), st.on.has(id), () => {
|
|
154
|
+
if (st.on.has(id)) st.on.delete(id); else st.on.add(id);
|
|
155
|
+
if (ui.r.ctx.overlay) mem.hide = LAYERS.map((l) => l[0]).filter((x) => !st.on.has(x)); // the overlay remembers what was hidden
|
|
156
|
+
}, el('i', { class: 'cal3-sw cal3-sw-' + id }))),
|
|
136
157
|
el('span', { class: 'cal3-sep', 'aria-hidden': 'true' }),
|
|
137
|
-
chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm'))
|
|
158
|
+
chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm')),
|
|
159
|
+
chip('tidy', 'Tidy-up', tidyCount(M, ui.d), st.tidy, () => { st.tidy = !st.tidy; }, c3ic('spark', 'sm')));
|
|
138
160
|
}
|
|
161
|
+
/* the number of suggestions in the range: the server's total (the list may be capped), or null when it sent none */
|
|
162
|
+
const tidyCount = (M, d) => (M.tidy ? Math.max(0, (typeof d.tidy_total === 'number' ? d.tidy_total : M.tidy.length) - (d.tidy.length - M.tidy.length)) : null); // less the ones the PM dismissed
|
|
139
163
|
|
|
140
164
|
/* ── a marker: glyph and short title, tone from the tokens; pressing it selects it (the detail has the reference) ─── */
|
|
141
165
|
|
|
@@ -153,15 +177,37 @@ function marker(ui, m, compact) {
|
|
|
153
177
|
const cssId = (s) => String(s).replace(/["\\]/g, '\\$&');
|
|
154
178
|
|
|
155
179
|
/* the markers of a span, the most important first, at most `max`, and a "+N" for the rest */
|
|
156
|
-
function markRow(ui, a, b, max, compact) {
|
|
180
|
+
function markRow(ui, a, b, max, compact, view) {
|
|
157
181
|
const all = marksIn(ui.M, ui.st, a, b);
|
|
158
|
-
return [...all.slice(0, max).map((m) => marker(ui, m, compact)), all.length > max ?
|
|
182
|
+
return [...all.slice(0, max).map((m) => marker(ui, m, compact)), all.length > max ? moreBtn(ui, all.length - max, a, b, view) : null];
|
|
183
|
+
}
|
|
184
|
+
/* the "+N" of a span is a button: it zooms into the week or the month that holds the rest */
|
|
185
|
+
function moreBtn(ui, n, a, b, view) {
|
|
186
|
+
const label = view === 'week' ? 'Open the week of ' + ui.h.dayLabel(a) : view === 'day' ? 'Open ' + ui.h.dayLabel(a) : 'Open ' + ui.h.MN[Number(a.slice(5, 7)) - 1] + ' ' + a.slice(0, 4);
|
|
187
|
+
return button({ label: '+' + n, variant: 'ghost', size: 'sm', attrs: { class: 'cal3-more', 'data-cal3-more': a, title: plural(n, 'more dated item') + '. ' + label, 'aria-label': plural(n, 'more dated item') + '. ' + label },
|
|
188
|
+
onClick: () => ui.h.go(ui.host, ui.r, { view, date: a, anim: 'zi' }) });
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
/* ── Day and Week: the timeline with the all-day marker strip above it ───────────────────────────────────────────── */
|
|
162
192
|
|
|
193
|
+
/* what a meeting is, for the grid: tied to a promise or a decision (an initiative, prep, or a record PMOS holds), or routine; blocked
|
|
194
|
+
* and prep left come from the event or its context when the server sends them (the mockup's blocked(), kind() and note) */
|
|
195
|
+
function evState(ui, m, nowStr) {
|
|
196
|
+
const { M, h } = ui; const e = m.e;
|
|
197
|
+
const rec = M.ctx ? (M.ctx[e.id] || M.ctx[h.baseId(e)] || null) : null;
|
|
198
|
+
const items = rec && Array.isArray(rec.items) ? rec.items : [];
|
|
199
|
+
const todo = items.some((x) => x.kind === 'todo') || e.prep_todo === true;
|
|
200
|
+
const held = !!rec && rec.held !== false && items.length > 0;
|
|
201
|
+
const standup = /stand-?up/i.test(e.title || '');
|
|
202
|
+
const tied = !standup && !(e.routine || e.mock_routine) && !!(e.initiative || (Array.isArray(e.prep) ? e.prep.length : e.prep) || items.length);
|
|
203
|
+
const past = String(e.end || e.at) <= nowStr;
|
|
204
|
+
return { tied, blocked: e.blocked === true || e.mock_hot === true, todo, held, past, note: past ? (held ? ' · held' : '') : (todo ? ' · prep to do' : '') };
|
|
205
|
+
}
|
|
206
|
+
const hoursOf = (m, h) => Math.max(0, h.endH(m.e) - h.timeOf(m.e.at));
|
|
207
|
+
|
|
163
208
|
function timeGrid(ui, days) {
|
|
164
209
|
const { M, st, h, d } = ui;
|
|
210
|
+
const PPH = ui.r.ctx.overlay ? PPH_OVERLAY : PPH_PAGE;
|
|
165
211
|
const now = nowIso(); const today = d.today;
|
|
166
212
|
const timedOn = (date) => (st.on.has('meetings') ? M.meetings.filter((m) => m.date === date && h.timeOf(m.e.at) !== null) : []);
|
|
167
213
|
const allDayOn = (date) => (st.on.has('meetings') ? M.meetings.filter((m) => m.date === date && h.timeOf(m.e.at) === null) : []);
|
|
@@ -174,14 +220,27 @@ function timeGrid(ui, days) {
|
|
|
174
220
|
const tg = el('div', { class: 'cal3-tg' + (days.length === 1 ? ' one' : ''), 'data-cal3': days.length === 1 ? 'day' : 'week' });
|
|
175
221
|
setVar(tg, 'n', days.length); setVar(tg, 'hgt', (t1 - t0) * PPH + 'px'); setVar(tg, 'pph', PPH + 'px');
|
|
176
222
|
|
|
223
|
+
/* the hours of a day and its load: tied to a promise or a decision, routine, and the rest, on a 6 hour scale */
|
|
224
|
+
const load = (date) => {
|
|
225
|
+
const es = timedOn(date); let tot = 0; let tied = 0;
|
|
226
|
+
for (const m of es) { const x = hoursOf(m, h); tot += x; if (evState(ui, m, now).tied) tied += x; }
|
|
227
|
+
return { tot, tied };
|
|
228
|
+
};
|
|
229
|
+
const seg = (hrs, cls) => setVar(el('i', { class: cls }), 'w', Math.min(100, hrs / DAY_LOAD * 100) + '%');
|
|
177
230
|
const headRow = el('div', { class: 'cal3-row cal3-dhead' }, el('div', { class: 'cal3-gut' }),
|
|
178
|
-
...days.map((date) =>
|
|
179
|
-
|
|
180
|
-
|
|
231
|
+
...days.map((date) => {
|
|
232
|
+
const L = load(date);
|
|
233
|
+
return el('div', { class: 'cal3-dh' + (date === today ? ' today' : ''), 'data-cal3-dh': date },
|
|
234
|
+
el('div', { class: 'dhl' },
|
|
235
|
+
days.length > 1 ? button({ label: h.dayLabel(date), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-day': date, title: 'Open ' + h.dayLabel(date) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date, anim: 'zi' }) })
|
|
236
|
+
: el('b', null, h.dayLabel(date)),
|
|
237
|
+
el('span', { class: 'hrs', 'data-cal3-hrs': date }, L.tot ? +L.tot.toFixed(1) + ' h' : '')),
|
|
238
|
+
el('div', { class: 'lbar', 'aria-hidden': 'true' }, seg(L.tied, 'tied'), seg(L.tot - L.tied, 'rt'), el('i', { class: 'rest' })));
|
|
239
|
+
}));
|
|
181
240
|
|
|
182
241
|
const strip = el('div', { class: 'cal3-row cal3-strip', 'data-cal3': 'strip' }, el('div', { class: 'cal3-gut' }, 'dates'),
|
|
183
242
|
...days.map((date) => {
|
|
184
|
-
const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...markRow(ui, date, date, days.length === 1 ? 12 : 4, days.length > 1)];
|
|
243
|
+
const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...markRow(ui, date, date, days.length === 1 ? 12 : 4, days.length > 1, 'day')];
|
|
185
244
|
return el('div', { class: 'cal3-sc', 'data-cal3-strip': date }, ...(kids.filter(Boolean).length ? kids : [el('span', { class: 'cal3-none' }, days.length === 1 ? 'No dated item on this day' : '')]));
|
|
186
245
|
}));
|
|
187
246
|
|
|
@@ -195,7 +254,8 @@ function timeGrid(ui, days) {
|
|
|
195
254
|
const evs = timedOn(date).sort((a, b) => h.timeOf(a.e.at) - h.timeOf(b.e.at));
|
|
196
255
|
lanes4(evs, h).forEach(([m, lane, n]) => {
|
|
197
256
|
const s = h.timeOf(m.e.at); const f = h.endH(m.e);
|
|
198
|
-
const
|
|
257
|
+
const S = evState(ui, m, now); const rt = !S.tied && !S.held && !S.todo; // routine: a standup, or no initiative, no prep and no record
|
|
258
|
+
const b = meetingButton(ui, m, 'vb cal3-ev' + (S.past ? ' past' : '') + (rt || m.e.routine || m.e.mock_routine ? ' rt' : '') + (S.blocked ? ' hot' : ''), [el('b', null, m.e.title || m.e.id), f - s >= 0.5 ? el('small', null, h.hhmm(s) + ' to ' + h.hhmm(f) + S.note) : null]);
|
|
199
259
|
setVar(b, 'y', (Y(s) + 1) + 'px'); setVar(b, 'h', Math.max(16, Y(f) - Y(s) - 3) + 'px'); setVar(b, 'l', lane); setVar(b, 'lc', n);
|
|
200
260
|
col.appendChild(b);
|
|
201
261
|
});
|
|
@@ -203,7 +263,9 @@ function timeGrid(ui, days) {
|
|
|
203
263
|
return col;
|
|
204
264
|
});
|
|
205
265
|
tg.append(headRow, strip, el('div', { class: 'cal3-row cal3-body' }, hours, ...cols));
|
|
206
|
-
|
|
266
|
+
const key = (cls, text, wrap) => el('span', { class: wrap || null }, el('i', { class: 'lgk ' + cls, 'aria-hidden': 'true' }), text);
|
|
267
|
+
const legend = el('div', { class: 'legend', 'data-cal3': 'legend' }, key('tied', 'tied to a promise or decision'), key('rt', 'routine'), key('fw', 'focus window', 'lgfw'), key('rel', 'release'), key('pro', 'promise'), key('rep', 'report'));
|
|
268
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-scroll' }, tg), legend);
|
|
207
269
|
}
|
|
208
270
|
|
|
209
271
|
/* overlapping meetings share the column side by side: [meeting, lane, lanes in its cluster] */
|
|
@@ -218,19 +280,24 @@ function lanes4(evs, h) {
|
|
|
218
280
|
function meetingButton(ui, m, cls, label) {
|
|
219
281
|
const sel = ui.st.sel && ui.st.sel.type === 'event' && (ui.st.sel.id === m.e.id || ui.st.sel.id === ui.h.baseId(m.e));
|
|
220
282
|
const t = m.e.title || m.e.id; const s = ui.h.timeOf(m.e.at);
|
|
221
|
-
|
|
283
|
+
// with the Tidy-up chip on, a meeting that a suggestion names carries the most severe one's tone and title
|
|
284
|
+
const tidy = ui.st.tidy ? (ui.M.tidyBy.get(m.e.id) || ui.M.tidyBy.get(ui.h.baseId(m.e)) || null) : null;
|
|
285
|
+
const sev = tidy ? tidy[0].severity : null;
|
|
286
|
+
const name = t + ', ' + ui.h.dayLabel(m.date) + (s !== null ? ' at ' + ui.h.hhmm(s) : ', all day') + (tidy ? '. Tidy-up: ' + tidy.map((x) => x.title).join('; ') : '');
|
|
222
287
|
return button({
|
|
223
288
|
label: label || t, variant: 'ghost', size: 'sm',
|
|
224
|
-
attrs: { class: cls + (sel ? ' sel' : ''), 'data-id': m.e.id, 'data-cal3-ev': m.e.id,
|
|
289
|
+
attrs: { class: cls + (sel ? ' sel' : '') + (tidy ? ' cal3-tidy-ev cal3-tidy-' + (TIDY_RANK[sev] !== undefined ? sev : 'info') : ''), 'data-id': m.e.id, 'data-cal3-ev': m.e.id,
|
|
290
|
+
...(tidy ? { 'data-cal3-tidy': sev } : {}), 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
|
|
225
291
|
onClick: () => ui.select({ type: 'event', id: m.e.id }, '[data-cal3-ev="' + cssId(m.e.id) + '"]'),
|
|
226
292
|
});
|
|
227
293
|
}
|
|
228
294
|
|
|
229
|
-
/* a free block:
|
|
295
|
+
/* a free block: a dashed focus window with its length; the suggested use is in its title and in the Free time list */
|
|
230
296
|
function freeBlock(ui, date, b, Y) {
|
|
231
297
|
const s = hmToH(b.from); const f = hmToH(b.to);
|
|
232
|
-
const
|
|
233
|
-
|
|
298
|
+
const text = b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION;
|
|
299
|
+
const n = el('div', { class: 'vf cal3-free', 'data-cal3-free': date + ' ' + b.from, title: b.from + ' to ' + b.to + ': ' + text },
|
|
300
|
+
el('b', null, +((f - s)).toFixed(1) + ' h focus'));
|
|
234
301
|
setVar(n, 'y', (Y(s) + 2) + 'px'); setVar(n, 'h', Math.max(18, Y(f) - Y(s) - 4) + 'px');
|
|
235
302
|
return n;
|
|
236
303
|
}
|
|
@@ -245,12 +312,10 @@ function monthGrid(ui) {
|
|
|
245
312
|
for (let i = 0; i < off; i++) g.appendChild(el('div', { class: 'cal3-mc out', 'aria-hidden': 'true' }));
|
|
246
313
|
for (const c of days) {
|
|
247
314
|
const wd = h.dowOf(c.start); const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
|
|
248
|
-
const fr = st.free ? (M.freeBy.get(c.start) || []).length : 0;
|
|
249
315
|
g.appendChild(el('div', { class: 'cal3-mc' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === d.today ? ' today' : ''), 'data-cal3-date': c.start },
|
|
250
316
|
button({ label: String(Number(c.start.slice(8))), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dn', 'data-cal3-day': c.start, 'aria-label': 'Open ' + h.dayLabel(c.start) + (n ? ', ' + plural(n, 'meeting') : ''), title: 'Open ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date: c.start, anim: 'zi' }) }),
|
|
251
317
|
n ? el('span', { class: 'cal3-cnt' }, plural(n, 'meeting')) : null,
|
|
252
|
-
...markRow(ui, c.start, c.start, 3, true)
|
|
253
|
-
fr ? el('span', { class: 'cal3-fcnt', 'data-cal3-fcnt': c.start }, fr + ' free') : null));
|
|
318
|
+
...markRow(ui, c.start, c.start, 3, true, 'day')));
|
|
254
319
|
}
|
|
255
320
|
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g);
|
|
256
321
|
}
|
|
@@ -260,18 +325,16 @@ function monthGrid(ui) {
|
|
|
260
325
|
function spanCounts(ui, c) {
|
|
261
326
|
const { M, st } = ui;
|
|
262
327
|
const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
|
|
263
|
-
const fr = st.free ? freeIn(M, c.start, c.end).length : 0;
|
|
264
328
|
return el('div', { class: 'cal3-counts' },
|
|
265
329
|
st.on.has('meetings') ? el('span', { class: 'tag' }, plural(n, 'meeting')) : null,
|
|
266
|
-
el('span', { class: 'tag' }, plural(marksIn(M, st, c.start, c.end).length, 'dated item'))
|
|
267
|
-
st.free ? el('span', { class: 'tag cal3-ftag' }, fr + ' free') : null);
|
|
330
|
+
el('span', { class: 'tag' }, plural(marksIn(M, st, c.start, c.end).length, 'dated item')));
|
|
268
331
|
}
|
|
269
332
|
|
|
270
333
|
function quarterRows(ui) {
|
|
271
334
|
const { M, h, d } = ui;
|
|
272
335
|
const rows = M.cells.map((c) => el('div', { class: 'cal3-qrow' + (c.start <= d.today && d.today <= c.end ? ' now' : ''), 'data-cal3-week': c.start },
|
|
273
336
|
button({ label: 'Week of ' + h.dayLabel(c.start), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', title: 'Open the week of ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'week', date: c.start, anim: 'zi' }) }),
|
|
274
|
-
spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 4, false))));
|
|
337
|
+
spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 4, false, 'week'))));
|
|
275
338
|
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-quarter', 'data-cal3': 'quarter' }, ...rows));
|
|
276
339
|
}
|
|
277
340
|
|
|
@@ -279,7 +342,7 @@ function yearCards(ui) {
|
|
|
279
342
|
const { M, h, d } = ui;
|
|
280
343
|
const cards = M.cells.map((c) => el('div', { class: 'cal3-ym' + (c.start <= d.today && d.today <= c.end ? ' now' : ''), 'data-cal3-month': c.start.slice(0, 7) },
|
|
281
344
|
button({ label: h.MN[Number(c.start.slice(5, 7)) - 1] + ' ' + c.start.slice(0, 4), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', title: 'Open the month' }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: c.start, anim: 'zi' }) }),
|
|
282
|
-
spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 3, false))));
|
|
345
|
+
spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 3, false, 'month'))));
|
|
283
346
|
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-year', 'data-cal3': 'year' }, ...cards));
|
|
284
347
|
}
|
|
285
348
|
|
|
@@ -301,6 +364,10 @@ function openRef(ui, ref, extra) {
|
|
|
301
364
|
if (ref.type === 'report') h.gotoTab('rep');
|
|
302
365
|
}
|
|
303
366
|
|
|
367
|
+
/* the home chip: where the record lives (the mockup's UI.homeChip) */
|
|
368
|
+
const homeChip = (name) => el('span', { class: 'chip2', title: 'Where this lives', 'data-cal3': 'home' }, c3ic('repo', 'sm'), name || 'its home');
|
|
369
|
+
const peopleOf = (e) => [].concat(e.people || e.with || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
|
|
370
|
+
|
|
304
371
|
function closeBtn(ui) {
|
|
305
372
|
return button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { class: 'cal3-close', 'data-cal3': 'close' }, onClick: () => { ui.st.sel = null; ui.redraw('[data-cal3-chip="meetings"]'); } });
|
|
306
373
|
}
|
|
@@ -315,10 +382,10 @@ function detail(ui) {
|
|
|
315
382
|
return box([
|
|
316
383
|
el('div', { class: 'cal3-dt' }, el('h3', null, c3ic(icon), word), closeBtn(ui)),
|
|
317
384
|
el('b', { class: 'cal3-title' }, m.title),
|
|
318
|
-
el('div', { class: 'chips2' }, el('span', { class: 'tag' }, word), el('span', { class: 'tag' }, h.dayLabel(m.date) + (m.end && m.end !== m.date ? ' to ' + h.dayLabel(m.end) : '')),
|
|
385
|
+
el('div', { class: 'chips2' }, homeChip(HOME[m.kind]), el('span', { class: 'tag' }, word), el('span', { class: 'tag' }, h.dayLabel(m.date) + (m.end && m.end !== m.date ? ' to ' + h.dayLabel(m.end) : '')),
|
|
319
386
|
m.tone && TONE[m.tone] ? el('span', { class: 'tag cal3-tone-' + m.tone }, TONE[m.tone]) : null, m.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === m.initiative) || {}).title || m.initiative)) : null),
|
|
320
387
|
el('div', { class: 'cal3-open', 'data-cal3': 'open' }, refLink(ui, m.ref, { initiative: m.initiative, title: m.title })),
|
|
321
|
-
el('p', { class: 'dim cal3-note' }, 'A marker shows the date only. You act on the item where it lives.'),
|
|
388
|
+
el('p', { class: 'dim cal3-note' }, m.kind === 'own' ? 'A marker shows the date only. You act on the item where it lives.' : 'This date lives here. Change it where the plan is kept, not on the grid.'),
|
|
322
389
|
], { 'data-cal3-sel': m.id });
|
|
323
390
|
}
|
|
324
391
|
}
|
|
@@ -341,21 +408,24 @@ function meetingDetail(ui, hit) {
|
|
|
341
408
|
const rows = items.filter((x) => kinds.includes(x.kind));
|
|
342
409
|
return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, title),
|
|
343
410
|
rows.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((x) => el('li', { class: 'cal3-ci', 'data-cal3-item': x.kind },
|
|
344
|
-
el('span', { class: 'tag' }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
|
|
411
|
+
el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, x.done && x.kind === 'todo' ? 'Done' : CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
|
|
345
412
|
x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
|
|
346
413
|
refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none));
|
|
347
414
|
};
|
|
348
415
|
let body;
|
|
349
416
|
if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
|
|
350
|
-
else if (!rec || rec.held === false) body = [fold('moon', '
|
|
417
|
+
else if (!rec || rec.held === false) body = [fold('moon', past ? 'Nothing held for this meeting: no note, decisions or linked items yet.' : 'Nothing to prepare for this meeting.', { 'data-cal3': 'nothing-held' })];
|
|
351
418
|
else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
|
|
419
|
+
else if (!items.some((x) => x.kind === 'prepared' || x.kind === 'todo')) body = [el('p', { class: 'dim cal3-note', 'data-cal3': 'nothing-to-prepare' }, 'Nothing to prepare for this meeting.')];
|
|
352
420
|
else body = [list(['prepared'], 'Prepared', 'prepared', 'Nothing is prepared yet.'), list(['todo'], 'To prepare', 'todo', 'Nothing is left to prepare.')];
|
|
353
421
|
return [
|
|
354
422
|
el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), closeBtn(ui)),
|
|
355
423
|
el('b', { class: 'cal3-title' }, e.title || e.id),
|
|
356
424
|
el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
|
|
425
|
+
el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
|
|
426
|
+
e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
|
|
357
427
|
...body,
|
|
358
|
-
|
|
428
|
+
el('p', { class: 'dim cal3-note' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : 'PMOS proposes changes. It never edits your calendar.'),
|
|
359
429
|
];
|
|
360
430
|
}
|
|
361
431
|
|
|
@@ -365,15 +435,64 @@ function freeList(ui, range) {
|
|
|
365
435
|
const head = el('h3', null, c3ic('clock'), 'Free time');
|
|
366
436
|
if (!M.free) return el('section', { class: 'card cal3-freel', 'data-cal3': 'free-list' }, head, fold('info', 'The calendar sent no free time for this range.', { 'data-cal3': 'no-free' }));
|
|
367
437
|
const all = freeIn(M, range.start, range.end);
|
|
368
|
-
const
|
|
438
|
+
const small = ui.M.view === 'day' || ui.M.view === 'week';
|
|
439
|
+
const cap = 8;
|
|
369
440
|
return el('section', { class: 'card cal3-freel', 'data-cal3': 'free-list' }, head,
|
|
370
441
|
el('p', { class: 'dim cal3-note' }, 'Blocks of ' + (M.free.min_minutes || 90) + ' minutes or more between ' + (M.free.day_start || '09:00') + ' and ' + (M.free.day_end || '18:00') + '. A suggestion is a proposal: PMOS writes nothing.'),
|
|
371
|
-
|
|
442
|
+
!small ? fold('info', 'Free time shows in the Day and Week views.', { 'data-cal3': 'free-small' })
|
|
443
|
+
: all.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...all.slice(0, cap).map((b) => el('li', { class: 'cal3-ci', 'data-cal3-fb': b.date + ' ' + b.from },
|
|
372
444
|
el('span', { class: 'dim mono cal3-at' }, h.dayLabel(b.date) + ' ' + b.from + ' to ' + b.to),
|
|
373
445
|
el('span', { class: 'cal3-ct' }, b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION),
|
|
374
446
|
b.suggestion && b.suggestion.ref ? refLink(ui, b.suggestion.ref, { title: b.suggestion.text }) : null)))
|
|
375
447
|
: el('p', { class: 'dim cal3-note' }, 'No free block of ' + (M.free.min_minutes || 90) + ' minutes or more in this range.'),
|
|
376
|
-
all.length > cap ? el('p', { class: 'dim mono cal3-note' }, '+' + (all.length - cap) + ' more blocks in this range') : null);
|
|
448
|
+
small && all.length > cap ? el('p', { class: 'dim mono cal3-note' }, '+' + (all.length - cap) + ' more blocks in this range') : null);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* the tidy-up suggestions of the range, grouped by severity: what is wrong, why, the proposed change and the references
|
|
452
|
+
* to the items involved. A suggestion is a proposal in words; the panel has no control that changes anything. */
|
|
453
|
+
function tidyList(ui) {
|
|
454
|
+
const { M, d, h } = ui;
|
|
455
|
+
const head = el('h3', null, c3ic('spark'), 'Tidy-up');
|
|
456
|
+
const box = (...kids) => el('section', { class: 'card cal3-tidyl', 'data-cal3': 'tidy-list', 'aria-label': 'Tidy-up suggestions' }, head, ...kids);
|
|
457
|
+
const notes = d.tidy_note && typeof d.tidy_note === 'object' ? Object.keys(d.tidy_note).filter((k) => d.tidy_note[k]) : [];
|
|
458
|
+
const noteList = notes.length ? el('div', { class: 'cal3-ctx', 'data-cal3': 'tidy-notes' }, el('h4', null, 'Not checked'),
|
|
459
|
+
el('ul', { class: 'cal3-cl', role: 'list' }, ...notes.map((k) => el('li', { class: 'cal3-ci', 'data-cal3-tidy-note': k },
|
|
460
|
+
el('span', { class: 'tag' }, TIDY_RULE[k] || k), el('span', { class: 'cal3-ct dim' }, String(d.tidy_note[k])))))) : null;
|
|
461
|
+
if (!M.tidy) return box(fold('info', 'The server sent no tidy-up list.', { 'data-cal3': 'no-tidy' }), noteList);
|
|
462
|
+
const lead = el('p', { class: 'dim cal3-note' }, 'Each suggestion is a proposal. PMOS changes nothing in your calendar: you make the change where the meeting or the item lives.');
|
|
463
|
+
if (!M.tidy.length) return box(lead, el('p', { class: 'dim cal3-note', 'data-cal3': 'tidy-empty' }, 'Nothing to tidy in this range.'), noteList);
|
|
464
|
+
const total = tidyCount(M, d);
|
|
465
|
+
const refOf = (s, x) => {
|
|
466
|
+
if (x.type === 'event') {
|
|
467
|
+
const hit = M.findEvent(x.id);
|
|
468
|
+
if (!hit) return el('span', { class: 'dim cal3-noptr' }, 'meeting not drawn');
|
|
469
|
+
return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': 'event:' + x.id, title: 'Show the meeting: ' + (hit.e.title || x.id),
|
|
470
|
+
onClick: (ev) => { ev.preventDefault(); ui.select({ type: 'event', id: hit.e.id }, '[data-cal3-ev="' + cssId(hit.e.id) + '"]'); } }, c3ic('cal', 'sm'), hit.e.title || 'Show the meeting');
|
|
471
|
+
}
|
|
472
|
+
const ini = (s.refs || []).find((y) => y && y.type === 'initiative');
|
|
473
|
+
return refLink(ui, x, { title: s.title, initiative: ini ? ini.id : undefined });
|
|
474
|
+
};
|
|
475
|
+
const groups = TIDY_GROUPS.map(([sev, label]) => {
|
|
476
|
+
const rows = M.tidy.filter((s) => s.severity === sev);
|
|
477
|
+
if (!rows.length) return null;
|
|
478
|
+
return el('div', { class: 'cal3-ctx', 'data-cal3-tidy-group': sev }, el('h4', { class: 'cal3-tone-' + sev }, label + ' (' + rows.length + ')'),
|
|
479
|
+
el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((s) => el('li', { class: 'cal3-ci cal3-tidy-i cal3-tidy-' + sev, 'data-cal3-tidy-item': s.id, 'data-cal3-rule': s.rule },
|
|
480
|
+
el('span', { class: 'tag cal3-tone-' + sev }, TIDY_RULE[s.rule] || s.rule),
|
|
481
|
+
el('span', { class: 'dim mono cal3-at' }, h.dayLabel(s.date)),
|
|
482
|
+
el('b', { class: 'cal3-tidy-t' }, s.title),
|
|
483
|
+
s.why ? el('p', { class: 'cal3-note cal3-tidy-why' }, s.why) : null,
|
|
484
|
+
s.suggestion ? el('p', { class: 'cal3-note cal3-tidy-fix', 'data-cal3': 'tidy-fix' }, el('b', null, 'Proposed: '), s.suggestion) : null,
|
|
485
|
+
el('span', { class: 'cal3-tidy-refs' }, ...(s.refs || []).filter((x) => x && x.type && x.id).map((x) => refOf(s, x))),
|
|
486
|
+
/* a proposal is a note on this page and nothing more: no control here changes the calendar or sends a word (D65) */
|
|
487
|
+
el('span', { class: 'iacts' },
|
|
488
|
+
proposed.has(s.id) ? el('span', { class: 'tag cal3-up', 'data-cal3-proposed': s.id }, 'Proposed')
|
|
489
|
+
: button({ label: 'Propose it', variant: 'outline', size: 'sm', attrs: { 'data-cal3-act': 'propose', 'data-cal3-key': s.id },
|
|
490
|
+
onClick: () => { proposed.add(s.id); toast('Proposal: ' + String(s.suggestion || s.title).replace(/\.$/, '') + '. Nothing changed in your calendar.'); ui.redraw('[data-cal3-proposed="' + cssId(s.id) + '"]'); } }),
|
|
491
|
+
button({ label: 'Dismiss', variant: 'ghost', size: 'sm', attrs: { 'data-cal3-act': 'dismiss', 'data-cal3-key': s.id },
|
|
492
|
+
onClick: () => { dismissed.add(s.id); toast('Dismissed'); ui.redraw('[data-cal3-chip="tidy"]'); } }))))));
|
|
493
|
+
});
|
|
494
|
+
return box(lead, ...groups.filter(Boolean),
|
|
495
|
+
total > M.tidy.length ? el('p', { class: 'dim mono cal3-note' }, 'Showing ' + M.tidy.length + ' of ' + total + ' suggestions, the most severe first.') : null, noteList);
|
|
377
496
|
}
|
|
378
497
|
|
|
379
498
|
/* each lane in words: its count in the range, or why it is empty */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* pm/c3-calpop.js — the calendar as a popup over any page of the console (briefing-brief PRD R7, R9).
|
|
2
2
|
*
|
|
3
3
|
* mountCalPopupTrigger(toolsHost) adds the calendar button to the top bar's tools, and binds the key C (once)
|
|
4
|
-
* openCalendarPopup(opener?) opens the
|
|
4
|
+
* openCalendarPopup(opener?) opens the Calendar page's own grid (five zooms, chips, detail, Free time, lanes) in a drawer over the page
|
|
5
5
|
* closeCalendarPopup(back?) closes it; `back` (default true) returns the focus to where it was
|
|
6
6
|
* lastZoom() the zoom last used, "day" until the PM changes it
|
|
7
7
|
*
|
|
@@ -22,7 +22,7 @@ const store = {
|
|
|
22
22
|
get() { try { return localStorage.getItem(KEY); } catch { return null; } },
|
|
23
23
|
set(v) { try { localStorage.setItem(KEY, v); } catch { /* no storage */ } },
|
|
24
24
|
};
|
|
25
|
-
let zoom = ZOOMS.includes(store.get()) ? store.get() : '
|
|
25
|
+
let zoom = ZOOMS.includes(store.get()) ? store.get() : 'week'; // the overlay opens on the Week (the mockup's mem.zoom)
|
|
26
26
|
let cur = null; // { wrap, host, opener, off, obs, stopPhone }
|
|
27
27
|
let keyBound = false;
|
|
28
28
|
|
|
@@ -57,8 +57,8 @@ export function openCalendarPopup(opener) {
|
|
|
57
57
|
const close = () => closeCalendarPopup(true);
|
|
58
58
|
const host = el('div', { class: 'calpop-host', 'data-c3pg': 'calpop-host' });
|
|
59
59
|
const closeBtn = c3ib('Close', 'x', { title: 'Close (Esc)', 'data-c3pg': 'calpop-close', onclick: close }, 'sm');
|
|
60
|
-
const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic('cal'), el('b', null, 'Calendar')), el('span', { class: 'chips2' }, closeBtn));
|
|
61
|
-
const dlg = el('div', { class: 'sheet bs calpop', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Calendar', tabindex: '-1', 'data-c3pg': 'calpop' },
|
|
60
|
+
const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic('cal'), el('b', null, 'Calendar')), el('span', { class: 'chips2' }, el('span', { class: 'dim calpop-esc' }, 'Esc to close'), closeBtn));
|
|
61
|
+
const dlg = el('div', { class: 'sheet bs dw calpop', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Calendar', tabindex: '-1', 'data-c3pg': 'calpop' },
|
|
62
62
|
el('div', { class: 'grab' }), head, el('div', { class: 'sb' }, host));
|
|
63
63
|
const wrap = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'calpop' }, el('div', { class: 'scrim', onclick: close }), dlg);
|
|
64
64
|
const stopPhone = c3Phone(wrap);
|
|
@@ -73,7 +73,7 @@ export function openCalendarPopup(opener) {
|
|
|
73
73
|
document.addEventListener('keydown', onKey, true);
|
|
74
74
|
document.body.appendChild(wrap);
|
|
75
75
|
cur = { wrap, host, opener: from, onKey, stopPhone, obs: null };
|
|
76
|
-
mountCalendarViews(host, { view: zoom,
|
|
76
|
+
mountCalendarViews(host, { view: zoom, page: true, overlay: true }); // the same page grid as the Calendar tab, 36px an hour, no deck
|
|
77
77
|
/* remember the zoom as it changes, so a reload of the page also keeps it */
|
|
78
78
|
cur.obs = new MutationObserver(() => { const z = readZoom(host); if (z) { zoom = z; store.set(z); } });
|
|
79
79
|
cur.obs.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ['aria-pressed'] });
|