create-pmos 0.178.0 → 0.180.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-report-setup-test.js +190 -0
- package/workspace/c3-reports-test.js +30 -97
- package/workspace/guide/console-calendar.md +17 -2
- package/workspace/guide/document-sheet.md +14 -0
- package/workspace/guide/index.md +3 -2
- package/workspace/guide/reports-dashboards.md +17 -28
- package/workspace/guide/reports-page.md +10 -5
- package/workspace/guide/reports-set-up.md +56 -0
- package/workspace/guide/reports.md +13 -1
- package/workspace/guide/settings.md +11 -2
- package/workspace/lib/doc-sync.js +28 -6
- package/workspace/lib/docs-cli.js +7 -11
- package/workspace/lib/four-panel.js +1 -1
- package/workspace/lib/headless.js +21 -2
- package/workspace/lib/notes-series.js +357 -0
- package/workspace/lib/notes.js +53 -4
- package/workspace/lib/report-checks.js +103 -0
- package/workspace/lib/report-cli.js +89 -4
- package/workspace/lib/report-export.js +347 -0
- package/workspace/lib/report-finder.js +156 -0
- package/workspace/lib/report-needs.js +1 -1
- package/workspace/lib/report-setups.js +743 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-c3-reports.js +35 -17
- package/workspace/lib/routes-doc-health.js +15 -1
- package/workspace/lib/routes-notes.js +27 -2
- package/workspace/lib/routes-report-setups.js +368 -0
- package/workspace/lib/schedule-logic.js +5 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-rep2.js +15 -0
- package/workspace/public/pm/c3-demo-rs.js +281 -0
- package/workspace/public/pm/c3-demo.js +1 -1
- package/workspace/public/pm/c3-doc-formats.js +152 -0
- package/workspace/public/pm/c3-doc-sheet.js +115 -25
- package/workspace/public/pm/c3-notes.js +299 -34
- package/workspace/public/pm/c3-quiet.js +2 -2
- package/workspace/public/pm/c3-report-print.css +20 -0
- package/workspace/public/pm/c3-report-setup.css +62 -0
- package/workspace/public/pm/c3-report-setup.js +260 -0
- package/workspace/public/pm/c3-report-store.js +208 -0
- package/workspace/public/pm/c3-reports.css +4 -0
- package/workspace/public/pm/c3-reports.js +47 -181
- package/workspace/public/pm/c3-settings.js +4 -3
- package/workspace/public/pm/c3-shell.js +15 -31
- package/workspace/public/pm/console3.css +13 -0
- package/workspace/run-tests.js +25 -1
- package/workspace/server.js +66 -5
- package/workspace/skills/doc-sync.skill +17 -8
- package/workspace/skills/report-find.skill +75 -0
- package/workspace/skills/report-setup.skill +67 -0
- package/workspace/test-files.json +12 -0
- package/workspace/test-lists/c3-notes-series.json +5 -0
- package/workspace/test-lists/report-workspace-ui.json +5 -0
|
@@ -64,14 +64,15 @@ const transcriptOf = (n) => (n.final && n.final.segments && n.final.segments.len
|
|
|
64
64
|
|
|
65
65
|
/** The notes state on the calendar instance r. */
|
|
66
66
|
function stateOf(r) {
|
|
67
|
-
if (!r.notes) r.notes = { box: el('div', { class: 'cal3-notes', 'data-cal3': 'notes' }), rows: null, err: null, open: null, note: null, phase: null, q: '', seq: 0, live: null, unsub: null
|
|
67
|
+
if (!r.notes) r.notes = { box: el('div', { class: 'cal3-notes', 'data-cal3': 'notes' }), rows: null, err: null, open: null, note: null, phase: null, q: '', seq: 0, live: null, unsub: null,
|
|
68
|
+
ser: null, thread: null, place: null, expand: new Set(), older: new Set(), found: null, every: false, open_by: {} };
|
|
68
69
|
return r.notes;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** The note the Calendar shows next: from the Record chip (phase During) or a meeting's detail (its default stage). */
|
|
72
73
|
export function openNote(r, id, phase) {
|
|
73
74
|
const s = stateOf(r);
|
|
74
|
-
s.open = id; s.note = null; s.phase = phase || null;
|
|
75
|
+
s.open = id; s.note = null; s.phase = phase || null; s.ser = null; s.thread = null;
|
|
75
76
|
r.page = r.page || null;
|
|
76
77
|
if (r.page) r.page.mode = 'notes';
|
|
77
78
|
else r.pendingMode = 'notes';
|
|
@@ -92,18 +93,25 @@ export function notesView(ui) {
|
|
|
92
93
|
s.ui = ui;
|
|
93
94
|
if (!s.unsub) s.unsub = onNotes(() => { if (s.box.isConnected) read(s); });
|
|
94
95
|
if (!s.boards) loadBoards(s);
|
|
95
|
-
if (!s.rows || (s.open && (!s.note || s.note.id !== s.open))) read(s);
|
|
96
|
+
if (!s.rows || (s.open && (!s.note || s.note.id !== s.open)) || (s.ser && (!s.thread || s.thread.key !== s.ser))) read(s);
|
|
96
97
|
draw(s);
|
|
97
98
|
return s.box;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async function read(s) {
|
|
101
102
|
const seq = ++s.seq;
|
|
102
|
-
const
|
|
103
|
+
const ser = s.ser;
|
|
104
|
+
const [l, one, th] = await Promise.all([pmGet('/api/notes'), !ser && s.open ? pmGet('/api/notes/one?id=' + encodeURIComponent(s.open)) : null,
|
|
105
|
+
ser ? pmGet('/api/notes/series?key=' + encodeURIComponent(ser)) : null]);
|
|
103
106
|
if (seq !== s.seq) return;
|
|
104
|
-
if (l && l.ok) { s.rows = l.notes || []; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
|
|
107
|
+
if (l && l.ok) { s.rows = l.notes || []; s.open_by = l.series_open || {}; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
|
|
108
|
+
if (ser) {
|
|
109
|
+
if (th && th.ok && th.series) s.thread = th.series;
|
|
110
|
+
else { s.ser = null; s.thread = null; toast('That meeting series could not be read' + (th && th.error ? ': ' + th.error : '.')); }
|
|
111
|
+
draw(s); return;
|
|
112
|
+
}
|
|
105
113
|
if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
|
|
106
|
-
if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.rec = one.recording || null; }
|
|
114
|
+
if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.rec = one.recording || null; s.place = one.place || null; }
|
|
107
115
|
draw(s);
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -114,9 +122,15 @@ async function loadBoards(s) {
|
|
|
114
122
|
if (r && r.ok && Array.isArray(r.boards)) { s.boards = r.boards.filter((b) => b && b.id && !b.error); if (s.box.isConnected) draw(s); }
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
function choose(s, id) {
|
|
118
|
-
if (s.open === id) return;
|
|
119
|
-
s.open = id; s.note = null; s.phase = null; s.shots = []; s.rec = null;
|
|
125
|
+
function choose(s, id, phase) {
|
|
126
|
+
if (s.open === id && !s.ser) return;
|
|
127
|
+
s.open = id; s.note = null; s.phase = phase || null; s.shots = []; s.rec = null; s.place = null; s.ser = null; s.thread = null;
|
|
128
|
+
draw(s); read(s);
|
|
129
|
+
}
|
|
130
|
+
/* open a meeting series on the right (the series page) */
|
|
131
|
+
function chooseSeries(s, key) {
|
|
132
|
+
if (s.ser === key) return;
|
|
133
|
+
s.ser = key; s.thread = null; s.every = false; s.note = null; s.open = null; s.place = null;
|
|
120
134
|
draw(s); read(s);
|
|
121
135
|
}
|
|
122
136
|
|
|
@@ -127,61 +141,135 @@ function draw(s) {
|
|
|
127
141
|
if (active && s.box.contains(active) && active.matches('textarea, [contenteditable="true"]') && !active.closest('[data-nt-part="live"]')) return;
|
|
128
142
|
const q = s.box.querySelector('[data-cal3-notes="search"]');
|
|
129
143
|
const typing = !!q && active === q; const caret = typing ? q.selectionStart : null;
|
|
130
|
-
s.box.replaceChildren(el('div', { class: 'cal3-nwrap' }, listCard(s), noteCol(s)));
|
|
144
|
+
s.box.replaceChildren(el('div', { class: 'cal3-nwrap' }, listCard(s), s.ser ? seriesCol(s) : noteCol(s)));
|
|
131
145
|
s.drawn = true;
|
|
132
146
|
if (typing) { const nq = s.box.querySelector('[data-cal3-notes="search"]'); if (nq) { nq.focus(); nq.setSelectionRange(caret, caret); } }
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
/* ── the list ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
136
150
|
|
|
151
|
+
const OLD_DAYS = 30;
|
|
152
|
+
const SEARCH_MIN = 2;
|
|
153
|
+
const seriesKeyOf = (r) => (r && r.series ? r.series.key : null);
|
|
154
|
+
|
|
137
155
|
function listCard(s) {
|
|
138
156
|
const qtext = s.q.trim().toLowerCase();
|
|
157
|
+
const found = s.found && s.found.q === qtext ? s.found.ids : null;
|
|
139
158
|
const shown = (s.rows || []).filter((r) => !qtext || String(r.title || '').toLowerCase().includes(qtext) || pathOf(r).some((x) => String(x).toLowerCase().includes(qtext))
|
|
140
|
-
|| (r.summary_lines || []).some((x) => String(x).toLowerCase().includes(qtext)));
|
|
141
|
-
const search = el('input', { type: 'text', class: 'cal3-nsearch', placeholder: 'Search notes', 'aria-label': 'Search notes', value: s.q, 'data-cal3-notes': 'search' });
|
|
142
|
-
search.addEventListener('input', () => { s.q = search.value; draw(s); });
|
|
159
|
+
|| (r.summary_lines || []).some((x) => String(x).toLowerCase().includes(qtext)) || (found && found.has(r.id)));
|
|
160
|
+
const search = el('input', { type: 'text', class: 'cal3-nsearch', placeholder: 'Search notes, decisions, what was said', 'aria-label': 'Search notes', value: s.q, 'data-cal3-notes': 'search' });
|
|
161
|
+
search.addEventListener('input', () => { s.q = search.value; draw(s); findLater(s); });
|
|
143
162
|
let body;
|
|
144
163
|
if (!s.rows) body = fold('clock', 'Reading the notes', { role: 'status' });
|
|
145
164
|
else if (!s.rows.length) body = fold('mic', s.err || 'No notes yet. Press Record at the top right to start one.', { 'data-cal3-notes': 'none' });
|
|
146
|
-
else if (!shown.length) body = fold('search', 'No note matches “' + s.q.trim() + '”.', { 'data-cal3-notes': 'no-match' });
|
|
147
|
-
else body = scopeGroups(s, shown).map((g) =>
|
|
148
|
-
|
|
165
|
+
else if (!shown.length) body = fold('search', qtext.length >= SEARCH_MIN && !found ? 'Searching the notes' : 'No note matches “' + s.q.trim() + '”.', { 'data-cal3-notes': 'no-match' });
|
|
166
|
+
else body = scopeGroups(s, shown).map((g) => {
|
|
167
|
+
const old = qtext ? [] : g.entries.filter((e) => isOld(e));
|
|
168
|
+
const recent = qtext ? g.entries : g.entries.filter((e) => !isOld(e));
|
|
169
|
+
const open = s.older.has(g.key);
|
|
170
|
+
const olderN = old.reduce((t, e) => t + e.rows.length, 0);
|
|
171
|
+
const rows = [...recent.flatMap((e) => entryRows(s, e, !!qtext)), ...(open ? old.flatMap((e) => entryRows(s, e, false)) : [])];
|
|
172
|
+
if (old.length) rows.push(el('li', { class: 'nrow cal3-nmore' }, btn('chip2 lk', { 'data-cal3-notes': 'older', 'aria-expanded': String(open) },
|
|
173
|
+
open ? 'Hide the older notes' : [c3ic('clock', 'sm'), 'Older than ' + OLD_DAYS + ' days · ' + olderN + (olderN === 1 ? ' note' : ' notes')],
|
|
174
|
+
() => { if (open) s.older.delete(g.key); else s.older.add(g.key); draw(s); })));
|
|
175
|
+
return el('div', { class: 'cal3-ctx', 'data-cal3-notes-group': g.label }, el('h4', null, g.label), el('ul', { class: 'list', role: 'list' }, rows));
|
|
176
|
+
});
|
|
149
177
|
return el('section', { class: 'card sect', 'data-cal3-notes': 'list', 'aria-label': 'Notes' },
|
|
150
178
|
el('div', { class: 'sh2' }, el('h2', null, c3ic('notes', 'sm'), 'Notes ', el('span', { class: 'bdg g', 'data-cal3-notes': 'count' }, String(shown.length)))),
|
|
151
179
|
el('div', { class: 'cal3-nsearchw' }, search), body);
|
|
152
180
|
}
|
|
181
|
+
const isOld = (e) => { const t = Date.parse(startOf(e.rows[0])); return Number.isFinite(t) && Date.now() - t > OLD_DAYS * 86400000 && !e.rows.some((r) => held(r.id) || r.state === 'recording' || r.state === 'paused'); };
|
|
182
|
+
|
|
183
|
+
/* search the notes' text on the server (GET /api/notes/search), a moment after the last key */
|
|
184
|
+
function findLater(s) {
|
|
185
|
+
clearTimeout(s.findT);
|
|
186
|
+
const q = s.q.trim().toLowerCase();
|
|
187
|
+
if (q.length < SEARCH_MIN) { s.found = null; return; }
|
|
188
|
+
s.findT = setTimeout(async () => {
|
|
189
|
+
const r = await pmGet('/api/notes/search?q=' + encodeURIComponent(q));
|
|
190
|
+
if (s.q.trim().toLowerCase() !== q) return;
|
|
191
|
+
s.found = { q, ids: new Set(r && r.ok && Array.isArray(r.ids) ? r.ids : []) };
|
|
192
|
+
if (s.box.isConnected) draw(s);
|
|
193
|
+
}, 220);
|
|
194
|
+
}
|
|
153
195
|
|
|
154
196
|
/* the list's groups: Portfolio, each board in the portfolio's order (a board it does not list goes after them by name), an
|
|
155
|
-
* initiative linked before boards were (no board on the note) under "Initiatives", then "Not linked"
|
|
156
|
-
*
|
|
197
|
+
* initiative linked before boards were (no board on the note) under "Initiatives", then "Not linked". The notes of one meeting
|
|
198
|
+
* series (lib/notes-series.js, two or more notes with one key) are one entry, in the group of their newest note. In a group,
|
|
199
|
+
* the newest meeting (or recording) first. → [{ key, label, entries: [{ key, rows (newest first) }] }] */
|
|
157
200
|
function scopeGroups(s, rows) {
|
|
158
201
|
const order = new Map((s.boards || []).map((b, i) => [boardKey(b), i]));
|
|
159
202
|
const known = new Map((s.boards || []).map((b) => [boardKey(b), b.name || b.id]));
|
|
160
|
-
const
|
|
203
|
+
const t = (r) => Date.parse(startOf(r)) || 0;
|
|
204
|
+
const all = (s.rows || []);
|
|
205
|
+
const size = new Map();
|
|
206
|
+
for (const r of all) { const k = seriesKeyOf(r); if (k) size.set(k, (size.get(k) || 0) + 1); }
|
|
207
|
+
const entries = new Map();
|
|
161
208
|
for (const r of rows) {
|
|
209
|
+
const k = seriesKeyOf(r);
|
|
210
|
+
const ek = k && size.get(k) >= 2 ? 'S' + k : 'N' + r.id;
|
|
211
|
+
if (!entries.has(ek)) entries.set(ek, { key: ek, series: ek[0] === 'S' ? k : null, rows: [], size: k ? size.get(k) : 1 });
|
|
212
|
+
entries.get(ek).rows.push(r);
|
|
213
|
+
}
|
|
214
|
+
const g = new Map();
|
|
215
|
+
for (const e of entries.values()) {
|
|
216
|
+
e.rows.sort((a, b) => t(b) - t(a));
|
|
217
|
+
const r = e.rows[0];
|
|
162
218
|
const k = r.portfolio ? '@portfolio' : r.board ? boardKey(r.board) : r.initiative ? '@initiative' : '';
|
|
163
|
-
if (!g.has(k)) g.set(k, { key: k, label: k === '@portfolio' ? 'Portfolio' : k === '@initiative' ? 'Initiatives' : !k ? 'Not linked' : known.get(k) || boardName(r.board),
|
|
164
|
-
g.get(k).
|
|
219
|
+
if (!g.has(k)) g.set(k, { key: k, label: k === '@portfolio' ? 'Portfolio' : k === '@initiative' ? 'Initiatives' : !k ? 'Not linked' : known.get(k) || boardName(r.board), entries: [] });
|
|
220
|
+
g.get(k).entries.push(e);
|
|
165
221
|
}
|
|
166
222
|
// the server lists notes by when they were last edited; a group reads newest meeting first
|
|
167
|
-
for (const x of g.values()) x.
|
|
223
|
+
for (const x of g.values()) x.entries.sort((a, b) => t(b.rows[0]) - t(a.rows[0]));
|
|
168
224
|
const rank = (x) => (x.key === '@portfolio' ? [0, 0] : x.key === '@initiative' ? [3, 0] : !x.key ? [4, 0] : order.has(x.key) ? [1, order.get(x.key)] : [2, 0]);
|
|
169
225
|
return [...g.values()].sort((a, b) => { const p = rank(a); const q = rank(b); return p[0] - q[0] || p[1] - q[1] || a.label.localeCompare(b.label); });
|
|
170
226
|
}
|
|
171
227
|
|
|
172
|
-
|
|
228
|
+
/* the rows an entry draws: a note's row, or a series row with its newest meetings under it when open (or matching a search) */
|
|
229
|
+
function entryRows(s, e, searching) {
|
|
230
|
+
if (!e.series) return [noteRow(s, e.rows[0])];
|
|
231
|
+
const open = s.expand.has(e.series) || searching;
|
|
232
|
+
const out = [seriesRow(s, e, open)];
|
|
233
|
+
if (!open) return out;
|
|
234
|
+
const shown = e.rows.slice(0, 3);
|
|
235
|
+
out.push(...shown.map((r) => noteRow(s, r, true)));
|
|
236
|
+
const more = (searching ? e.rows.length : e.size) - shown.length;
|
|
237
|
+
if (more > 0) out.push(el('li', { class: 'nrow cal3-nsub cal3-nmore' }, btn('chip2 lk', { 'data-cal3-notes': 'series-all' },
|
|
238
|
+
searching ? more + ' more matching in the series' : 'All ' + e.size + ' meetings in the series', () => chooseSeries(s, e.series))));
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function seriesRow(s, e, open) {
|
|
243
|
+
const r = e.rows[0];
|
|
244
|
+
const on = s.ser === e.series;
|
|
245
|
+
const o = s.open_by[e.series];
|
|
246
|
+
const title = (r.meeting && r.meeting.kind === 'event' && r.meeting.title) || r.title || 'Untitled meeting';
|
|
247
|
+
return el('li', { class: 'nrow cal3-nrow cal3-nser' + (on ? ' on' : ''), 'data-cal3-series': e.series },
|
|
248
|
+
el('span', { class: 'ki' }, c3ic('notes')),
|
|
249
|
+
btn('cal3-nopen', { title: 'Open the series ' + title, 'aria-current': on ? 'true' : 'false' }, [
|
|
250
|
+
el('span', { class: 'tt' }, title),
|
|
251
|
+
el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, whenShort(startOf(r)) + ' · ' + e.size + ' meetings'),
|
|
252
|
+
el('span', { class: 'tag', title: 'A regular meeting: its notes are kept together' }, 'series'),
|
|
253
|
+
o ? el('span', { class: 'st ' + (o.carried ? 'warn' : 'unt'), title: o.open + (o.open === 1 ? ' action is' : ' actions are') + ' carried from meeting to meeting', 'data-cal3-notes': 'series-open' }, c3ic(o.carried ? 'alert' : 'todo', 'sm'), o.open + ' open') : null)],
|
|
254
|
+
() => chooseSeries(s, e.series)),
|
|
255
|
+
btn('ib sm', { 'data-cal3-notes': 'series-x', 'aria-expanded': String(open), title: open ? 'Hide its recent meetings' : 'Show its recent meetings', 'aria-label': open ? 'Hide recent meetings' : 'Show recent meetings' },
|
|
256
|
+
c3ic(open ? 'fold' : 'next', 'sm'), () => { if (s.expand.has(e.series)) s.expand.delete(e.series); else s.expand.add(e.series); draw(s); }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function noteRow(s, r, sub) {
|
|
173
260
|
const mode = held(r.id) || (r.state === 'recording' || r.state === 'paused' ? r.state : null);
|
|
174
|
-
const on = s.open === r.id;
|
|
261
|
+
const on = s.open === r.id && !s.ser;
|
|
175
262
|
const len = mins(r.meeting_ms || 0);
|
|
176
|
-
|
|
263
|
+
const gist = sub ? (r.summary_lines || [])[0] : null;
|
|
264
|
+
return el('li', { class: 'nrow cal3-nrow' + (on ? ' on' : '') + (sub ? ' cal3-nsub' : ''), 'data-cal3-note-row': r.id },
|
|
177
265
|
el('span', { class: 'ki' + (mode ? ' hot' : '') }, c3ic(mode ? 'mic' : 'doc')),
|
|
178
266
|
btn('cal3-nopen', { title: 'Open ' + (r.title || 'Untitled note'), 'aria-current': on ? 'true' : 'false' }, [
|
|
179
|
-
el('span', { class: 'tt' }, r.title || 'Untitled note'),
|
|
180
|
-
el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, [mode ? 'now' : whenShort(startOf(r)), len].filter(Boolean).join(' · ')),
|
|
267
|
+
el('span', { class: 'tt' }, gist || r.title || 'Untitled note'),
|
|
268
|
+
el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, [mode ? 'now' : sub ? dayOf(startOf(r)) : whenShort(startOf(r)), len].filter(Boolean).join(' · ')),
|
|
181
269
|
mode ? el('span', { class: 'tag', 'data-cal3-notes': 'live' }, mode === 'paused' ? 'paused' : 'recording')
|
|
182
|
-
: r.initiative ? el('span', { class: 'chip2', title: 'Initiative', 'data-cal3-notes': 'row-initiative' }, c3ic('init', 'sm'), iniName(r.initiative))
|
|
270
|
+
: sub ? null : r.initiative ? el('span', { class: 'chip2', title: 'Initiative', 'data-cal3-notes': 'row-initiative' }, c3ic('init', 'sm'), iniName(r.initiative))
|
|
183
271
|
: r.board ? el('span', { class: 'tag', title: 'Linked to the board, not to one initiative', 'data-cal3-notes': 'row-board' }, 'board') : null,
|
|
184
|
-
!linked(r) && !mode ? el('span', { class: 'tag', title: 'Not linked to a meeting', 'data-cal3-notes': 'no-meeting' }, 'no meeting') : null,
|
|
272
|
+
!linked(r) && !mode && !sub ? el('span', { class: 'tag', title: 'Not linked to a meeting', 'data-cal3-notes': 'no-meeting' }, 'no meeting') : null,
|
|
185
273
|
r.conflicts ? el('span', { class: 'st warn', title: r.conflicts + (r.conflicts === 1 ? ' statement differs' : ' statements differ') + ' from a file' }, c3ic('alert', 'sm'), String(r.conflicts)) : null)],
|
|
186
274
|
() => choose(s, r.id)),
|
|
187
275
|
el('span'));
|
|
@@ -206,8 +294,8 @@ function noteCol(s) {
|
|
|
206
294
|
[dayOf(startOf(n)), len].filter(Boolean).join(' · '),
|
|
207
295
|
btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n)),
|
|
208
296
|
btn('ib sm', { title: 'Delete this note', 'aria-label': 'Delete note', 'data-cal3-notes': 'delete' }, c3ic('trash'), () => remove(s, n))),
|
|
209
|
-
links(s, n, ev));
|
|
210
|
-
const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : after(s, n, mode);
|
|
297
|
+
links(s, n, ev), seriesLine(s, n));
|
|
298
|
+
const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : [sinceCard(s, n), after(s, n, mode)].filter(Boolean);
|
|
211
299
|
return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
|
|
212
300
|
}
|
|
213
301
|
|
|
@@ -309,12 +397,13 @@ function before(s, n) {
|
|
|
309
397
|
const ev = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
|
|
310
398
|
const h3 = el('h3', null, c3ic('spark'), 'Before the meeting');
|
|
311
399
|
if (!ev) {
|
|
312
|
-
return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3,
|
|
400
|
+
return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...carriedIn(s.place),
|
|
313
401
|
fold('info', n.meeting && n.meeting.kind === 'adhoc' ? 'A meeting that is not on the calendar, so nothing was prepared for it.' : 'Not linked to a meeting, so nothing was prepared. Link it to a meeting to bring in its people and agenda.', { 'data-cal3-notes': 'no-meeting-before' }),
|
|
314
402
|
linkButtons(s, n));
|
|
315
403
|
}
|
|
316
404
|
const c = ev.context || null;
|
|
317
405
|
const lines = transcriptOf(n).map((x) => ({ t: x.t, text: x.text }));
|
|
406
|
+
const pl = s.place && s.place.key ? s.place : null;
|
|
318
407
|
const heard = c ? agendaHeard(c, lines) : [];
|
|
319
408
|
const rows = [];
|
|
320
409
|
const isHeard = (i) => heard[i] !== null && heard[i] !== undefined;
|
|
@@ -331,7 +420,7 @@ function before(s, n) {
|
|
|
331
420
|
rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.where ? 'where: ' + c.where : null, c && c.organizer ? 'organizer: ' + c.organizer : null,
|
|
332
421
|
c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null, c && c.external ? 'with people outside the company' : null,
|
|
333
422
|
c && c.attendees && names.length ? c.attendees + ' attendees' : null].filter(Boolean).join(' · ')));
|
|
334
|
-
return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...rows,
|
|
423
|
+
return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...carriedIn(pl), ...rows,
|
|
335
424
|
c ? null : el('p', { class: 'dim cal3-sm' }, 'The calendar gave no more about this meeting.'));
|
|
336
425
|
}
|
|
337
426
|
|
|
@@ -392,7 +481,7 @@ function after(s, n, mode) {
|
|
|
392
481
|
if (p.actions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'actions' }, el('thead', null, el('tr', null, el('th', null, 'Next step'), el('th', null, 'Owner'), el('th', null, 'Due'), el('th'))),
|
|
393
482
|
el('tbody', null, p.actions.map((a) => el('tr', { 'data-cal3-notes': 'action' }, cell(a, 'text', 'Next step'),
|
|
394
483
|
cell(a, 'owner', 'Owner', 'No owner'), cell(a, 'due', 'Due', 'No date'),
|
|
395
|
-
el('td', { class: 'cal3-tdr' }, btn('chip2 lk', { 'data-cal3-notes': 'make-task', title: n.initiative ? 'Add this step to ' + iniName(n.initiative) + ' as a plan item' : 'Link the note to an initiative first' },
|
|
484
|
+
el('td', { class: 'cal3-tdr' }, carriedTag(s, a), btn('chip2 lk', { 'data-cal3-notes': 'make-task', title: n.initiative ? 'Add this step to ' + iniName(n.initiative) + ' as a plan item' : 'Link the note to an initiative first' },
|
|
396
485
|
[c3ic('plus', 'sm'), 'Make task'], (e) => makeTask(n, a, { btn: e.currentTarget }))))))));
|
|
397
486
|
if (p.questions.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'questions' }, el('h4', null, 'Open questions'),
|
|
398
487
|
el('ul', { class: 'cal3-cl', role: 'list' }, p.questions.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'tag' }, 'Question'), el('span', { class: 'cal3-ct' }, x.text))))));
|
|
@@ -465,6 +554,182 @@ async function remove(s, n) {
|
|
|
465
554
|
read(s);
|
|
466
555
|
}
|
|
467
556
|
|
|
557
|
+
/* ── a meeting series (initiative c3-notes-series) ────────────────────────────────────────────────────────────────── */
|
|
558
|
+
|
|
559
|
+
/* the words of an item, as lib/notes-series.js norm() matches them */
|
|
560
|
+
const normItem = (t) => String(t || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
|
|
561
|
+
const plural = (n, one, many) => n + ' ' + (n === 1 ? one : many || one + 's');
|
|
562
|
+
const sect = (name, ic, title, count, kids) => el('section', { class: 'card sect', 'data-cal3-series-card': name },
|
|
563
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic(ic, 'sm'), title, count !== null && count !== undefined ? el('span', { class: 'bdg g' }, String(count)) : null)), kids);
|
|
564
|
+
|
|
565
|
+
/* "carried · meeting N": the step was raised in N meetings of the series up to this one */
|
|
566
|
+
function carriedTag(s, a) {
|
|
567
|
+
const k = s.place && s.place.carried ? s.place.carried[normItem(a.text)] : 0;
|
|
568
|
+
return k ? el('span', { class: 'st ' + (k >= 3 ? 'warn' : 'unt'), title: 'Raised in ' + k + ' meetings of this series', 'data-cal3-notes': 'carried' }, 'carried · meeting ' + k) : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* Before: what was still open coming in from the meeting before */
|
|
572
|
+
function carriedIn(pl) {
|
|
573
|
+
if (!pl || !pl.prev) return [];
|
|
574
|
+
return [prepRow('notes', 'Carried in from ' + dayOf(pl.prev.at), pl.prev.overview || 'The meeting before in this series.'),
|
|
575
|
+
...(pl.carried_in || []).map((x) => prepRow('todo', x.text, x.owner || 'No owner'))];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* the head's series line: the meeting before, "IDPF - DSU · 4 of 21" (opens the series), the meeting after, and Take out;
|
|
579
|
+
* a note in no series offers Add to a series, and one taken out offers to go back */
|
|
580
|
+
function seriesLine(s, n) {
|
|
581
|
+
const pl = s.place;
|
|
582
|
+
if (pl && pl.key) {
|
|
583
|
+
const go = (x, ic, label) => { const b = btn('ib sm', { 'data-cal3-notes': 'series-' + label, title: x ? 'The meeting ' + (label === 'prev' ? 'before' : 'after') + ', ' + dayOf(x.at) : 'None ' + (label === 'prev' ? 'before' : 'after'), 'aria-label': label === 'prev' ? 'The meeting before' : 'The meeting after' }, c3ic(ic, 'sm'), () => { if (x) choose(s, x.id, s.phase); }); if (!x) b.disabled = true; return b; };
|
|
584
|
+
return el('div', { class: 'chips2 cal3-sm cal3-nserl', 'data-cal3-notes': 'series-line' }, go(pl.prev, 'prev', 'prev'),
|
|
585
|
+
btn('chip2 lk', { 'data-cal3-notes': 'series-open-page', title: 'Open the series: what is still open, decided and coming up' }, [c3ic('notes', 'sm'), pl.title + ' · ' + pl.pos + ' of ' + pl.count], () => chooseSeries(s, pl.key)),
|
|
586
|
+
go(pl.next, 'next', 'next'),
|
|
587
|
+
btn('chip2 lk', { 'data-cal3-notes': 'series-out', title: 'This note is not a meeting of this series' }, 'Not part of this series', () => member(s, n, 'off')));
|
|
588
|
+
}
|
|
589
|
+
if (n.series_pm === 'off') return el('div', { class: 'chips2 cal3-sm cal3-nserl', 'data-cal3-notes': 'series-line' }, c3ic('notes', 'sm'), el('span', { class: 'dim' }, 'Taken out of its series'),
|
|
590
|
+
btn('chip2 lk', { 'data-cal3-notes': 'series-back' }, 'Put it back', () => member(s, n, null)));
|
|
591
|
+
const size = new Map();
|
|
592
|
+
for (const r of s.rows || []) { const k = seriesKeyOf(r); if (k) size.set(k, (size.get(k) || [])); if (k) size.get(k).push(r); }
|
|
593
|
+
const series = [...size.entries()].filter(([, rs]) => rs.length >= 2 && !rs.some((r) => r.id === n.id));
|
|
594
|
+
if (!series.length || n.private) return null;
|
|
595
|
+
const b = btn('chip2 lk', { 'data-cal3-notes': 'series-add', title: 'Keep this note with the notes of a regular meeting' }, [c3ic('notes', 'sm'), 'Add to a series'], () => {
|
|
596
|
+
menu({ anchor: b, ariaLabel: 'Add to a series', items: series.map(([k, rs]) => {
|
|
597
|
+
const r = rs.slice().sort((x, y) => (Date.parse(startOf(y)) || 0) - (Date.parse(startOf(x)) || 0))[0];
|
|
598
|
+
const title = (r.meeting && r.meeting.kind === 'event' && r.meeting.title) || r.title || 'Untitled meeting';
|
|
599
|
+
return { label: title, detail: plural(rs.length, 'meeting'), iconNode: icon('notes'), attrs: { 'data-cal3-series-pick': k }, onSelect: () => member(s, n, k) };
|
|
600
|
+
}) });
|
|
601
|
+
});
|
|
602
|
+
return el('div', { class: 'chips2 cal3-sm cal3-nserl', 'data-cal3-notes': 'series-line' }, b);
|
|
603
|
+
}
|
|
604
|
+
async function member(s, n, key) {
|
|
605
|
+
const r = await pmPost('/api/notes/series-member', { id: n.id, key });
|
|
606
|
+
if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return; }
|
|
607
|
+
toast(key === 'off' ? 'Taken out of the series.' : key ? 'Added to the series.' : 'Back in its series.');
|
|
608
|
+
n.series_pm = key || undefined;
|
|
609
|
+
refreshNotes();
|
|
610
|
+
read(s);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* After: Since the last one. Each next step of the meeting before: Done (you marked it), Raised again (this note has it) or
|
|
614
|
+
* Not mentioned; then this note's new steps. */
|
|
615
|
+
function sinceCard(s, n) {
|
|
616
|
+
const pl = s.place;
|
|
617
|
+
if (!pl || !pl.since || n.private) return null;
|
|
618
|
+
const st = { done: ['ok', 'Done', 'check'], again: ['warn', 'Raised again', 'todo'], quiet: ['unt', 'Not mentioned', 'todo'] };
|
|
619
|
+
const rows = [
|
|
620
|
+
...pl.since.items.map((x) => el('li', { class: 'nrow', 'data-cal3-since': x.state }, el('span', { class: 'ki' }, c3ic(st[x.state][2])),
|
|
621
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, x.text), el('div', { class: 'chips' }, el('span', { class: 'st ' + st[x.state][0] }, st[x.state][1]), x.owner ? el('span', { class: 'dim cal3-xs' }, x.owner) : null)), el('span'))),
|
|
622
|
+
...pl.since.fresh.map((x) => el('li', { class: 'nrow', 'data-cal3-since': 'new' }, el('span', { class: 'ki' }, c3ic('plus')),
|
|
623
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, x.text), el('div', { class: 'chips' }, el('span', { class: 'tag' }, 'New'), x.owner ? el('span', { class: 'dim cal3-xs' }, x.owner) : null)), el('span'))),
|
|
624
|
+
];
|
|
625
|
+
return el('section', { class: 'card sect', 'data-cal3-notes': 'since' },
|
|
626
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('notes', 'sm'), 'Since the last one, ' + dayOf(pl.since.at))),
|
|
627
|
+
rows.length ? el('ul', { class: 'list', role: 'list' }, rows) : fold('info', 'The meeting before had no next steps, and this one has none.'));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* The series page: the head, Next one, Still open and Decided so far, Keeps coming up and Still unanswered, Every meeting.
|
|
631
|
+
* Every line comes from the series' notes (GET /api/notes/series) or from the PM's own marks and lines. */
|
|
632
|
+
function seriesCol(s) {
|
|
633
|
+
const T = s.thread;
|
|
634
|
+
if (!T) return el('div', { 'data-cal3-notes': 'series' }, el('section', { class: 'card' }, fold('clock', 'Reading the series', { role: 'status' })));
|
|
635
|
+
const a = T.about || {};
|
|
636
|
+
const about = a.initiative ? [boardName(a.board), iniName(a.initiative)].filter(Boolean).join(' › ') : a.board ? boardName(a.board) : a.portfolio ? 'Portfolio' : null;
|
|
637
|
+
const joined = (T.joined.calendar ? 'From the calendar series' : 'Notes with the same title')
|
|
638
|
+
+ (T.joined.calendar && T.joined.title ? ', plus ' + plural(T.joined.title, 'note') + ' with the same title' : '') + (T.joined.pm ? ', ' + T.joined.pm + ' added by you' : '') + '.';
|
|
639
|
+
const head = el('section', { class: 'card', 'data-cv': 'stage-head', 'data-cal3-notes': 'series-head' },
|
|
640
|
+
el('div', { class: 'cal3-nhead' }, el('span', { class: 'chips2' }, el('b', { 'data-cal3-notes': 'title' }, T.title), el('span', { class: 'tag' }, 'series')),
|
|
641
|
+
btn('ib sm', { title: 'Copy the series as Markdown', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copySeries(T))),
|
|
642
|
+
el('div', { class: 'chips2 dim cal3-sm' }, plural(T.count, 'meeting') + ' since ' + dayOf(T.since)),
|
|
643
|
+
el('div', { class: 'chips2 cal3-sm' }, about ? el('span', { class: 'chip2' }, c3ic(a.portfolio && !a.board ? 'org' : 'init', 'sm'), about) : null, el('span', { class: 'dim', 'data-cal3-notes': 'series-joined' }, joined)));
|
|
644
|
+
return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'series', 'data-cal3-series': T.key }, head, nextCard(s, T),
|
|
645
|
+
el('div', { class: 'cal3-two' }, openCard(s, T), decidedCard(s, T)),
|
|
646
|
+
el('div', { class: 'cal3-two' }, topicsCard(T), questionsCard(s, T)), everyCard(s, T));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function nextCard(s, T) {
|
|
650
|
+
const x = T.next || {};
|
|
651
|
+
const rows = [
|
|
652
|
+
...(x.bring || []).map((b) => el('div', { class: 'chips2 cal3-prep', 'data-cal3-bring': b.id }, el('span', { class: 'ki' }, c3ic('spark')),
|
|
653
|
+
el('div', { class: 'cal3-prepb' }, el('b', null, b.text), el('div', { class: 'dim' }, 'Added by you, ' + dayOf(b.at))),
|
|
654
|
+
btn('ib sm', { title: 'Remove this line', 'aria-label': 'Remove', 'data-cal3-notes': 'bring-remove' }, c3ic('x', 'sm'), () => seriesPost(s, '/api/notes/series-unbring', { key: T.key, id: b.id }, 'Removed.')))),
|
|
655
|
+
...(x.derived || []).map((d) => (d.kind === 'carried' ? prepRow('alert', d.text, 'Still open after ' + d.seen + ' meetings' + (d.owner ? ' · ' + d.owner : ''))
|
|
656
|
+
: prepRow('info', d.text, 'Unanswered in ' + d.seen + ' meetings'))),
|
|
657
|
+
];
|
|
658
|
+
const add = el('input', { type: 'text', class: 'cal3-nsearch', placeholder: 'Add something to bring up', 'aria-label': 'Add something to bring up', 'data-cal3-notes': 'bring-input', maxlength: '300' });
|
|
659
|
+
add.addEventListener('keydown', (e) => { if (e.key === 'Enter' && add.value.trim()) { e.preventDefault(); seriesPost(s, '/api/notes/series-bring', { key: T.key, text: add.value.trim() }, 'Added. It shows here until you remove it.'); } });
|
|
660
|
+
const when = x.at ? dayOf(x.at) + ' ' + hm(x.at) : null;
|
|
661
|
+
return el('section', { class: 'card sect', 'data-cal3-series-card': 'next' },
|
|
662
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), when ? 'Next one, ' + when : 'Next one'), when ? null : el('span', { class: 'dim cal3-sm' }, 'not on the calendar yet')),
|
|
663
|
+
rows.length ? rows : fold('info', 'Nothing to bring up yet.', { 'data-cal3-notes': 'bring-none' }),
|
|
664
|
+
el('div', { class: 'chips2 cal3-nadd' }, add,
|
|
665
|
+
x.id ? btn('chip2 lk', { 'data-cal3-notes': 'series-meeting', title: 'Show the next meeting in the calendar' }, [c3ic('cal', 'sm'), 'Open the meeting'], () => showMeeting(s, { start: x.at, event_id: x.id })) : null));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function openCard(s, T) {
|
|
669
|
+
const rows = T.open.map((o) => el('li', { class: 'nrow', 'data-cal3-open': o.key }, el('span', { class: 'ki' }, c3ic('todo')),
|
|
670
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, o.text),
|
|
671
|
+
el('div', { class: 'chips' }, o.owner ? el('span', { class: 'chip2' }, c3ic('user', 'sm'), o.owner) : null, el('span', { class: 'dim mono cal3-xs' }, 'since ' + dayOf(o.first)),
|
|
672
|
+
el('span', { class: 'st ' + (o.seen >= 3 ? 'warn' : 'unt'), title: 'Raised in each of these meetings', 'data-cal3-notes': 'seen' }, o.seen >= 3 ? c3ic('alert', 'sm') : null, 'in ' + plural(o.seen, 'meeting'))),
|
|
673
|
+
el('div', { class: 'dim cal3-sm' }, o.last.quote ? 'Last said: “' + o.last.quote + '” ' + dayOf(o.last.at) : 'Last raised ' + dayOf(o.last.at))),
|
|
674
|
+
btn('chip2 lk', { 'data-cal3-notes': 'series-done', title: 'Mark it done: it stops carrying over' }, [c3ic('check', 'sm'), 'Done'], () => seriesPost(s, '/api/notes/series-done', { key: T.key, text: o.text, kind: 'action' }, 'Done. It stops carrying over.'))));
|
|
675
|
+
const closed = T.closed.length ? el('details', { class: 'cal3-ctx', 'data-cal3-notes': 'closed' }, el('summary', null, el('b', null, 'Closed in the last two weeks'), el('span', { class: 'dim' }, ' · ' + T.closed.length)),
|
|
676
|
+
el('ul', { class: 'list', role: 'list' }, T.closed.map((c) => el('li', { class: 'nrow' }, el('span', { class: 'ki' }, c3ic('check')),
|
|
677
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, c.text), el('div', { class: 'chips' }, el('span', { class: 'dim cal3-xs' }, 'marked done ' + dayOf(c.at)))),
|
|
678
|
+
btn('chip2 lk', { 'data-cal3-notes': 'series-undone', title: 'Open it again' }, 'Open again', () => seriesPost(s, '/api/notes/series-done', { key: T.key, text: c.text, kind: 'action', done: false }, 'Open again.')))))) : null;
|
|
679
|
+
return sect('open', 'todo', 'Still open', T.open.length, [rows.length ? el('ul', { class: 'list', role: 'list' }, rows) : fold('info', 'Nothing is carried over.'), closed]);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function decidedCard(s, T) {
|
|
683
|
+
return sect('decisions', 'scale', 'Decided so far', T.decisions.length, T.decisions.length ? el('ul', { class: 'list', role: 'list' }, T.decisions.map((d) => el('li', { class: 'nrow cal3-nrow' },
|
|
684
|
+
el('span', { class: 'ki' }, c3ic('scale')),
|
|
685
|
+
btn('cal3-nopen', { title: 'Open the note of ' + dayOf(d.at) }, [el('span', { class: 'tt' }, d.text), el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, dayOf(d.at)), d.why ? el('span', { class: 'dim cal3-xs' }, d.why) : null)], () => choose(s, d.note)),
|
|
686
|
+
el('span')))) : fold('info', 'No decisions yet.'));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function topicsCard(T) {
|
|
690
|
+
if (!T.topics.length) return null;
|
|
691
|
+
return sect('topics', 'spark', 'Keeps coming up', null, el('ul', { class: 'list', role: 'list' }, T.topics.map((t) => el('li', { class: 'nrow' }, el('span', { class: 'ki' }, c3ic('spark')),
|
|
692
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, t.name), el('div', { class: 'chips' },
|
|
693
|
+
el('span', { class: 'cal3-dots', title: 'The last ' + t.hits.length + ' meetings, newest on the left' }, t.hits.map((h) => el('i', { class: h ? 'cal3-dot on' : 'cal3-dot' }))),
|
|
694
|
+
el('span', { class: 'dim mono cal3-xs' }, t.count + ' of last ' + t.hits.length), t.kind === 'agenda' ? el('span', { class: 'tag' }, 'agenda') : null)), el('span')))));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function questionsCard(s, T) {
|
|
698
|
+
if (!T.questions.length) return null;
|
|
699
|
+
return sect('questions', 'info', 'Still unanswered', T.questions.length, el('ul', { class: 'list', role: 'list' }, T.questions.map((q) => el('li', { class: 'nrow' }, el('span', { class: 'ki' }, c3ic('info')),
|
|
700
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, q.text), el('div', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, 'asked ' + dayOf(q.first)),
|
|
701
|
+
el('span', { class: 'st unt' }, q.seen > 1 ? 'asked in ' + q.seen + ' meetings' : 'not answered yet'))),
|
|
702
|
+
btn('chip2 lk', { 'data-cal3-notes': 'series-answered', title: 'Mark it answered' }, [c3ic('check', 'sm'), 'Answered'], () => seriesPost(s, '/api/notes/series-done', { key: T.key, text: q.text, kind: 'question' }, 'Marked answered.'))))));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function everyCard(s, T) {
|
|
706
|
+
const show = s.every ? T.meetings : T.meetings.slice(0, 6);
|
|
707
|
+
const rows = show.map((m) => el('li', { class: 'nrow cal3-nrow', 'data-cal3-meeting': m.id }, el('span', { class: 'ki' }, c3ic(m.private ? 'lock' : 'doc')),
|
|
708
|
+
btn('cal3-nopen', { title: 'Open the note of ' + dayOf(m.at) }, [el('span', { class: 'tt' }, m.private ? 'Private note' : m.overview || m.title),
|
|
709
|
+
el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, [dayOf(m.at), mins(m.ms)].filter(Boolean).join(' · ')),
|
|
710
|
+
m.decisions ? el('span', { class: 'tag' }, '+' + m.decisions + ' decided') : null, m.new_actions ? el('span', { class: 'tag' }, '+' + plural(m.new_actions, 'new step')) : null,
|
|
711
|
+
m.by === 'title' ? el('span', { class: 'tag', title: 'In the series because the title matches' }, 'by title') : m.by === 'pm' ? el('span', { class: 'tag' }, 'added by you') : null)],
|
|
712
|
+
() => choose(s, m.id)), el('span')));
|
|
713
|
+
const more = T.meetings.length - show.length;
|
|
714
|
+
return sect('every', 'notes', 'Every meeting', T.meetings.length, [el('ul', { class: 'list', role: 'list' }, rows),
|
|
715
|
+
more > 0 ? el('div', { class: 'cal3-nadd' }, btn('chip2 lk', { 'data-cal3-notes': 'series-every' }, 'Show ' + plural(more, 'earlier meeting'), () => { s.every = true; draw(s); })) : null]);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function seriesPost(s, url, body, said) {
|
|
719
|
+
const r = await pmPost(url, body);
|
|
720
|
+
if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return; }
|
|
721
|
+
toast(said);
|
|
722
|
+
read(s);
|
|
723
|
+
}
|
|
724
|
+
async function copySeries(T) {
|
|
725
|
+
const md = ['# ' + T.title, '', plural(T.count, 'meeting') + ' since ' + dayOf(T.since) + '.', ''];
|
|
726
|
+
if (T.open.length) md.push('## Still open', ...T.open.map((o) => '- ' + o.text + (o.owner ? ' (' + o.owner + ')' : '') + ', in ' + plural(o.seen, 'meeting')), '');
|
|
727
|
+
if (T.decisions.length) md.push('## Decided so far', ...T.decisions.map((d) => '- ' + dayOf(d.at) + ': ' + d.text), '');
|
|
728
|
+
if (T.questions.length) md.push('## Still unanswered', ...T.questions.map((q) => '- ' + q.text), '');
|
|
729
|
+
md.push('## Every meeting', ...T.meetings.map((m) => '- ' + dayOf(m.at) + ': ' + (m.private ? 'Private note' : m.overview || m.title)));
|
|
730
|
+
try { await navigator.clipboard.writeText(md.join('\n')); toast('The series is on the clipboard as Markdown.'); } catch { toast('The clipboard was not available.'); }
|
|
731
|
+
}
|
|
732
|
+
|
|
468
733
|
/* "Show in the calendar": the Calendar switch back to the grid, on the meeting's day, with the meeting selected */
|
|
469
734
|
function showMeeting(s, ev) {
|
|
470
735
|
const ui = s.ui; if (!ui) return;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - the portfolio: the headline joins the breadcrumb, an empty triage card is not shown, the timeline folds;
|
|
9
9
|
* - empty chips ("No owner", "0 epics", ...) are not drawn;
|
|
10
10
|
* - Calendar: the layer chips sit behind one Layers button, the Detail card shows once a meeting is picked;
|
|
11
|
-
* - Work: the lenses sit beside Add work; Reports:
|
|
11
|
+
* - Work: the lenses sit beside Add work; Reports: Send this as a report and Edit dashboard join the tab row.
|
|
12
12
|
* Desktop only: below 601px the page keeps today's layout.
|
|
13
13
|
* The views draw their own markup; this module only moves and hides it, so every press still goes to the view's own handler.
|
|
14
14
|
* It runs again, debounced, whenever the views redraw (one MutationObserver on the root). */
|
|
@@ -144,7 +144,7 @@ function apply(root) {
|
|
|
144
144
|
const add = w.querySelector(':scope > .c3add'), bar = w.querySelector(':scope > .calbar'), l = bar && bar.querySelector('[data-pm-lenses]');
|
|
145
145
|
if (add && l && l.parentElement === bar) { for (const o of add.querySelectorAll('[data-pm-lenses]')) o.remove(); add.appendChild(l); bar.classList.add('qt-moved'); }
|
|
146
146
|
}
|
|
147
|
-
// Reports:
|
|
147
|
+
// Reports: Send this as a report and Edit dashboard on the tab row.
|
|
148
148
|
for (const r of root.querySelectorAll('[data-rp3]')) {
|
|
149
149
|
const scope = r.querySelector(':scope > .calbar .rp3-scope'), acts = r.querySelector('.rp3-head > .chips2');
|
|
150
150
|
if (scope && acts) { for (const o of scope.querySelectorAll('.qt-acts')) o.remove(); acts.classList.add('qt-acts'); scope.appendChild(acts); }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* The printable report (Download > PDF): its own window, printed by the browser. Colours are the slide tokens of
|
|
2
|
+
tokens.css (linked first), so the paper reads the same in either theme. Slides are 16:9 landscape cards (.rp-slide),
|
|
3
|
+
everything else is A4. */
|
|
4
|
+
@page { size: A4; margin: 16mm; }
|
|
5
|
+
.rp-print { margin: 0; font-family: var(--sans); font-size: 14px; line-height: 1.5; color: var(--slide-fg); background: var(--slide-bg); }
|
|
6
|
+
.rp-print h1 { font-size: 20px; margin: 0 0 4mm; }
|
|
7
|
+
.rp-print h2 { font-size: 16px; margin: 6mm 0 2mm; color: var(--slide-brand); }
|
|
8
|
+
.rp-print p, .rp-print ul { margin: 0 0 3mm; }
|
|
9
|
+
.rp-print table { border-collapse: collapse; width: 100%; margin: 0 0 3mm; }
|
|
10
|
+
.rp-print th, .rp-print td { border: 1px solid var(--slide-border); padding: 2mm 3mm; text-align: left; font-size: 12px; }
|
|
11
|
+
.rp-print th { background: var(--slide-secondary); }
|
|
12
|
+
.rp-print .rp-print-m { color: var(--slide-muted); }
|
|
13
|
+
.rp-print-slides { margin: 0; }
|
|
14
|
+
@page slides { size: 297mm 167mm; margin: 0; }
|
|
15
|
+
.rp-slide { page: slides; page-break-after: always; width: 297mm; height: 167mm; box-sizing: border-box; padding: 14mm; font-size: 20px; }
|
|
16
|
+
.rp-slide h2 { margin-top: 0; font-size: 24px; }
|
|
17
|
+
.rp-slide li { margin-bottom: 2mm; }
|
|
18
|
+
.rp-slide table { font-size: 16px; }
|
|
19
|
+
.rp-slide--title { background: var(--slide-brand); color: var(--slide-bg); }
|
|
20
|
+
.rp-slide--title h2 { color: var(--slide-bg); }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* pm/c3-report-setup.css — the Reports tab (Ask PMOS, Found in your work, Set up, Documents), the "Send this as a report" sheet, the Briefing card, the Settings
|
|
2
|
+
* block and the report formats of the document sheet (initiative report-workspace). The mockup's 41-reportsetup.css under real class names: .mk-rs is .rs,
|
|
3
|
+
* .mk-rs-* is .rs-*, .mk-kv is .rs-kv. Tokens only. Corners: 6px on a control or a card drawn as paper, 8px on a container, else 0.
|
|
4
|
+
* c3-report-setup.js and c3-doc-sheet.js link it once. The slide colours are the --slide-* tokens of tokens.css. */
|
|
5
|
+
/* in .brief a .nrow has two columns, so the row buttons sit under the text */
|
|
6
|
+
.c3 .rs .nrow > .acts { grid-column: 2; justify-content: flex-start; display: flex; gap: 6px; flex-wrap: wrap; }
|
|
7
|
+
.c3 .rs, .c3 .rs > * { min-width: 0; }
|
|
8
|
+
.c3 .rs .nrow .chips .chip2 { max-width: 100%; }
|
|
9
|
+
.c3 .rs .form { margin: 0; }
|
|
10
|
+
.c3 .rs .seg { max-width: 100%; overflow-x: auto; }
|
|
11
|
+
.c3 .rs .seg button { white-space: nowrap; }
|
|
12
|
+
.c3 [data-rs-finder] .nrow > .acts { grid-column: 2; justify-content: flex-start; display: flex; gap: 6px; flex-wrap: wrap; }
|
|
13
|
+
.c3 [data-rs-finder] .seg { max-width: 100%; overflow-x: auto; }
|
|
14
|
+
.c3 .rs .lh { grid-template-columns: auto minmax(0, 1fr); }
|
|
15
|
+
.c3 .rs .lh > .chips2 { grid-column: 1 / -1; }
|
|
16
|
+
.c3 .rs-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; font-size: var(--fs-2); }
|
|
17
|
+
.c3 .rs-kv dt { font: var(--fs-1) var(--mono); color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .06em; padding-top: 2px; }
|
|
18
|
+
.c3 .rs-kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
19
|
+
.c3 .rs-kv input[type="text"] { width: 100%; box-sizing: border-box; }
|
|
20
|
+
.c3 .rs-kv ol { margin: 0; padding-left: 18px; }
|
|
21
|
+
.c3 .rs-lbl { font: var(--fs-1) var(--mono); color: var(--muted-foreground); }
|
|
22
|
+
.c3 .rs-help { font-size: var(--fs-2); }
|
|
23
|
+
.c3 .rs-stack { display: grid; gap: var(--sp-2); }
|
|
24
|
+
.c3 .rs-tools { display: inline-flex; gap: 6px; align-items: center; }
|
|
25
|
+
.c3 .rs-menu { align-items: center; }
|
|
26
|
+
@media (max-width: 560px) { .c3 .rs-kv { grid-template-columns: minmax(0, 1fr); } }
|
|
27
|
+
|
|
28
|
+
/* Output formats: the document drawn as slides, a grid, a doc page or A4 pages. Paper colours are the slide tokens. */
|
|
29
|
+
.c3 .rs-slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: var(--sp-3); }
|
|
30
|
+
.c3 .rs-slides > .ds-sec { margin-left: 0; }
|
|
31
|
+
.c3 .rs-slide { aspect-ratio: 16 / 9; min-width: 0; display: grid; grid-template-rows: auto 1fr auto; gap: var(--sp-2); padding: var(--sp-3); background: var(--slide-bg); color: var(--slide-fg); border: 1px solid var(--slide-border); border-radius: 6px; font-size: var(--fs-2); overflow: hidden; }
|
|
32
|
+
.c3 .rs-sh { font-weight: 600; font-size: var(--fs-3); color: var(--slide-brand); overflow-wrap: anywhere; }
|
|
33
|
+
.c3 .rs-slide.title { background: var(--slide-brand); color: var(--slide-bg); }
|
|
34
|
+
.c3 .rs-slide.title .rs-sh { color: var(--slide-bg); font-size: var(--fs-4); }
|
|
35
|
+
.c3 .rs-slide .m, .c3 .rs-page .m { color: var(--slide-muted); margin: 0; }
|
|
36
|
+
.c3 .rs-slide.title .m { color: var(--slide-border); }
|
|
37
|
+
.c3 .rs-slide p, .c3 .rs-slide .ds-md { margin: 0; font-size: var(--fs-2); color: inherit; }
|
|
38
|
+
.c3 .rs-slide ul, .c3 .rs-page ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; }
|
|
39
|
+
.c3 .rs-sf { display: flex; justify-content: space-between; font: var(--fs-1) var(--mono); color: var(--slide-foot); }
|
|
40
|
+
.c3 .rs-big { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
|
|
41
|
+
.c3 .rs-big b { display: block; font-size: var(--fs-4); color: var(--slide-primary); }
|
|
42
|
+
.c3 .rs-big span { color: var(--slide-muted); font-size: var(--fs-1); }
|
|
43
|
+
.c3 .rs-sheets { display: grid; gap: var(--sp-3); }
|
|
44
|
+
.c3 .rs-gridwrap { max-width: 100%; overflow-x: auto; border: 1px solid var(--slide-border); border-radius: 6px; background: var(--slide-bg); }
|
|
45
|
+
.c3 .rs-grid { border-collapse: collapse; min-width: 640px; width: 100%; color: var(--slide-fg); font-size: var(--fs-2); }
|
|
46
|
+
.c3 .rs-grid th, .c3 .rs-grid td { border: 1px solid var(--slide-border); padding: 4px 8px; text-align: left; }
|
|
47
|
+
.c3 .rs-grid thead th, .c3 .rs-grid tbody th { background: var(--slide-secondary); color: var(--slide-muted); font: var(--fs-1) var(--mono); text-align: center; }
|
|
48
|
+
.c3 .rs-grid tr.hd td { font-weight: 600; background: var(--slide-track); }
|
|
49
|
+
.c3 .rs-grid td.n { text-align: right; font-family: var(--mono); }
|
|
50
|
+
.c3 .rs-grid td.pos { color: var(--slide-done); }
|
|
51
|
+
.c3 .rs-grid td.neg { color: var(--slide-overdue); }
|
|
52
|
+
.c3 .rs-page { background: var(--slide-bg); color: var(--slide-fg); border: 1px solid var(--slide-border); border-radius: 6px; padding: var(--sp-4); display: grid; gap: var(--sp-2); align-content: start; min-width: 0; font-size: var(--fs-3); }
|
|
53
|
+
.c3 .rs-page h3 { margin: 0; font-size: var(--fs-5); }
|
|
54
|
+
.c3 .rs-page p { margin: 0; }
|
|
55
|
+
.c3 .rs-page .ds-md { color: inherit; }
|
|
56
|
+
.c3 .rs-page .ds-sec-h h3 { color: var(--slide-brand); }
|
|
57
|
+
.c3 .rs-pages { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
|
|
58
|
+
.c3 .rs-page.a4 { aspect-ratio: 1 / 1.414; grid-template-rows: auto; align-content: start; position: relative; }
|
|
59
|
+
.c3 .rs-pn { position: absolute; bottom: var(--sp-2); right: var(--sp-3); font: var(--fs-1) var(--mono); color: var(--slide-foot); }
|
|
60
|
+
|
|
61
|
+
.c3 .rs-min0 { min-width: 0; }
|
|
62
|
+
.c3 .rs-kv .chip2 + .rs-help { margin-left: 6px; }
|