create-pmos 0.130.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 +4 -2
- package/workspace/c3-work-test.js +200 -0
- package/workspace/fixtures/one-home-baseline.json +0 -91
- package/workspace/guide/console-calendar.md +65 -0
- package/workspace/guide/index.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/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-calendar.js +392 -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 +10 -3
- package/workspace/public/pm/c3-shell.js +1 -1
- package/workspace/public/pm/calendar-views.js +10 -3
- package/workspace/public/pm/components.css +1 -0
- package/workspace/public/pm/console3.css +155 -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 +2 -0
- package/workspace/test-lists/calendar-layers.json +5 -0
- package/workspace/test-lists/calendar-time-page.json +5 -0
- package/workspace/test-lists/console3-work-page.json +9 -0
- package/workspace/public/pm/c3-demo-work2.js +0 -13
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/* pm/c3-calendar.js — the Calendar tab of /c3.html as a page about time (initiative calendar-time-page, PRD R1 to R8).
|
|
2
|
+
*
|
|
3
|
+
* calendar-views.js mounts it: mountCalendarViews(host, { page: true, ... }) makes draw() hand the instance to
|
|
4
|
+
* drawCalendarPage(host, r, helpers). That file keeps the data call (GET /api/calendar-views), the zoom bar, the Today
|
|
5
|
+
* chip and the swipe, pinch and key gestures; the helpers it passes are its own, so nothing is copied here.
|
|
6
|
+
*
|
|
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 markers on it,
|
|
9
|
+
* and the detail column (under the grid on a narrow screen, at the right from 1100px).
|
|
10
|
+
*
|
|
11
|
+
* What it reads from the answer, all optional and additive: `cells` (the meetings), `layers` (the dated items, each with
|
|
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) and `layers_note`.
|
|
14
|
+
* A key that is missing reads as "the server sent none", in words. Nothing is invented.
|
|
15
|
+
*
|
|
16
|
+
* 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
|
+
* It writes nothing. A marker carries no action: its detail has one reference link that opens the item where it lives. */
|
|
18
|
+
import { el } from './dom.js';
|
|
19
|
+
import { c3ic } from './pm-work-view.js';
|
|
20
|
+
import { button } from './primitives.js';
|
|
21
|
+
|
|
22
|
+
const LAYERS = [
|
|
23
|
+
['meetings', 'Meetings'],
|
|
24
|
+
['milestones', 'Milestones and due dates'],
|
|
25
|
+
['releases', 'Releases'],
|
|
26
|
+
['reports', 'Report dates'],
|
|
27
|
+
['mine', 'My due items'],
|
|
28
|
+
];
|
|
29
|
+
const KIND = {
|
|
30
|
+
milestone: ['Milestone', 'flag'], due: ['Due date', 'clock'], release: ['Release', 'bolt'], report: ['Report date', 'report'], own: ['My due item', 'todo'],
|
|
31
|
+
};
|
|
32
|
+
const RANK = { milestone: 0, release: 1, report: 2, due: 3, own: 4 };
|
|
33
|
+
const TONE = { bad: 'Late or at risk of missing', warn: 'At risk', ok: 'On track' };
|
|
34
|
+
const REF = { initiative: 'Open the initiative', 'plan-item': 'Open the initiative', item: 'Open in Work', release: 'Open the release', report: 'Open in Reports' };
|
|
35
|
+
const CTXWORD = { decision: 'Decision', action: 'Action', moved: 'Moved', prepared: 'Prepared', todo: 'To prepare' };
|
|
36
|
+
const NO_SUGGESTION = 'Free, nothing specific to suggest';
|
|
37
|
+
const PPH = 38; // pixels per hour on the Day and Week timelines (the mockup's vday)
|
|
38
|
+
|
|
39
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
40
|
+
const nowIso = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; };
|
|
41
|
+
const plural = (n, one, many) => n + ' ' + (n === 1 ? one : (many || one + 's'));
|
|
42
|
+
const kindOf = (m) => KIND[m.kind] || ['Dated item', 'cal'];
|
|
43
|
+
const hmToH = (s) => { const m = /^(\d{1,2}):(\d{2})/.exec(String(s || '')); return m ? Number(m[1]) + Number(m[2]) / 60 : null; };
|
|
44
|
+
const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name, String(value)); return node; };
|
|
45
|
+
|
|
46
|
+
/* ── the page's own state on the instance: which layers are on, whether free time shows, what is selected ─────────── */
|
|
47
|
+
|
|
48
|
+
function state(r) {
|
|
49
|
+
const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0])), free: false, sel: null });
|
|
50
|
+
if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
|
|
51
|
+
return st;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ── what the answer holds ────────────────────────────────────────────────────────────────────────────────────────── */
|
|
55
|
+
|
|
56
|
+
function model(d, h) {
|
|
57
|
+
const view = d.view || (d.range && d.range.view) || 'week';
|
|
58
|
+
const cells = d.cells || [];
|
|
59
|
+
const meetingsOf = (c) => (c.events || []).filter((e) => e.kind !== 'milestone'); // a milestone event is the older marker; the layers carry dates now
|
|
60
|
+
const meetings = cells.flatMap((c) => meetingsOf(c).map((e) => ({ e, date: String(e.at || c.start).slice(0, 10) })));
|
|
61
|
+
const layers = Array.isArray(d.layers) ? d.layers : null;
|
|
62
|
+
const free = d.free && Array.isArray(d.free.days) ? d.free : null;
|
|
63
|
+
const freeBy = new Map(); if (free) for (const x of free.days) freeBy.set(x.date, x.blocks || []);
|
|
64
|
+
const ctx = d.context && typeof d.context === 'object' ? d.context : null;
|
|
65
|
+
const findEvent = (id) => meetings.find((m) => m.e.id === id) || meetings.find((m) => h.baseId(m.e) === id) || null;
|
|
66
|
+
return { view, cells, meetingsOf, meetings, layers, free, freeBy, ctx, findEvent };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
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));
|
|
70
|
+
function marksIn(M, st, a, b) {
|
|
71
|
+
if (!M.layers) return [];
|
|
72
|
+
return M.layers.filter((m) => st.on.has(m.layer) && m.date >= a && m.date <= b).sort(sortMarks);
|
|
73
|
+
}
|
|
74
|
+
const freeIn = (M, a, b) => (M.free ? M.free.days.filter((x) => x.date >= a && x.date <= b).flatMap((x) => (x.blocks || []).map((bl) => ({ ...bl, date: x.date }))) : []);
|
|
75
|
+
|
|
76
|
+
/* ── the page ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
77
|
+
|
|
78
|
+
/** Draw the Calendar tab of instance r into a new node. h is calendar-views.js's own helpers (see PAGE there). */
|
|
79
|
+
export function drawCalendarPage(host, r, h) {
|
|
80
|
+
const st = state(r);
|
|
81
|
+
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
|
+
const bar = h.bar(host, r, phone);
|
|
85
|
+
const page = (...kids) => el('div', { class: 'cal3', 'data-cal3': 'page' }, head, bar, ...kids);
|
|
86
|
+
if (r.error) return page(fold('alert', 'The calendar could not be read: ' + r.error, { 'data-cv': 'error', role: 'status' }));
|
|
87
|
+
if (!r.data) return page(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
|
|
88
|
+
|
|
89
|
+
const d = r.data; const M = model(d, h);
|
|
90
|
+
const redraw = (focusSel) => { h.redraw(host, r); if (focusSel) { const n = r.root.querySelector(focusSel); if (n) n.focus(); } };
|
|
91
|
+
const select = (sel, focusSel) => {
|
|
92
|
+
st.sel = sel; redraw(focusSel);
|
|
93
|
+
const det = r.root.querySelector('[data-cal3="detail"]');
|
|
94
|
+
if (det && det.scrollIntoView && r.root.getBoundingClientRect().width < 1100) det.scrollIntoView({ block: 'nearest' });
|
|
95
|
+
};
|
|
96
|
+
const ui = { host, r, h, st, M, d, redraw, select };
|
|
97
|
+
|
|
98
|
+
const range = d.range || { start: d.today, end: d.today };
|
|
99
|
+
const nMeet = st.on.has('meetings') ? M.meetings.length : 0;
|
|
100
|
+
const nMarks = marksIn(M, st, range.start, range.end).length;
|
|
101
|
+
const empty = !nMeet && !nMarks ? fold('moon', M.meetings.length || (M.layers && M.layers.length)
|
|
102
|
+
? 'Every layer with items in this range is turned off. Turn a layer on to see it.'
|
|
103
|
+
: d.events_source === 'none' && !M.layers ? 'No calendar is cached yet, and the server sent no dated items. Nothing is drawn.'
|
|
104
|
+
: 'Nothing is scheduled and nothing falls due in this range.', { 'data-cal3': 'empty' }) : null;
|
|
105
|
+
|
|
106
|
+
let grid;
|
|
107
|
+
if (M.view === 'day' || M.view === 'week') {
|
|
108
|
+
const days = []; for (let x = range.start; x <= range.end && days.length < 7; x = h.shift('day', x, 1)) days.push(x);
|
|
109
|
+
grid = timeGrid(ui, days);
|
|
110
|
+
} else if (M.view === 'month') grid = monthGrid(ui);
|
|
111
|
+
else if (M.view === 'quarter') grid = quarterRows(ui);
|
|
112
|
+
else grid = yearCards(ui);
|
|
113
|
+
|
|
114
|
+
const body = el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') },
|
|
115
|
+
el('div', { class: 'cal3-wrap' },
|
|
116
|
+
el('div', { class: 'cal3-main' }, empty, grid),
|
|
117
|
+
el('div', { class: 'cal3-side' }, detail(ui), st.free ? freeList(ui, range) : null, lanes(ui, range))));
|
|
118
|
+
return page(chips(ui, range), body);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fold(icon, text, attrs) { return el('div', { class: 'fold', ...(attrs || {}) }, c3ic(icon, 'sm'), text); }
|
|
122
|
+
|
|
123
|
+
/* ── the layer chips ──────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
function chips(ui, range) {
|
|
126
|
+
const { st, M } = ui;
|
|
127
|
+
const count = (id) => (id === 'meetings' ? M.meetings.length : M.layers ? M.layers.filter((m) => m.layer === id && m.date >= range.start && m.date <= range.end).length : null);
|
|
128
|
+
const chip = (id, label, n, on, toggle, lead) => button({
|
|
129
|
+
label: [lead, label, n === null ? null : el('span', { class: 'cal3-n' }, String(n))], variant: 'ghost', size: 'sm',
|
|
130
|
+
attrs: { class: 'cal3-chip', 'aria-pressed': String(on), 'data-cal3-chip': id, title: (on ? 'Hide ' : 'Show ') + label },
|
|
131
|
+
onClick: () => { toggle(); ui.redraw('[data-cal3-chip="' + id + '"]'); },
|
|
132
|
+
});
|
|
133
|
+
const freeN = M.free ? (typeof M.free.count === 'number' ? M.free.count : freeIn(M, range.start, range.end).length) : null;
|
|
134
|
+
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), () => { if (st.on.has(id)) st.on.delete(id); else st.on.add(id); }, el('i', { class: 'cal3-sw cal3-sw-' + id }))),
|
|
136
|
+
el('span', { class: 'cal3-sep', 'aria-hidden': 'true' }),
|
|
137
|
+
chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm')));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── a marker: glyph and short title, tone from the tokens; pressing it selects it (the detail has the reference) ─── */
|
|
141
|
+
|
|
142
|
+
function marker(ui, m, compact) {
|
|
143
|
+
const [word, icon] = kindOf(m);
|
|
144
|
+
const sel = ui.st.sel && ui.st.sel.type === 'marker' && ui.st.sel.id === m.id;
|
|
145
|
+
const name = word + ': ' + m.title + ', ' + ui.h.dayLabel(m.date) + (m.tone && TONE[m.tone] ? '. ' + TONE[m.tone] : '');
|
|
146
|
+
return button({
|
|
147
|
+
label: [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, m.title)], variant: 'ghost', size: 'sm',
|
|
148
|
+
attrs: { class: 'cal3-mk cal3-k-' + (KIND[m.kind] ? m.kind : 'other') + (m.tone ? ' cal3-tone-' + m.tone : '') + (compact ? ' compact' : '') + (sel ? ' sel' : ''),
|
|
149
|
+
'data-cal3-mk': m.id, 'data-cal3-layer': m.layer, 'data-cal3-kind': m.kind, 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
|
|
150
|
+
onClick: () => ui.select({ type: 'marker', id: m.id }, '[data-cal3-mk="' + cssId(m.id) + '"]'),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const cssId = (s) => String(s).replace(/["\\]/g, '\\$&');
|
|
154
|
+
|
|
155
|
+
/* 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) {
|
|
157
|
+
const all = marksIn(ui.M, ui.st, a, b);
|
|
158
|
+
return [...all.slice(0, max).map((m) => marker(ui, m, compact)), all.length > max ? el('span', { class: 'cal3-more', title: plural(all.length - max, 'more dated item') }, '+' + (all.length - max)) : null];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ── Day and Week: the timeline with the all-day marker strip above it ───────────────────────────────────────────── */
|
|
162
|
+
|
|
163
|
+
function timeGrid(ui, days) {
|
|
164
|
+
const { M, st, h, d } = ui;
|
|
165
|
+
const now = nowIso(); const today = d.today;
|
|
166
|
+
const timedOn = (date) => (st.on.has('meetings') ? M.meetings.filter((m) => m.date === date && h.timeOf(m.e.at) !== null) : []);
|
|
167
|
+
const allDayOn = (date) => (st.on.has('meetings') ? M.meetings.filter((m) => m.date === date && h.timeOf(m.e.at) === null) : []);
|
|
168
|
+
const all = days.flatMap(timedOn);
|
|
169
|
+
const fr = M.free || {};
|
|
170
|
+
const t0 = Math.min(Math.floor(hmToH(fr.day_start) ?? 8), 8, ...all.map((m) => Math.floor(h.timeOf(m.e.at))));
|
|
171
|
+
const t1 = Math.max(Math.ceil(hmToH(fr.day_end) ?? 18), 18, ...all.map((m) => Math.ceil(h.endH(m.e))));
|
|
172
|
+
const Y = (t) => Math.round((Math.max(t0, Math.min(t1, t)) - t0) * PPH);
|
|
173
|
+
|
|
174
|
+
const tg = el('div', { class: 'cal3-tg' + (days.length === 1 ? ' one' : ''), 'data-cal3': days.length === 1 ? 'day' : 'week' });
|
|
175
|
+
setVar(tg, 'n', days.length); setVar(tg, 'hgt', (t1 - t0) * PPH + 'px'); setVar(tg, 'pph', PPH + 'px');
|
|
176
|
+
|
|
177
|
+
const headRow = el('div', { class: 'cal3-row cal3-dhead' }, el('div', { class: 'cal3-gut' }),
|
|
178
|
+
...days.map((date) => el('div', { class: 'cal3-dh' + (date === today ? ' today' : '') },
|
|
179
|
+
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' }) })
|
|
180
|
+
: el('b', null, h.dayLabel(date)))));
|
|
181
|
+
|
|
182
|
+
const strip = el('div', { class: 'cal3-row cal3-strip', 'data-cal3': 'strip' }, el('div', { class: 'cal3-gut' }, 'dates'),
|
|
183
|
+
...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)];
|
|
185
|
+
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
|
+
}));
|
|
187
|
+
|
|
188
|
+
const hours = el('div', { class: 'cal3-hours', 'aria-hidden': 'true' });
|
|
189
|
+
for (let t = t0; t < t1; t++) hours.appendChild(setVar(el('span', null, pad(t) + ':00'), 'y', Y(t) + 'px'));
|
|
190
|
+
|
|
191
|
+
const cols = days.map((date) => {
|
|
192
|
+
const wd = h.dowOf(date);
|
|
193
|
+
const col = el('div', { class: 'vday cal3-col' + (date === today ? ' today' : '') + (wd === 0 || wd === 6 ? ' we' : ''), 'data-cal3-col': date });
|
|
194
|
+
if (st.free) for (const b of (M.freeBy.get(date) || [])) col.appendChild(freeBlock(ui, date, b, Y));
|
|
195
|
+
const evs = timedOn(date).sort((a, b) => h.timeOf(a.e.at) - h.timeOf(b.e.at));
|
|
196
|
+
lanes4(evs, h).forEach(([m, lane, n]) => {
|
|
197
|
+
const s = h.timeOf(m.e.at); const f = h.endH(m.e);
|
|
198
|
+
const b = meetingButton(ui, m, 'vb cal3-ev' + (String(m.e.end || m.e.at) <= now ? ' past' : '') + (m.e.routine || m.e.mock_routine ? ' rt' : ''), [el('b', null, m.e.title || m.e.id), f - s >= 1 ? el('small', null, h.hhmm(s) + ' to ' + h.hhmm(f)) : null]);
|
|
199
|
+
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
|
+
col.appendChild(b);
|
|
201
|
+
});
|
|
202
|
+
if (date === today && now.slice(0, 10) === today) { const t = hmToH(now.slice(11)); if (t > t0 && t < t1) col.appendChild(setVar(el('div', { class: 'vnow cal3-now', 'aria-hidden': 'true' }), 'y', Y(t) + 'px')); }
|
|
203
|
+
return col;
|
|
204
|
+
});
|
|
205
|
+
tg.append(headRow, strip, el('div', { class: 'cal3-row cal3-body' }, hours, ...cols));
|
|
206
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-scroll' }, tg));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* overlapping meetings share the column side by side: [meeting, lane, lanes in its cluster] */
|
|
210
|
+
function lanes4(evs, h) {
|
|
211
|
+
const out = []; let cluster = []; let cEnd = -1;
|
|
212
|
+
const flush = () => { const ends = []; const at = cluster.map((m) => { let l = ends.findIndex((x) => x <= h.timeOf(m.e.at)); if (l < 0) { l = ends.length; ends.push(h.endH(m.e)); } else ends[l] = h.endH(m.e); return [m, l]; }); at.forEach(([m, l]) => out.push([m, l, ends.length])); cluster = []; };
|
|
213
|
+
for (const m of evs) { if (cluster.length && h.timeOf(m.e.at) >= cEnd) flush(); cluster.push(m); cEnd = Math.max(cEnd, h.endH(m.e)); }
|
|
214
|
+
if (cluster.length) flush();
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function meetingButton(ui, m, cls, label) {
|
|
219
|
+
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
|
+
const t = m.e.title || m.e.id; const s = ui.h.timeOf(m.e.at);
|
|
221
|
+
const name = t + ', ' + ui.h.dayLabel(m.date) + (s !== null ? ' at ' + ui.h.hhmm(s) : ', all day');
|
|
222
|
+
return button({
|
|
223
|
+
label: label || t, variant: 'ghost', size: 'sm',
|
|
224
|
+
attrs: { class: cls + (sel ? ' sel' : ''), 'data-id': m.e.id, 'data-cal3-ev': m.e.id, 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
|
|
225
|
+
onClick: () => ui.select({ type: 'event', id: m.e.id }, '[data-cal3-ev="' + cssId(m.e.id) + '"]'),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* a free block: the existing hatched window (.vf) with its suggested use, as text only */
|
|
230
|
+
function freeBlock(ui, date, b, Y) {
|
|
231
|
+
const s = hmToH(b.from); const f = hmToH(b.to);
|
|
232
|
+
const n = el('div', { class: 'vf cal3-free', 'data-cal3-free': date + ' ' + b.from },
|
|
233
|
+
el('b', null, b.from + ' to ' + b.to + ' free'), el('span', null, b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION));
|
|
234
|
+
setVar(n, 'y', (Y(s) + 2) + 'px'); setVar(n, 'h', Math.max(18, Y(f) - Y(s) - 4) + 'px');
|
|
235
|
+
return n;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ── Month: a cell per day with the meeting count, up to three markers and +N, and the free count ───────────────── */
|
|
239
|
+
|
|
240
|
+
function monthGrid(ui) {
|
|
241
|
+
const { M, st, h, d } = ui;
|
|
242
|
+
const days = M.cells.filter((c) => c.start === c.end);
|
|
243
|
+
const g = el('div', { class: 'cal3-month', 'data-cal3': 'month' }, ...['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((n) => el('div', { class: 'cal3-wd' }, n)));
|
|
244
|
+
const off = days.length ? (h.dowOf(days[0].start) + 6) % 7 : 0;
|
|
245
|
+
for (let i = 0; i < off; i++) g.appendChild(el('div', { class: 'cal3-mc out', 'aria-hidden': 'true' }));
|
|
246
|
+
for (const c of days) {
|
|
247
|
+
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
|
+
g.appendChild(el('div', { class: 'cal3-mc' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === d.today ? ' today' : ''), 'data-cal3-date': c.start },
|
|
250
|
+
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
|
+
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));
|
|
254
|
+
}
|
|
255
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* ── Quarter: a row per week; Year: a card per month. Counts, and the markers of the most important kinds ────────── */
|
|
259
|
+
|
|
260
|
+
function spanCounts(ui, c) {
|
|
261
|
+
const { M, st } = ui;
|
|
262
|
+
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
|
+
return el('div', { class: 'cal3-counts' },
|
|
265
|
+
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);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function quarterRows(ui) {
|
|
271
|
+
const { M, h, d } = ui;
|
|
272
|
+
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
|
+
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))));
|
|
275
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-quarter', 'data-cal3': 'quarter' }, ...rows));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function yearCards(ui) {
|
|
279
|
+
const { M, h, d } = ui;
|
|
280
|
+
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
|
+
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))));
|
|
283
|
+
return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-year', 'data-cal3': 'year' }, ...cards));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ── the detail column ────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
287
|
+
|
|
288
|
+
function refLink(ui, ref, extra) {
|
|
289
|
+
if (!ref || !ref.type || !ref.id) return el('span', { class: 'dim cal3-noptr' }, 'no pointer');
|
|
290
|
+
if (!REF[ref.type]) return el('span', { class: 'dim mono cal3-noptr' }, ref.type + ' ' + ref.id);
|
|
291
|
+
return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': ref.type + ':' + ref.id, title: REF[ref.type] + ': ' + ref.id,
|
|
292
|
+
onClick: (ev) => { ev.preventDefault(); openRef(ui, ref, extra || {}); } }, c3ic('out', 'sm'), REF[ref.type]);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* open the home of a reference: an initiative on Initiatives, a work item on Work, a report on Reports */
|
|
296
|
+
function openRef(ui, ref, extra) {
|
|
297
|
+
const { h, host, r } = ui;
|
|
298
|
+
if (ref.type === 'initiative') { if (!h.goInitiative(ref.id)) h.openInit(host, r, ref.id, extra.title); return; }
|
|
299
|
+
if (ref.type === 'plan-item' || ref.type === 'release') { const ini = extra.initiative || ref.initiative; if (!(ini && h.goInitiative(ini))) h.gotoTab('init'); return; }
|
|
300
|
+
if (ref.type === 'item') { h.gotoTab('work'); return; }
|
|
301
|
+
if (ref.type === 'report') h.gotoTab('rep');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function closeBtn(ui) {
|
|
305
|
+
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
|
+
}
|
|
307
|
+
|
|
308
|
+
function detail(ui) {
|
|
309
|
+
const { st, M, h } = ui;
|
|
310
|
+
const box = (kids, attrs) => el('section', { class: 'card cal3-detail', 'data-cal3': 'detail', 'aria-label': 'Detail', ...(attrs || {}) }, ...kids);
|
|
311
|
+
if (st.sel && st.sel.type === 'marker' && M.layers) {
|
|
312
|
+
const m = M.layers.find((x) => x.id === st.sel.id);
|
|
313
|
+
if (m) {
|
|
314
|
+
const [word, icon] = kindOf(m);
|
|
315
|
+
return box([
|
|
316
|
+
el('div', { class: 'cal3-dt' }, el('h3', null, c3ic(icon), word), closeBtn(ui)),
|
|
317
|
+
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) : '')),
|
|
319
|
+
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
|
+
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.'),
|
|
322
|
+
], { 'data-cal3-sel': m.id });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (st.sel && st.sel.type === 'event') {
|
|
326
|
+
const hit = M.findEvent(st.sel.id);
|
|
327
|
+
if (hit) return box(meetingDetail(ui, hit), { 'data-cal3-sel': hit.e.id });
|
|
328
|
+
}
|
|
329
|
+
return box([el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('info'), 'Detail')),
|
|
330
|
+
el('p', { class: 'dim cal3-note' }, 'Select a meeting to see what came out of it or what to prepare. Select a dated marker to see its date and open its home.')]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function meetingDetail(ui, hit) {
|
|
334
|
+
const { M, h } = ui; const e = hit.e;
|
|
335
|
+
const s = h.timeOf(e.at); const f = s === null ? null : h.endH(e);
|
|
336
|
+
const rec = M.ctx ? (M.ctx[e.id] || M.ctx[h.baseId(e)] || null) : null;
|
|
337
|
+
const phase = rec && rec.phase ? rec.phase : String(e.end || e.at) <= nowIso() ? 'past' : 'upcoming';
|
|
338
|
+
const past = phase === 'past';
|
|
339
|
+
const items = rec && Array.isArray(rec.items) ? rec.items : [];
|
|
340
|
+
const list = (kinds, title, key, none) => {
|
|
341
|
+
const rows = items.filter((x) => kinds.includes(x.kind));
|
|
342
|
+
return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, title),
|
|
343
|
+
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),
|
|
345
|
+
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
|
+
refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none));
|
|
347
|
+
};
|
|
348
|
+
let body;
|
|
349
|
+
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', 'PMOS holds nothing for this meeting: no note, no decision and no linked item.', { 'data-cal3': 'nothing-held' })];
|
|
351
|
+
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.')];
|
|
352
|
+
else body = [list(['prepared'], 'Prepared', 'prepared', 'Nothing is prepared yet.'), list(['todo'], 'To prepare', 'todo', 'Nothing is left to prepare.')];
|
|
353
|
+
return [
|
|
354
|
+
el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), closeBtn(ui)),
|
|
355
|
+
el('b', { class: 'cal3-title' }, e.title || e.id),
|
|
356
|
+
el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
|
|
357
|
+
...body,
|
|
358
|
+
rec && rec.held !== false ? el('p', { class: 'dim cal3-note' }, 'Only what PMOS holds a record for. Each line opens its source.') : null,
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* the free blocks of the range in words, with the suggested use or none; nothing is written */
|
|
363
|
+
function freeList(ui, range) {
|
|
364
|
+
const { M, h } = ui;
|
|
365
|
+
const head = el('h3', null, c3ic('clock'), 'Free time');
|
|
366
|
+
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
|
+
const all = freeIn(M, range.start, range.end);
|
|
368
|
+
const cap = 12;
|
|
369
|
+
return el('section', { class: 'card cal3-freel', 'data-cal3': 'free-list' }, head,
|
|
370
|
+
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
|
+
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
|
+
el('span', { class: 'dim mono cal3-at' }, h.dayLabel(b.date) + ' ' + b.from + ' to ' + b.to),
|
|
373
|
+
el('span', { class: 'cal3-ct' }, b.suggestion && b.suggestion.text ? b.suggestion.text : NO_SUGGESTION),
|
|
374
|
+
b.suggestion && b.suggestion.ref ? refLink(ui, b.suggestion.ref, { title: b.suggestion.text }) : null)))
|
|
375
|
+
: 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);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* each lane in words: its count in the range, or why it is empty */
|
|
380
|
+
function lanes(ui, range) {
|
|
381
|
+
const { M, st, d } = ui;
|
|
382
|
+
const notes = d.layers_note && typeof d.layers_note === 'object' ? d.layers_note : {};
|
|
383
|
+
const rows = LAYERS.map(([id, label]) => {
|
|
384
|
+
const n = id === 'meetings' ? M.meetings.length : M.layers ? M.layers.filter((m) => m.layer === id && m.date >= range.start && m.date <= range.end).length : null;
|
|
385
|
+
const why = n ? plural(n, id === 'meetings' ? 'meeting' : 'item') + (st.on.has(id) ? '' : ', hidden')
|
|
386
|
+
: id === 'meetings' ? (d.events_source === 'none' ? 'No calendar is cached yet.' : 'No meeting in this range.')
|
|
387
|
+
: n === null ? 'The calendar sent no dated items, so this layer is empty.' : notes[id] || 'Nothing in this range.';
|
|
388
|
+
return el('li', { class: 'cal3-lane', 'data-cal3-lane': id }, el('i', { class: 'cal3-sw cal3-sw-' + id }), el('span', null, label), el('span', { class: 'dim cal3-why' }, why));
|
|
389
|
+
});
|
|
390
|
+
const extra = Object.keys(notes).filter((k) => !LAYERS.some((l) => l[0] === k)).map((k) => el('li', { class: 'cal3-lane dim' }, el('span', null, k), el('span', { class: 'cal3-why' }, String(notes[k]))));
|
|
391
|
+
return el('section', { class: 'card cal3-lanes', 'data-cal3': 'lanes' }, el('h3', null, c3ic('filter'), 'In this range'), el('ul', { class: 'cal3-cl', role: 'list' }, ...rows, ...extra));
|
|
392
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* pm/c3-demo-cal4.js — DEMO fixtures for the Calendar page (initiative calendar-time-page) on /c3.html?demo=1.
|
|
2
|
+
* c3-demo.js passes every GET /api/calendar-views answer through withCalendarPage(), which adds the three keys the page reads:
|
|
3
|
+
* layers the dated items of the range (milestones and due dates, releases, report dates, my due items), each with its kind,
|
|
4
|
+
* its layer, its tone and the reference to its home
|
|
5
|
+
* free the free blocks of 90 minutes or more between 09:00 and 18:00 on the working days from today, each with a
|
|
6
|
+
* suggested use or none
|
|
7
|
+
* context for some meetings: what came out of a past one and what moved after it, or what is prepared and what to prepare
|
|
8
|
+
* for a coming one. Every claim carries a pointer. One past meeting has nothing held, to show the honest state.
|
|
9
|
+
* The content reuses the demo's initiatives (checkout, sso, search, returns), its people and its meetings and work items
|
|
10
|
+
* (c3-demo-data.js). Real mode never loads this file. Nothing here reaches the server. */
|
|
11
|
+
import * as D from './c3-demo-data.js';
|
|
12
|
+
|
|
13
|
+
const DAY = 86400000;
|
|
14
|
+
const addDays = (iso, n) => new Date(Date.parse(iso + 'T00:00:00Z') + n * DAY).toISOString().slice(0, 10);
|
|
15
|
+
const dow = (iso) => new Date(Date.parse(iso + 'T00:00:00Z')).getUTCDay();
|
|
16
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
17
|
+
const hm = (min) => pad(Math.floor(min / 60)) + ':' + pad(min % 60);
|
|
18
|
+
const minOf = (at) => { const m = /T(\d{2}):(\d{2})/.exec(String(at || '')); return m ? Number(m[1]) * 60 + Number(m[2]) : null; };
|
|
19
|
+
|
|
20
|
+
const init = (id) => ({ type: 'initiative', id });
|
|
21
|
+
const item = (id) => ({ type: 'item', id });
|
|
22
|
+
|
|
23
|
+
/* the dated items: [date, kind, layer, title, tone, ref, initiative] */
|
|
24
|
+
const DATED = [
|
|
25
|
+
['2026-10-09', 'due', 'milestones', 'Copy freeze (Lee)', null, { type: 'plan-item', id: 'checkout-copy' }, 'checkout'],
|
|
26
|
+
['2026-10-12', 'milestone', 'milestones', 'SSO freeze', 'warn', init('sso'), 'sso'],
|
|
27
|
+
['2026-10-13', 'milestone', 'milestones', 'Interview 5: park or continue Returns', null, init('returns'), 'returns'],
|
|
28
|
+
['2026-10-24', 'milestone', 'milestones', 'Ship Checkout v2 (promised)', 'bad', init('checkout'), 'checkout'],
|
|
29
|
+
['2026-11-06', 'milestone', 'milestones', 'Search relevance beta', 'warn', init('search'), 'search'],
|
|
30
|
+
['2026-12-14', 'due', 'milestones', 'Board update review', null, { type: 'plan-item', id: 'board-update' }, 'checkout'],
|
|
31
|
+
['2026-10-14', 'release', 'releases', 'SSO general release', 'warn', { type: 'release', id: 'rel-sso-1' }, 'sso'],
|
|
32
|
+
['2026-10-31', 'release', 'releases', 'Checkout v2 on the full scope', 'bad', { type: 'release', id: 'rel-checkout-2' }, 'checkout'],
|
|
33
|
+
['2026-11-20', 'release', 'releases', 'Search relevance 1.0', null, { type: 'release', id: 'rel-search-1' }, 'search'],
|
|
34
|
+
['2026-12-20', 'report', 'reports', 'Quarterly board update', null, { type: 'report', id: 'board-quarterly' }, null],
|
|
35
|
+
['2026-10-06', 'own', 'mine', 'Payments sandbox keys', 'bad', item('item-demo018'), 'checkout'],
|
|
36
|
+
['2026-10-08', 'own', 'mine', 'Reply to Marta: SSO 14 Oct, SCIM in Nov', 'warn', item('item-demo011'), 'sso'],
|
|
37
|
+
['2026-10-12', 'own', 'mine', 'Confirm the SSO date to Marta', null, item('item-demo016'), 'sso'],
|
|
38
|
+
['2026-10-13', 'own', 'mine', 'Share returns insights with sales', null, item('item-demo017'), 'returns'],
|
|
39
|
+
['2026-11-16', 'own', 'mine', 'Write the Q1 discovery plan', null, item('item-demo020'), null],
|
|
40
|
+
];
|
|
41
|
+
/* the weekly status to leadership is a report date every Friday */
|
|
42
|
+
for (let d = '2026-09-04'; d <= '2026-12-25'; d = addDays(d, 7)) DATED.push([d, 'report', 'reports', 'Weekly status to leadership', null, { type: 'report', id: 'leadership-weekly' }, null]);
|
|
43
|
+
|
|
44
|
+
function layersIn(start, end) {
|
|
45
|
+
return DATED.filter((x) => x[0] >= start && x[0] <= end).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
46
|
+
.map(([date, kind, layer, title, tone, ref, initiative], i) => ({ id: 'dated-' + date + '-' + i, date, kind, layer, title, tone, ref, ...(initiative ? { initiative } : {}) }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* the suggested uses of a free block, from the demo's work items; a block past the first of a day gets none */
|
|
50
|
+
const SUGGEST = [
|
|
51
|
+
{ text: 'Prep for Sprint planning at 14:00: settle the scope cut first', ref: item('item-demo010') },
|
|
52
|
+
{ text: 'Draft the weekly status for leadership, due Friday', ref: item('item-demo012') },
|
|
53
|
+
{ text: 'Confirm the SSO date to Marta before the freeze', ref: item('item-demo016') },
|
|
54
|
+
{ text: 'Write the Q1 discovery plan, due 16 Nov', ref: item('item-demo020') },
|
|
55
|
+
];
|
|
56
|
+
const MIN = 90; const START = 9 * 60; const END = 18 * 60;
|
|
57
|
+
|
|
58
|
+
function meetingsOn(date) {
|
|
59
|
+
const day = D.calendarViews('/api/calendar-views?view=day&date=' + date);
|
|
60
|
+
return ((day.cells[0] && day.cells[0].events) || []).filter((e) => e.kind !== 'milestone')
|
|
61
|
+
.map((e) => [minOf(e.at), minOf(e.end)]).filter((x) => x[0] !== null && x[1] !== null).sort((a, b) => a[0] - b[0]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function freeIn(start, end) {
|
|
65
|
+
const days = []; let n = 0; let k = 0;
|
|
66
|
+
for (let d = start < D.TODAY ? D.TODAY : start; d <= end; d = addDays(d, 1)) {
|
|
67
|
+
if (dow(d) === 0 || dow(d) === 6) continue;
|
|
68
|
+
const blocks = []; let cur = START;
|
|
69
|
+
for (const [s, e] of [...meetingsOn(d), [END, END]]) {
|
|
70
|
+
const a = Math.max(cur, START); const b = Math.min(s, END);
|
|
71
|
+
if (b - a >= MIN) blocks.push({ from: hm(a), to: hm(b), minutes: b - a, suggestion: blocks.length ? null : SUGGEST[k++ % SUGGEST.length] });
|
|
72
|
+
cur = Math.max(cur, e);
|
|
73
|
+
}
|
|
74
|
+
n += blocks.length;
|
|
75
|
+
if (blocks.length) days.push({ date: d, blocks });
|
|
76
|
+
}
|
|
77
|
+
return { min_minutes: MIN, day_start: hm(START), day_end: hm(END), days, count: n };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* what PMOS holds about some meetings. The keys are the demo's event ids (base id @ date). */
|
|
81
|
+
const CONTEXT = {
|
|
82
|
+
'm2@2026-10-07': { phase: 'past', held: true, items: [
|
|
83
|
+
{ kind: 'decision', text: 'Cut Checkout v2 to three core flows if the sandbox keys slip past Friday', ref: init('checkout'), at: '2026-10-07T11:15:00' },
|
|
84
|
+
{ kind: 'action', text: 'Dara confirms the payments sandbox keys', ref: item('item-demo018'), at: '2026-10-07T11:15:00' },
|
|
85
|
+
{ kind: 'action', text: 'Lee sends the annual-plan label', ref: item('item-demo019'), at: '2026-10-07T11:15:00' },
|
|
86
|
+
{ kind: 'moved', text: 'Payments sandbox keys moved to Waiting for Dara', ref: item('item-demo018'), at: '2026-10-07T14:10:00' },
|
|
87
|
+
{ kind: 'moved', text: 'The Checkout v2 forecast moved from 24 Oct to 31 Oct', ref: init('checkout'), at: '2026-10-07T16:40:00' },
|
|
88
|
+
] },
|
|
89
|
+
'm3@2026-10-06': { phase: 'past', held: false, items: [] },
|
|
90
|
+
'm1@2026-10-08': { phase: 'upcoming', held: true, items: [
|
|
91
|
+
{ kind: 'prepared', text: 'Async summary of the 3 open tickets', ref: init('search') },
|
|
92
|
+
] },
|
|
93
|
+
'm2@2026-10-08': { phase: 'upcoming', held: true, items: [
|
|
94
|
+
{ kind: 'prepared', text: 'Agenda: scope cut, blockers, next steps', ref: init('checkout') },
|
|
95
|
+
{ kind: 'prepared', text: 'Pre-read: the Checkout v2 PRD with 2 open comments', ref: { type: 'plan-item', id: 'checkout-prd' } },
|
|
96
|
+
{ kind: 'todo', text: 'Confirm the 1 Oct numbers before the 10:30 update', ref: item('item-demo001') },
|
|
97
|
+
] },
|
|
98
|
+
'm3@2026-10-08': { phase: 'upcoming', held: true, items: [
|
|
99
|
+
{ kind: 'prepared', text: 'Interview guide, draft 2', ref: init('returns') },
|
|
100
|
+
{ kind: 'todo', text: 'Review the 13:00 interview guide', ref: item('item-demo004') },
|
|
101
|
+
] },
|
|
102
|
+
'm4@2026-10-08': { phase: 'upcoming', held: true, items: [
|
|
103
|
+
{ kind: 'prepared', text: 'Three scope options with their dates', ref: init('checkout') },
|
|
104
|
+
{ kind: 'todo', text: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', ref: item('item-demo010') },
|
|
105
|
+
] },
|
|
106
|
+
'm5@2026-10-08': { phase: 'upcoming', held: false, items: [] },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The answer of GET /api/calendar-views with the Calendar page's keys added. */
|
|
110
|
+
export function withCalendarPage(out) {
|
|
111
|
+
if (!out || !out.range) return out;
|
|
112
|
+
const { start, end } = out.range;
|
|
113
|
+
return { ...out, layers: layersIn(start, end), free: freeIn(start, end), context: CONTEXT };
|
|
114
|
+
}
|