create-pmos 0.130.0 → 0.132.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/data-freshness.md +40 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/my-work.md +76 -1
- package/workspace/lib/calendar-cli.js +84 -0
- package/workspace/lib/calendar-layers.js +372 -0
- package/workspace/lib/facts-async.js +144 -35
- package/workspace/lib/freshness.js +167 -0
- package/workspace/lib/offloop.js +94 -0
- package/workspace/lib/pm-items.js +71 -6
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/report-needs.js +1 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-board.js +3 -1
- package/workspace/lib/routes-calendar-views.js +112 -3
- package/workspace/lib/routes-my-work.js +27 -9
- package/workspace/lib/routes-pm-items.js +123 -13
- package/workspace/lib/routes-sources.js +64 -0
- 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 +5 -2
- package/workspace/public/pm/calendar-views.js +10 -3
- package/workspace/public/pm/components.css +34 -0
- package/workspace/public/pm/console3.css +155 -0
- package/workspace/public/pm/dest-settings.js +10 -1
- package/workspace/public/pm/freshness-line.js +123 -0
- package/workspace/public/pm/pm-work-view.js +364 -172
- package/workspace/public/pm/work-place.js +58 -0
- package/workspace/server.js +39 -2
- package/workspace/skills/calendar-sync.skill +30 -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/test-lists/workspace-data-freshness.json +6 -0
- package/workspace/public/pm/c3-demo-work2.js +0 -13
|
@@ -5,26 +5,34 @@
|
|
|
5
5
|
* (see pm/console3-lift.md). This file keeps the real data calls and draws what they return, nothing else.
|
|
6
6
|
*
|
|
7
7
|
* mountPmWork(host, ctx) draws once per host; disposePmWork(host) removes it.
|
|
8
|
+
* Views (initiative console3-work-page, planning/prd/console3-work-page-prd.md): Inbox, Today, Upcoming, Waiting, Someday and Review.
|
|
9
|
+
* The SERVER places every item and answers `work`, `review`, `focus` and `counts`; this file draws them and never copies an item from
|
|
10
|
+
* one view to another. Today opens with Focus (up to three jobs, each with the reason: Accept, Edit title, Not today). Upcoming groups
|
|
11
|
+
* the next 7 days by day, then Later. Review is a weekly pass, one item at a time (Keep, Change, Drop). An item done outside PMOS
|
|
12
|
+
* (`kind: offline`) shows the person and offers Done with an optional outcome, Not yet and Drop, and "Looks done?" with Confirm when
|
|
13
|
+
* it carries `evidence`; PMOS never closes it by itself.
|
|
8
14
|
* Routes (lib/routes-pm-items.js). Every write is a page press through pmPost:
|
|
9
|
-
* GET /api/pm-items POST /api/pm-items/{add,edit,remove,undo,done,reopen,
|
|
10
|
-
* POST /api/pm-jobs/{add,edit,delete}
|
|
15
|
+
* GET /api/pm-items POST /api/pm-items/{add,edit,remove,undo,done,reopen,unpark,flag,defer,someday,waiting,notyet,keep,steps/add,steps/who,steps/plan}
|
|
16
|
+
* POST /api/pm-items/focus/{accept,dismiss,clear} POST /api/pm-jobs/{add,edit,delete}
|
|
11
17
|
* A row opens its CASE FILE in place (c3-expand.js + c3-case.js: the job sentence, evidence, options with a recommendation, a slide to
|
|
12
|
-
* confirm); the editable fields sit inside it behind "Edit details".
|
|
13
|
-
* action of its
|
|
14
|
-
* arrows do the swipes. Every action offers Undo through the reverse route (reopen,
|
|
15
|
-
* would send something to a person is an outward option: its slide is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* microphone is drawn disabled.
|
|
18
|
+
* confirm), then the shared actions (Flag, Not before, Someday, Waiting, Drop); the editable fields sit inside it behind "Edit details".
|
|
19
|
+
* A row swipes (c3-swipe.js) and takes keys: right = the right-hand action of its view (done, in Today, ...), left = the left-hand one
|
|
20
|
+
* (tomorrow, Someday, ...); j and k move, and the arrows do the swipes. Every action offers Undo through the reverse route (reopen,
|
|
21
|
+
* unpark, flag back, defer back, waiting back, undo). Anything that would send something to a person is an outward option: its slide is
|
|
22
|
+
* drawn disabled ("no channel connected"); nothing is sent.
|
|
23
|
+
* In demo mode (/c3.html?demo=1, window.__c3demo) the answers of the writes are laid over the fixtures in memory (`applyOv`) and placed
|
|
24
|
+
* with work-place.js, the server's rule; the agent run of a row with a suggested move is a mock that only walks the steps and shows a
|
|
25
|
+
* labelled draft. The deck is ctx.plan, a node the page builds. The microphone is drawn disabled.
|
|
20
26
|
*
|
|
21
27
|
* The helpers c3ic, c3ring, ensureC3Css and c3Phone are shared with briefing-deck.js and calendar-views.js. */
|
|
22
28
|
import { el } from './dom.js';
|
|
29
|
+
import { button } from './primitives.js';
|
|
23
30
|
import { pmGet, pmPost } from './http.js';
|
|
24
31
|
import { renderCase } from './c3-case.js';
|
|
25
32
|
import { makeXp, attachExpand } from './c3-expand.js';
|
|
26
33
|
import { attachSwipe } from './c3-swipe.js';
|
|
27
34
|
import { toast } from './c3-toast.js';
|
|
35
|
+
import { VIEWS, REVIEW_DAYS, workPlace, arrives, buildWork, addIso, daysBetween, untouchedDays } from './work-place.js';
|
|
28
36
|
|
|
29
37
|
/* ── shared with the Briefing and Calendar views: the stylesheet, the icon sprite, the ring, the phone class ───────── */
|
|
30
38
|
|
|
@@ -96,11 +104,12 @@ export function c3ib(label, icon, attrs, cls) {
|
|
|
96
104
|
|
|
97
105
|
const API = '/api/pm-items';
|
|
98
106
|
const JOBS = '/api/pm-jobs';
|
|
99
|
-
|
|
100
|
-
const
|
|
107
|
+
/* The six views (initiative console3-work-page, PRD R1): five places an open item can be, and the weekly Review pass. */
|
|
108
|
+
const LENSES = [['inbox', 'Inbox'], ['today', 'Today'], ['upcoming', 'Upcoming'], ['waiting', 'Waiting'], ['someday', 'Someday'], ['review', 'Review']];
|
|
109
|
+
const WLI = { inbox: 'inbox', today: 'sun', upcoming: 'cal', waiting: 'wait', someday: 'moon', review: 'eye' };
|
|
101
110
|
const WHO = ['you', 'agent-drafts', 'agent-does'];
|
|
102
111
|
const OUTWARD = /\b(send|sends|sending|email|emails|message|messages|tell|post|reply to)\b/i; // mirrors lib/item-steps.js
|
|
103
|
-
const
|
|
112
|
+
const RESOLVES_TIP = 'Your work. The team task is separate: close it in its initiative.';
|
|
104
113
|
|
|
105
114
|
export const COPY = Object.freeze({
|
|
106
115
|
addPlaceholder: 'Add work: type or speak',
|
|
@@ -110,23 +119,48 @@ export const COPY = Object.freeze({
|
|
|
110
119
|
addJob: 'Add a job', addJobPlaceholder: 'when I plan a release, I want honest dates, so Dana can trust them',
|
|
111
120
|
groupJob: 'Group by job to be done', groupTime: 'Group by time', loading: 'Reading…',
|
|
112
121
|
micOff: 'Speak is off on this line: on-device dictation runs in Notes, in the main app. Type the item here.',
|
|
113
|
-
jobsNote: 'The count is every item in the job, across all
|
|
122
|
+
jobsNote: 'The count is every item in the job, across all views. Tap a job to edit or delete it.',
|
|
114
123
|
legend: ['open', 'done', 'migrated', 'scheduled'], readError: 'Your items could not be read.', saveError: 'Nothing was saved.',
|
|
115
|
-
removed: 'Removed.', undo: 'Undo', restored: 'Restored.', tooLate: 'Too late to undo.', drop: 'Drop
|
|
124
|
+
removed: 'Removed.', undo: 'Undo', restored: 'Restored.', tooLate: 'Too late to undo.', drop: 'Drop',
|
|
116
125
|
reassigned: (n) => (n === 1 ? '1 item now has no job.' : `${n} items now have no job.`),
|
|
117
126
|
progress: (d, t) => `${d} of ${t} done`,
|
|
118
127
|
jobLine: (j) => `When ${j.when}, I want ${j.want}, so ${j.so}.`,
|
|
119
128
|
lens: {
|
|
120
|
-
|
|
121
|
-
waiting: (n) => `${n} waiting on others`,
|
|
129
|
+
inbox: (n) => `${n} to decide`, today: (n) => `${n} for today`, upcoming: (n) => `${n} in the days ahead`,
|
|
130
|
+
waiting: (n) => `${n} waiting on others`, someday: (n) => `${n} parked in Someday`, review: (n) => `${n} to review this week`,
|
|
122
131
|
},
|
|
123
132
|
lensSub: {
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
inbox: 'Captured, not decided. Give each one a day, a Not before date, a person to wait on, or park it in Someday.',
|
|
134
|
+
today: 'Overdue, due today and flagged. Focus says which jobs matter most today.',
|
|
135
|
+
upcoming: 'The next 7 days by day, then Later. A Not before date keeps an item out of Today until that day.',
|
|
136
|
+
waiting: 'Someone else must act first.',
|
|
137
|
+
someday: 'Parked on purpose. Nothing here asks for attention until the weekly Review.',
|
|
138
|
+
review: 'Items in Inbox, Waiting or Someday that nobody touched for 7 days, most stale first. Keep, change or drop each one.',
|
|
126
139
|
},
|
|
127
|
-
|
|
140
|
+
emptyView: {
|
|
141
|
+
inbox: 'Inbox is empty. Capture anything that crosses your mind.', today: 'Nothing due, overdue or flagged. Pick from Upcoming when you are ready.',
|
|
142
|
+
upcoming: 'Nothing dated in the days ahead.', waiting: 'Nobody owes you anything right now.', someday: 'Nothing parked.',
|
|
143
|
+
},
|
|
144
|
+
done: 'Done', doneToast: 'Marked done.', steps: 'Steps', noSteps: 'No steps yet.', addStep: 'Add a step',
|
|
128
145
|
who: { you: 'You', 'agent-drafts': 'Agent drafts', 'agent-does': 'Agent does' },
|
|
129
146
|
whoOutward: 'Sends to a person: the agent drafts, you slide to send.', options: 'Options', plan: 'Plan the steps',
|
|
147
|
+
/* Focus (PRD R4) */
|
|
148
|
+
focus: 'Focus', focusSub: 'Up to three jobs for today, each with the reason.', focusNone: 'No focus suggested for today. Everything below still counts.',
|
|
149
|
+
focusPinned: 'Focus today', focusSuggested: 'Suggested', accept: 'Accept', editTitle: 'Edit title', notToday: 'Not today', unpin: 'Unpin', save: 'Save', cancel: 'Cancel',
|
|
150
|
+
why: (w) => 'Why: ' + w, focusAccepted: 'Pinned to Today.', focusDismissed: 'Not today. It does not come back today.', focusCleared: 'Unpinned.',
|
|
151
|
+
/* offline work and the shared actions (PRD R6) */
|
|
152
|
+
offline: 'outside PMOS', offlineNote: (p) => 'Happens outside PMOS' + (p ? ', with ' + p : '') + '. Tell PMOS how it went.',
|
|
153
|
+
outcome: 'What came out of it (optional)', notYet: 'Not yet:', laterToday: 'Later today', tomorrow: 'Tomorrow', nextWeek: 'Next week',
|
|
154
|
+
looksDone: 'Looks done?', confirm: 'Confirm done', flag: 'Flag', unflag: 'Unflag', notBefore: 'Not before', someday: 'Someday', bringBack: 'Bring back',
|
|
155
|
+
waitingField: 'Waiting on', waitingWho: 'Who?', stopWaiting: 'Stop waiting', flagged: 'Flagged',
|
|
156
|
+
notBeforeChip: (d) => 'Not before ' + d, advances: (t) => 'Advances: ' + t, moved: (n) => (n === 1 ? 'Moved once' : `Moved ${n} times`),
|
|
157
|
+
overdue: (n) => (n === 1 ? '1 day overdue' : `${n} days overdue`),
|
|
158
|
+
/* Review (PRD R7) */
|
|
159
|
+
reviewHead: 'Weekly review', keep: 'Keep', change: 'Change', next: 'Done changing, next', reviewDone: 'Review done',
|
|
160
|
+
reviewDoneText: (n) => `You looked at ${n} item${n === 1 ? '' : 's'}. Anything you changed or dropped has moved to its new view.`,
|
|
161
|
+
reviewNone: 'Nothing waits for review. Every item was touched in the last 7 days.', again: 'Start another pass',
|
|
162
|
+
untouched: (n) => `Untouched for ${n} days`, livesIn: (v) => 'Lives in ' + v, upNext: 'Up next: ',
|
|
163
|
+
kept: 'Kept. It comes back if nobody touches it for another week.',
|
|
130
164
|
});
|
|
131
165
|
const insts = new WeakMap();
|
|
132
166
|
|
|
@@ -135,37 +169,37 @@ const say = toast;
|
|
|
135
169
|
|
|
136
170
|
/* ── the demo overlay ────────────────────────────────────────────────────────────────────────────────────────────── *
|
|
137
171
|
* In /c3.html?demo=1 every write is answered { ok: true } and the fixtures never change, so the page keeps what the PM did in
|
|
138
|
-
* memory (`inst.ov`) and lays it over each read
|
|
172
|
+
* memory (`inst.ov`) and lays it over each read, then places the items with the server's rule (work-place.js). Real mode has no
|
|
173
|
+
* overlay: the server places every item and every press re-reads. */
|
|
139
174
|
const DEMO = () => typeof window !== 'undefined' && !!window.__c3demo;
|
|
140
|
-
const newOv = () => ({ done: new Set(), gone: new Set(),
|
|
141
|
-
const LENS_IDS = LENSES.map((l) => l[0]);
|
|
175
|
+
const newOv = () => ({ done: new Set(), gone: new Set(), edits: new Map(), added: [], steps: new Map(), who: new Map(), focus: new Map(), jobsAdded: [], jobEdits: new Map(), jobsGone: new Map(), seq: 0 });
|
|
142
176
|
|
|
143
177
|
function applyOv(base, ov) {
|
|
144
178
|
const d = JSON.parse(JSON.stringify(base));
|
|
145
|
-
const home = {};
|
|
146
|
-
LENS_IDS.forEach((l) => (d.lists[l] || []).forEach((i) => { home[i.id] = l; }));
|
|
147
|
-
const added = ov.added.map((a) => { home[a.item.id] = a.lens; return a.item; }).reverse();
|
|
148
179
|
const jobOfItem = {};
|
|
149
|
-
let items = [...added, ...(d.items || [])].filter((i) => !ov.gone.has(i.id));
|
|
150
180
|
const wasDone = new Set((d.items || []).filter((i) => i.done).map((i) => i.id));
|
|
151
|
-
items = items.map((i) => {
|
|
181
|
+
const items = [...ov.added.slice().reverse(), ...(d.items || [])].filter((i) => !ov.gone.has(i.id)).map((i) => {
|
|
152
182
|
const m = { ...i, ...(ov.edits.get(i.id) || {}) };
|
|
153
183
|
m.done = !!(i.done || ov.done.has(i.id));
|
|
154
184
|
m.steps = [...(i.steps || []), ...(ov.steps.get(i.id) || [])].map((s) => (ov.who.has(s.id) ? { ...s, who: ov.who.get(s.id) } : s));
|
|
155
185
|
jobOfItem[m.id] = m.job;
|
|
156
186
|
return m;
|
|
157
187
|
});
|
|
158
|
-
const
|
|
159
|
-
items
|
|
160
|
-
/*
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
const placed = buildWork(items, d.today, d.review_days || REVIEW_DAYS);
|
|
189
|
+
d.items = items; d.work = placed.work; d.review = placed.review; d.counts = placed.counts;
|
|
190
|
+
/* Focus: an accept pins, a dismiss hides for the day, a clear undoes either; a suggestion whose items are all closed goes */
|
|
191
|
+
const F = d.focus || { suggestions: [], accepted: [] };
|
|
192
|
+
const open = new Set(items.filter((i) => !i.done).map((i) => i.id));
|
|
193
|
+
const live = (f) => !(f.item_ids || []).length || f.item_ids.some((id) => open.has(id));
|
|
194
|
+
const st = (k) => ov.focus.get(k);
|
|
195
|
+
d.focus = {
|
|
196
|
+
accepted: [...(F.accepted || []).filter((a) => !st(a.key)), ...[...ov.focus.values()].filter((x) => x.state === 'acc').map((x) => x.f)],
|
|
197
|
+
suggestions: (F.suggestions || []).filter((s) => (!st(s.key) || st(s.key).state === 'clear') && live(s)).slice(0, 3),
|
|
198
|
+
};
|
|
165
199
|
const jobs = [...(d.jobs || []), ...ov.jobsAdded].filter((j) => !ov.jobsGone.has(j.id)).map((j) => ({ ...j, ...(ov.jobEdits.get(j.id) || {}) }));
|
|
166
200
|
jobs.forEach((j) => {
|
|
167
201
|
j.done = (j.done || 0) + [...ov.done].filter((id) => !wasDone.has(id) && jobOfItem[id] === j.id).length;
|
|
168
|
-
j.total = (j.total || 0) + ov.added.filter((a) => a.
|
|
202
|
+
j.total = (j.total || 0) + ov.added.filter((a) => a.job === j.id).length;
|
|
169
203
|
});
|
|
170
204
|
d.jobs = jobs;
|
|
171
205
|
return d;
|
|
@@ -176,21 +210,30 @@ function applyLocal(inst, path, body, r) {
|
|
|
176
210
|
const ov = inst.ov; const id = body && body.id;
|
|
177
211
|
const today = (inst.base && inst.base.today) || iso(new Date());
|
|
178
212
|
const cur = id && ((inst.data && inst.data.items) || []).find((i) => i.id === id);
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
const edit = (f) => ov.edits.set(id, { ...(ov.edits.get(id) || {}), ...(f || {}), touched_at: now }); // the server stamps touched_at on every event
|
|
179
215
|
switch (path.replace(API, '').replace(JOBS, '/job')) {
|
|
180
|
-
case '/done': ov.done.add(id); break;
|
|
181
|
-
case '/reopen': ov.done.delete(id); break;
|
|
182
|
-
case '/park':
|
|
183
|
-
case '/unpark':
|
|
184
|
-
case '/
|
|
185
|
-
case '/
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
case '/done': ov.done.add(id); edit(body.outcome ? { outcome: body.outcome } : {}); break;
|
|
217
|
+
case '/reopen': ov.done.delete(id); edit(); break;
|
|
218
|
+
case '/park': case '/someday': edit({ parked: true }); break;
|
|
219
|
+
case '/unpark': edit({ parked: false }); break;
|
|
220
|
+
case '/edit': edit(body.fields); break;
|
|
221
|
+
case '/flag': edit({ flagged: !!body.flagged }); break;
|
|
222
|
+
case '/defer': edit({ defer: body.until || null }); break;
|
|
223
|
+
case '/waiting': edit({ waiting_on: body.on || null }); break;
|
|
224
|
+
case '/notyet': {
|
|
225
|
+
const n = body.when === 'tomorrow' ? 1 : body.when === 'next-week' ? 7 : 0;
|
|
226
|
+
edit({ not_yet_count: ((cur && cur.not_yet_count) || 0) + 1, ...(n ? { defer: (r && r.defer) || addIso(today, n) } : {}) });
|
|
188
227
|
break;
|
|
189
228
|
}
|
|
229
|
+
case '/keep': edit(); break;
|
|
230
|
+
case '/focus/accept': ov.focus.set(body.key, { state: 'acc', f: { key: body.key, title: body.title, why: body.why, item_ids: body.item_ids || [], job: body.job || null } }); break;
|
|
231
|
+
case '/focus/dismiss': ov.focus.set(body.key, { state: 'no' }); break;
|
|
232
|
+
case '/focus/clear': ov.focus.set(body.key, { state: 'clear' }); break;
|
|
190
233
|
case '/add': {
|
|
191
234
|
const p = previewParse(body.line, today);
|
|
192
|
-
const item = { ...(r.item || {}), id: (r.item && r.item.id) || 'item-demo-add' + (++ov.seq), title: p.title || String(body.line), due: p.due, priority: p.priority, waiting_on: p.waiting_on, job: null, steps: [], created_at:
|
|
193
|
-
ov.added.push(
|
|
235
|
+
const item = { ...(r.item || {}), id: (r.item && r.item.id) || 'item-demo-add' + (++ov.seq), title: p.title || String(body.line), due: p.due, priority: p.priority, waiting_on: p.waiting_on, job: null, steps: [], created_at: now, touched_at: now };
|
|
236
|
+
ov.added.push(item);
|
|
194
237
|
r.item = item;
|
|
195
238
|
break;
|
|
196
239
|
}
|
|
@@ -199,7 +242,6 @@ function applyLocal(inst, path, body, r) {
|
|
|
199
242
|
case '/steps/add': { const l = ov.steps.get(id) || []; l.push({ id: 'step-demo' + (++ov.seq), text: body.text, who: 'you', done: false }); ov.steps.set(id, l); break; }
|
|
200
243
|
case '/steps/who': ov.who.set(body.step_id, body.who); break;
|
|
201
244
|
case '/steps/plan': {
|
|
202
|
-
const t = (cur && cur.title) || 'the item';
|
|
203
245
|
const l = ov.steps.get(id) || [];
|
|
204
246
|
[['Read the item and what it links to', 'agent-does'], ['Draft the first pass or the message', 'agent-drafts'], ['Check it and decide', 'you']].forEach(([text, who]) => l.push({ id: 'step-demo' + (++ov.seq), text, who, done: false }));
|
|
205
247
|
ov.steps.set(id, l);
|
|
@@ -227,8 +269,9 @@ export function mountPmWork(host, ctx) {
|
|
|
227
269
|
if (inst) return;
|
|
228
270
|
if (ctx && ctx.FIXTURE) return;
|
|
229
271
|
ensureC3Css();
|
|
230
|
-
inst = { host, ctx: ctx || {}, root: el('div', { class: 'c3 calwrap', 'data-pm-work': '' }), data: null, base: null, ov: newOv(), lens: '
|
|
231
|
-
opens: new Set(), picks: new Map(), editing: new Set(), planning: new Set(), runs: new Map(), trays: new Set(), cases: new Set(), timers: new Set(), hidden: new Set(), jobNew: null, jobForm: false
|
|
272
|
+
inst = { host, ctx: ctx || {}, root: el('div', { class: 'c3 calwrap', 'data-pm-work': '' }), data: null, base: null, ov: newOv(), lens: 'today', group: false,
|
|
273
|
+
opens: new Set(), picks: new Map(), editing: new Set(), planning: new Set(), runs: new Map(), trays: new Set(), cases: new Set(), timers: new Set(), hidden: new Set(), jobNew: null, jobForm: false,
|
|
274
|
+
focEdit: new Set(), rv: { ids: null, handled: new Set(), changing: false } };
|
|
232
275
|
inst.body = el('div', { class: 'calbody pmw-body' });
|
|
233
276
|
inst.bar = el('div', { class: 'calbar' });
|
|
234
277
|
inst.addBox = addLine(inst);
|
|
@@ -236,6 +279,7 @@ export function mountPmWork(host, ctx) {
|
|
|
236
279
|
inst.stopPhone = c3Phone(inst.root);
|
|
237
280
|
insts.set(host, inst);
|
|
238
281
|
host.appendChild(inst.root);
|
|
282
|
+
if (inst.ctx.onLens) inst.ctx.onLens(inst.lens); // the page's deck follows the view the page opens on
|
|
239
283
|
load(inst);
|
|
240
284
|
}
|
|
241
285
|
|
|
@@ -286,18 +330,34 @@ async function act(inst, rowEl, spec) {
|
|
|
286
330
|
}
|
|
287
331
|
|
|
288
332
|
const undoBy = (inst, route, body) => async () => !!(await press(inst, API + route, body, { quiet: true }));
|
|
333
|
+
/* Does the change move the item out of the view on screen? Then its row leaves; otherwise it stays and redraws. */
|
|
334
|
+
const leaves = (inst, item, patch) => inst.lens === 'review' || workPlace({ ...item, ...patch }, inst.data.today) !== inst.lens;
|
|
289
335
|
|
|
290
|
-
const doneAct = (inst, item, row, text) => act(inst, row, { route: '/done', body: { id: item.id }, text: text || COPY.doneToast, undo: undoBy(inst, '/reopen', { id: item.id }) });
|
|
291
|
-
const parkAct = (inst, item, row, text) => act(inst, row, { route: '/park', body: { id: item.id }, text: text || COPY.parkToast, undo: undoBy(inst, '/unpark', { id: item.id }) });
|
|
292
|
-
const moveAct = (inst, item, row, list, text) => {
|
|
293
|
-
const back = lensOfItem(inst, item.id);
|
|
294
|
-
return act(inst, row, { route: '/move', body: { id: item.id, list }, text, undo: undoBy(inst, '/move', { id: item.id, list: back }) });
|
|
295
|
-
};
|
|
296
|
-
const dueAct = (inst, item, row, due, text) => act(inst, row, { route: '/edit', body: { id: item.id, fields: { due } }, text, undo: undoBy(inst, '/edit', { id: item.id, fields: { due: item.due || null } }) });
|
|
336
|
+
const doneAct = (inst, item, row, text, outcome) => act(inst, row, { route: '/done', body: outcome ? { id: item.id, outcome } : { id: item.id }, text: text || COPY.doneToast, undo: undoBy(inst, '/reopen', { id: item.id }) });
|
|
297
337
|
const dropAct = (inst, item, row) => act(inst, row, { route: '/remove', body: { id: item.id }, text: COPY.removed, undo: undoBy(inst, '/undo', { id: item.id }) });
|
|
338
|
+
const todayAct = (inst, item, row) => act(inst, row, { route: '/edit', body: { id: item.id, fields: { due: inst.data.today, defer: null } }, text: 'In Today.',
|
|
339
|
+
undo: undoBy(inst, '/edit', { id: item.id, fields: { due: item.due || null, defer: item.defer || null } }), leave: leaves(inst, item, { due: inst.data.today, defer: null }) });
|
|
340
|
+
const flagAct = (inst, item, row) => { const on = !item.flagged; return act(inst, row, { route: '/flag', body: { id: item.id, flagged: on }, text: on ? 'Flagged for today.' : 'Unflagged.', undo: undoBy(inst, '/flag', { id: item.id, flagged: !on }), leave: leaves(inst, item, { flagged: on }) }); };
|
|
341
|
+
const deferAct = (inst, item, row, until) => act(inst, row, { route: '/defer', body: { id: item.id, until }, text: until ? COPY.notBeforeChip(dayLabel(until)) + '.' : 'Not before cleared.',
|
|
342
|
+
undo: undoBy(inst, '/defer', { id: item.id, until: item.defer || null }), leave: leaves(inst, item, { defer: until }) });
|
|
343
|
+
const somedayAct = (inst, item, row) => act(inst, row, { route: '/someday', body: { id: item.id }, text: 'Parked in Someday.', undo: undoBy(inst, '/unpark', { id: item.id }), leave: leaves(inst, item, { parked: true }) });
|
|
344
|
+
const unparkAct = (inst, item, row) => act(inst, row, { route: '/unpark', body: { id: item.id }, text: 'Back from Someday.', undo: undoBy(inst, '/someday', { id: item.id }), leave: leaves(inst, item, { parked: false }) });
|
|
345
|
+
const waitAct = (inst, item, row, on) => act(inst, row, { route: '/waiting', body: { id: item.id, on }, text: on ? 'Waiting on ' + on + '.' : 'No longer waiting.',
|
|
346
|
+
undo: undoBy(inst, '/waiting', { id: item.id, on: item.waiting_on || null }), leave: leaves(inst, item, { waiting_on: on }) });
|
|
347
|
+
function notYetAct(inst, item, row, when) {
|
|
348
|
+
const today = inst.data.today; const n = when === 'tomorrow' ? 1 : when === 'next-week' ? 7 : 0;
|
|
349
|
+
return act(inst, row, { route: '/notyet', body: { id: item.id, when }, text: n ? 'Not yet. Back in Today on ' + dayLabel(addIso(today, n)) + '.' : 'Not yet. It stays in Today for later.',
|
|
350
|
+
undo: undoBy(inst, '/defer', { id: item.id, until: item.defer || null }), leave: n ? leaves(inst, item, { defer: addIso(today, n) }) : false });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Focus (PRD R4): accept pins a job to Today, Not today hides it for the day, Unpin takes it back; each offers Undo. */
|
|
354
|
+
const focusBody = (f) => ({ key: f.key, title: f.title, why: f.why, item_ids: f.item_ids || [], ...(f.job ? { job: f.job } : {}) });
|
|
355
|
+
const focusAccept = (inst, f) => { inst.focEdit.delete(f.key); return act(inst, null, { route: '/focus/accept', body: focusBody(f), text: COPY.focusAccepted, undo: undoBy(inst, '/focus/clear', { key: f.key }), leave: false }); };
|
|
356
|
+
const focusDismiss = (inst, f) => act(inst, null, { route: '/focus/dismiss', body: { key: f.key }, text: COPY.focusDismissed, undo: undoBy(inst, '/focus/clear', { key: f.key }), leave: false });
|
|
357
|
+
const focusClear = (inst, f) => act(inst, null, { route: '/focus/clear', body: { key: f.key }, text: COPY.focusCleared, undo: undoBy(inst, '/focus/accept', focusBody(f)), leave: false });
|
|
298
358
|
|
|
299
359
|
function lensOfItem(inst, id) {
|
|
300
|
-
for (const l of
|
|
360
|
+
for (const l of VIEWS) if ((views(inst)[l] || []).some((i) => i.id === id)) return l;
|
|
301
361
|
return inst.lens;
|
|
302
362
|
}
|
|
303
363
|
|
|
@@ -319,9 +379,8 @@ function planSteps(inst, item) {
|
|
|
319
379
|
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
320
380
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
321
381
|
const iso = (d) => d.toISOString().slice(0, 10);
|
|
322
|
-
const addIso = (s, n) => { const d = new Date(s + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() + n); return iso(d); };
|
|
323
382
|
const nextMonday = (s) => { const d = new Date(s + 'T00:00:00Z'); return addIso(s, ((8 - d.getUTCDay()) % 7) || 7); };
|
|
324
|
-
const dayLabel = (s) => { const d = new Date(s + 'T00:00:00Z'); return DAYS[d.getUTCDay()].slice(0, 1).toUpperCase() + DAYS[d.getUTCDay()].slice(1, 3) + ' ' + d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
|
|
383
|
+
const dayLabel = (s) => { const d = new Date(String(s).slice(0, 10) + 'T00:00:00Z'); return DAYS[d.getUTCDay()].slice(0, 1).toUpperCase() + DAYS[d.getUTCDay()].slice(1, 3) + ' ' + d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
|
|
325
384
|
|
|
326
385
|
function previewParse(text, todayIso) {
|
|
327
386
|
let rest = String(text || '').trim();
|
|
@@ -346,24 +405,16 @@ function previewParse(text, todayIso) {
|
|
|
346
405
|
return out;
|
|
347
406
|
}
|
|
348
407
|
|
|
349
|
-
function lensOf(o, todayIso) {
|
|
350
|
-
if (o.waiting_on) return 'waiting';
|
|
351
|
-
if (!o.due) return 'triage';
|
|
352
|
-
if (o.due <= todayIso) return 'today';
|
|
353
|
-
const w = new Date(todayIso + 'T00:00:00Z'); w.setUTCDate(w.getUTCDate() + 7);
|
|
354
|
-
return o.due <= iso(w) ? 'week' : 'later';
|
|
355
|
-
}
|
|
356
|
-
|
|
357
408
|
const labelOf = (id) => (LENSES.find((l) => l[0] === id) || [])[1] || id;
|
|
358
409
|
const small = 'font-size:var(--fs-2)';
|
|
359
410
|
|
|
360
|
-
/* The mockup's c3prev(): "adds to", a .tag, then .chip2 / .fl / .st chips
|
|
411
|
+
/* The mockup's c3prev(): "adds to", a .tag with the view the server will place the line in (PRD R2), then .chip2 / .fl / .st chips. */
|
|
361
412
|
function previewChips(inst, text) {
|
|
362
413
|
const today = (inst.data && inst.data.today) || iso(new Date());
|
|
363
414
|
if (!text.trim()) return [el('span', { class: 'dim mono', style: small }, COPY.hint)];
|
|
364
415
|
const o = previewParse(text, today);
|
|
365
416
|
if (!o.title) return [el('span', { class: 'dim mono', style: small }, 'The line needs a title.')];
|
|
366
|
-
const out = [el('span', { class: 'dim mono'
|
|
417
|
+
const out = [el('span', { class: 'dim mono wk-sm' }, COPY.addsTo), el('span', { class: 'tag', 'data-pm-lands': workPlace(o, today) }, labelOf(workPlace(o, today)))];
|
|
367
418
|
if (o.due) out.push(el('span', { class: 'chip2' }, c3ic('cal', 'sm'), o.due));
|
|
368
419
|
if (o.priority) out.push(el('span', { class: 'fl ' + o.priority.toLowerCase() }, o.priority));
|
|
369
420
|
if (o.waiting_on) out.push(el('span', { class: 'chip2' }, c3ic('wait', 'sm'), COPY.waitingOn(o.waiting_on)));
|
|
@@ -379,12 +430,12 @@ function addLine(inst) {
|
|
|
379
430
|
const send = async () => {
|
|
380
431
|
const line = input.value.trim();
|
|
381
432
|
if (!line) return;
|
|
382
|
-
const before = inst.data && inst.data.today;
|
|
383
|
-
const o = previewParse(line, before
|
|
433
|
+
const before = (inst.data && inst.data.today) || iso(new Date());
|
|
434
|
+
const o = previewParse(line, before);
|
|
384
435
|
const r = await press(inst, API + '/add', { line }, { quiet: true });
|
|
385
436
|
if (!r) return;
|
|
386
437
|
input.value = ''; chips.replaceChildren(...previewChips(inst, ''));
|
|
387
|
-
inst.lens =
|
|
438
|
+
inst.lens = workPlace(o, before); if (inst.ctx.onLens) inst.ctx.onLens(inst.lens); inst.group = false;
|
|
388
439
|
if (r.item) inst.opens.add(r.item.id);
|
|
389
440
|
say({ text: 'Added to ' + labelOf(inst.lens), timeout: r.undo_ms || 6000, actionLabel: COPY.undo, onAction: async () => { if (r.item) await press(inst, API + '/remove', { id: r.item.id }, { quiet: true }); await load(inst); } });
|
|
390
441
|
await load(inst);
|
|
@@ -402,11 +453,28 @@ function addLine(inst) {
|
|
|
402
453
|
/* ── rows and the case file ──────────────────────────────────────────────────────────────────────────────────────── */
|
|
403
454
|
|
|
404
455
|
const jobById = (inst, id) => (inst.data.jobs || []).find((j) => j.id === id);
|
|
405
|
-
|
|
406
|
-
|
|
456
|
+
/* The five places, as the server answers them (`work`). An older server that sends only `lists` is read through the same five names. */
|
|
457
|
+
function views(inst) {
|
|
458
|
+
const d = inst.data || {};
|
|
459
|
+
if (d.work) return d.work;
|
|
460
|
+
const L = d.lists || {};
|
|
461
|
+
return { inbox: L.triage || [], today: L.today || [], upcoming: [...(L.week || []), ...(L.later || []).filter((i) => !i.parked)], waiting: L.waiting || [], someday: (L.later || []).filter((i) => i.parked) };
|
|
462
|
+
}
|
|
463
|
+
const shown = (inst, id) => ((id === 'review' ? (inst.data || {}).review : views(inst)[id]) || []).filter((i) => !inst.hidden.has(i.id) && !i.done);
|
|
464
|
+
const countOf = (inst, id) => { const c = (inst.data || {}).counts; return c && typeof c[id] === 'number' ? c[id] : shown(inst, id).length; };
|
|
465
|
+
const itemById = (inst, id) => ((inst.data && inst.data.items) || []).find((i) => i.id === id)
|
|
466
|
+
|| [...VIEWS.map((v) => views(inst)[v] || []).flat(), ...((inst.data && inst.data.review) || [])].find((i) => i.id === id);
|
|
467
|
+
/* The title of the team task a Work item advances (PRD R8): the item's own copy, then a map the answer may carry, else the id. */
|
|
468
|
+
const resolvesTitle = (inst, item) => item.resolves_title || ((inst.data && inst.data.resolves_titles) || {})[item.resolves] || item.resolves;
|
|
407
469
|
|
|
408
470
|
/* The mockup's .c3jf: a label (.dim.mono) and an input on one line (console3.html line 4651). */
|
|
409
471
|
function field(label, input) { return el('label', null, el('span', { class: 'dim mono' }, label), input); }
|
|
472
|
+
/* A text button of the Work page (the mockup's .pill). */
|
|
473
|
+
function pill(label, icon, attrs) {
|
|
474
|
+
const b = button({ label, variant: 'ghost', attrs: { ...(attrs || {}), class: 'pill' } });
|
|
475
|
+
if (icon) b.prepend(c3ic(icon, 'sm'));
|
|
476
|
+
return b;
|
|
477
|
+
}
|
|
410
478
|
|
|
411
479
|
function editField(inst, item, label, shortLabel, kind, key, value) {
|
|
412
480
|
const input = el(kind === 'select' ? 'select' : 'input', { 'aria-label': label, 'data-pm-edit': item.id + ':' + key, ...(kind === 'date' ? { type: 'date' } : kind === 'text' ? { type: 'text' } : {}) });
|
|
@@ -426,6 +494,15 @@ function editField(inst, item, label, shortLabel, kind, key, value) {
|
|
|
426
494
|
return field(shortLabel, input);
|
|
427
495
|
}
|
|
428
496
|
|
|
497
|
+
/* The editable fields of an item (title, due, not before, priority, waiting on, job): in the case file behind "Edit details", and Review's Change. */
|
|
498
|
+
const editForm = (inst, item) => el('div', { class: 'c3jf', 'data-pm-edit-form': item.id },
|
|
499
|
+
editField(inst, item, 'Title', 'Title', 'text', 'title', item.title),
|
|
500
|
+
editField(inst, item, 'Due', 'Due', 'date', 'due', item.due),
|
|
501
|
+
editField(inst, item, COPY.notBefore, COPY.notBefore, 'date', 'defer', item.defer),
|
|
502
|
+
editField(inst, item, 'Priority', 'Priority', 'select', 'priority', item.priority),
|
|
503
|
+
editField(inst, item, 'Waiting on', 'Waiting', 'text', 'waiting_on', item.waiting_on),
|
|
504
|
+
editField(inst, item, 'Job', 'Job', 'select', 'job', item.job));
|
|
505
|
+
|
|
429
506
|
/* The mockup's c3stepsHtml(): .c3steps > .c3sh + ul > li (.c3sg, .c3stt, .chips2 with the who pill) + an add input (line 4595). */
|
|
430
507
|
function stepsBlock(inst, item) {
|
|
431
508
|
if (inst.planning.has(item.id)) return el('div', { class: 'fold a', 'data-pm-planning': item.id, role: 'status' }, c3ic('spark', 'sm'), 'The agent is reading the item and planning the steps');
|
|
@@ -452,48 +529,78 @@ function stepsBlock(inst, item) {
|
|
|
452
529
|
el('div', { class: 'form' }, input));
|
|
453
530
|
}
|
|
454
531
|
|
|
532
|
+
/* ── the shared actions and offline work (PRD R6) ──────────────────────────────────────────────────────────────────── */
|
|
533
|
+
|
|
534
|
+
const rowOf = (inst, item) => inst.root.querySelector('[data-pm-item="' + item.id + '"]');
|
|
535
|
+
|
|
536
|
+
/** Flag, Not before, Someday, Waiting and Drop: every item has them in its case file. Each goes through act(), so each offers Undo. */
|
|
537
|
+
function quickActs(inst, item, { noDrop } = {}) {
|
|
538
|
+
const today = inst.data.today;
|
|
539
|
+
const date = el('input', { type: 'date', 'aria-label': COPY.notBefore, 'data-pm-defer': item.id, min: addIso(today, 1) });
|
|
540
|
+
date.value = item.defer && item.defer > today ? item.defer : '';
|
|
541
|
+
date.addEventListener('change', () => { if (date.value && date.value !== item.defer) deferAct(inst, item, rowOf(inst, item), date.value); });
|
|
542
|
+
const who = el('input', { type: 'text', placeholder: COPY.waitingWho, 'aria-label': COPY.waitingField, 'data-pm-wait-on': item.id, autocomplete: 'off' });
|
|
543
|
+
who.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing && who.value.trim()) waitAct(inst, item, rowOf(inst, item), who.value.trim()); });
|
|
544
|
+
return el('div', { class: 'wkq', 'data-pm-quick': item.id },
|
|
545
|
+
pill(item.flagged ? COPY.unflag : COPY.flag, 'flag', { 'data-pm-flag': item.id, 'aria-pressed': String(!!item.flagged), onclick: () => flagAct(inst, item, rowOf(inst, item)) }),
|
|
546
|
+
el('label', { class: 'wkq-f' }, el('span', { class: 'dim mono' }, COPY.notBefore), date),
|
|
547
|
+
item.parked ? pill(COPY.bringBack, 'sun', { 'data-pm-unpark': item.id, onclick: () => unparkAct(inst, item, rowOf(inst, item)) })
|
|
548
|
+
: pill(COPY.someday, 'moon', { 'data-pm-someday': item.id, onclick: () => somedayAct(inst, item, rowOf(inst, item)) }),
|
|
549
|
+
item.waiting_on ? pill(COPY.stopWaiting, 'wait', { 'data-pm-unwait': item.id, onclick: () => waitAct(inst, item, rowOf(inst, item), null) })
|
|
550
|
+
: el('label', { class: 'wkq-f' }, el('span', { class: 'dim mono' }, COPY.waitingField), who),
|
|
551
|
+
noDrop ? null : pill(COPY.drop, 'trash', { 'data-pm-drop': item.id, onclick: () => dropAct(inst, item, rowOf(inst, item)) }));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** An item done outside PMOS: who it involves, Done with an optional outcome, Not yet, Drop, and "Looks done?" when there is proof.
|
|
555
|
+
* PMOS never closes it by itself: Confirm is the PM's press. */
|
|
556
|
+
function offlineBlock(inst, item) {
|
|
557
|
+
const out = el('input', { type: 'text', placeholder: COPY.outcome, 'aria-label': COPY.outcome, 'data-pm-outcome': item.id, autocomplete: 'off' });
|
|
558
|
+
const doneNow = () => { const v = out.value.trim(); return doneAct(inst, item, rowOf(inst, item), v ? 'Done. The outcome is saved.' : 'Done.', v || undefined); };
|
|
559
|
+
out.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) doneNow(); });
|
|
560
|
+
return el('div', { class: 'wkoff', 'data-pm-offline': item.id },
|
|
561
|
+
el('p', { class: 'wkoff-n' }, c3ic('user', 'sm'), COPY.offlineNote(item.person)),
|
|
562
|
+
item.evidence ? el('div', { class: 'wkev', 'data-pm-evidence': item.id }, c3ic('eye', 'sm'), el('span', { class: 'wk-min' }, el('b', null, COPY.looksDone + ' '), item.evidence),
|
|
563
|
+
pill(COPY.confirm, 'check', { class: 'pill pri', 'data-pm-confirm': item.id, onclick: () => doneAct(inst, item, rowOf(inst, item), 'Done. You confirmed it.') })) : null,
|
|
564
|
+
el('div', { class: 'wkq' }, el('span', { class: 'form wkoff-o' }, out), pill(COPY.done, 'check', { class: 'pill pri', 'data-pm-offline-done': item.id, onclick: doneNow })),
|
|
565
|
+
el('div', { class: 'wkq' }, el('span', { class: 'dim mono' }, COPY.notYet),
|
|
566
|
+
...[['later-today', COPY.laterToday], ['tomorrow', COPY.tomorrow], ['next-week', COPY.nextWeek]].map(([w, l]) => pill(l, null, { 'data-pm-notyet': w, onclick: () => notYetAct(inst, item, rowOf(inst, item), w) })),
|
|
567
|
+
pill(COPY.drop, 'trash', { 'data-pm-drop': item.id, onclick: () => dropAct(inst, item, rowOf(inst, item)) })));
|
|
568
|
+
}
|
|
569
|
+
|
|
455
570
|
/* ── the case model of one item (console3.html genTask line 3216, agDecor line 5126) ─────────────────────────────────────────── */
|
|
456
571
|
|
|
457
572
|
const OUT_REASON = 'No channel connected, so this stays a draft. Nothing is sent.';
|
|
458
573
|
const fx = (...a) => a;
|
|
459
574
|
|
|
460
|
-
/** The options of an item by its
|
|
575
|
+
/** The options of an item by its view. Each: { id, l, rec, fx, outward, run(row) } where run does the work through a press route.
|
|
576
|
+
* An offline item has none: it acts through its own block (Done, Not yet, Looks done). */
|
|
461
577
|
function lensOptions(inst, item, lens) {
|
|
462
|
-
const
|
|
463
|
-
const today = inst.data.today; const mon = nextMonday(today);
|
|
578
|
+
const who = item.waiting_on || 'them';
|
|
579
|
+
const today = inst.data.today; const tom = addIso(today, 1); const mon = nextMonday(today);
|
|
464
580
|
const o = (id, l, rec, f, run, extra) => ({ id, l, rec: !!rec, fx: f, run, ...(extra || {}) });
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
const
|
|
581
|
+
if (item.kind === 'offline') return { conf: 0, cta: '', opts: [] };
|
|
582
|
+
const doneO = (rec) => o('done', 'Mark it done', rec, [fx('check', 'Closes the item', 'ok')], (r) => doneAct(inst, item, r, 'Done.'));
|
|
583
|
+
const todayO = (rec) => o('today', 'Do it today', rec, [fx('sun', 'Moves to Today', 'ok')], (r) => todayAct(inst, item, r));
|
|
584
|
+
const deferO = (d, rec) => o('defer-' + d, COPY.notBeforeChip(dayLabel(d)), rec, [fx('moon', 'Out of Today until then')], (r) => deferAct(inst, item, r, d));
|
|
585
|
+
const somedayO = () => o('someday', 'Park it in Someday', 0, [fx('moon', 'Back in the weekly Review')], (r) => somedayAct(inst, item, r));
|
|
468
586
|
const dropO = () => o('drop', 'Drop it', 0, [fx('x', 'Gone, with Undo', 'warn')], (r) => dropAct(inst, item, r));
|
|
469
587
|
if (lens === 'waiting') {
|
|
470
588
|
return { conf: 80, cta: 'Slide to nudge', sit: 'Waiting on ' + who + '. ' + (item.why || 'No reply yet.'), opts: [
|
|
471
589
|
o('nudge', 'Nudge ' + who + ' now (drafted)', 1, [fx('out', 'Goes to Review first', 'ok')], null, { outward: true }),
|
|
472
|
-
o('
|
|
590
|
+
o('unwait', 'Stop waiting: it is yours again', 0, [fx('check', 'Leaves Waiting', 'ok')], (r) => waitAct(inst, item, r, null)),
|
|
473
591
|
o('wait', 'Keep waiting', 0, [fx('clock', 'Ages another day', 'warn')], () => say({ text: 'Still waiting.', timeout: 2500 })),
|
|
474
592
|
] };
|
|
475
593
|
}
|
|
476
|
-
if (lens === '
|
|
477
|
-
o('
|
|
478
|
-
if (lens === '
|
|
479
|
-
o('
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (lens === 'today') return { conf: 82, cta: 'Slide to finish', opts: [nowO(1),
|
|
483
|
-
o('mig', 'Migrate to tomorrow', 0, [fx('moon', 'Back tomorrow 09:00', 'warn')], (r) => dueAct(inst, item, r, addIso(today, 1), 'Migrated to tomorrow.')), schedO()] };
|
|
484
|
-
return { conf: q ? 90 : 78, cta: q ? 'Slide to do it now' : 'Slide to commit', opts: q ? [nowO(1), todayO(0), schedO(), dropO()] : [todayO(1), nowO(0), schedO(), dropO()] };
|
|
594
|
+
if (lens === 'someday') return { conf: 66, cta: 'Slide to bring it back', sit: 'Parked on purpose. It only matters again when you give it a date.', opts: [
|
|
595
|
+
o('back', 'Bring it back', 1, [fx('check', 'Placed by its dates again', 'ok')], (r) => unparkAct(inst, item, r)), dropO()] };
|
|
596
|
+
if (lens === 'upcoming') return { conf: 70, cta: 'Slide to keep the date', opts: [
|
|
597
|
+
o('keep', 'Keep the date', 1, [fx('check', 'On plan', 'ok')], () => say({ text: 'Kept.', timeout: 2500 })), todayO(0), somedayO()] };
|
|
598
|
+
if (lens === 'today') return { conf: 82, cta: 'Slide to finish', opts: [doneO(1), deferO(tom, 0), deferO(mon, 0)] };
|
|
599
|
+
return { conf: 78, cta: 'Slide to decide', opts: [todayO(1), deferO(mon, 0), somedayO(), dropO()] };
|
|
485
600
|
}
|
|
486
601
|
|
|
487
602
|
/** { model, run } for an item: the model goes to renderCase(), `run(id, row)` does what the picked option says. */
|
|
488
603
|
function itemCase(inst, item, lens) {
|
|
489
|
-
if (item.noise) { // the mockup's genTask() for an FYI item (line 3232): the agent read it and found nothing for the PM
|
|
490
|
-
const o = (id, l, rec, f, run) => ({ id, l, rec: !!rec, fx: f, run });
|
|
491
|
-
const L = [o('arch', 'Archive it', 1, [fx('check', 'Keeps the inbox clean', 'ok')], (r) => doneAct(inst, item, r, 'Archived.')), o('keep', 'Keep it', 0, [fx('moon', 'Stays in the inbox')], () => say({ text: 'Kept.', timeout: 2500 }))];
|
|
492
|
-
const model = { id: 'task:' + item.id, sit: 'Noise. Nothing in it needs you, and the agent has already read it.', chg: [['clock', 'About under 2 min', 'effort']], conf: 92, cta: 'Slide to archive', unsure: '', ign: '', outboxReason: OUT_REASON,
|
|
493
|
-
opts: L.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: false })), why: '' };
|
|
494
|
-
const run = {}; L.forEach((x) => { run[x.id] = x.run; });
|
|
495
|
-
return { model, run };
|
|
496
|
-
}
|
|
497
604
|
const ix = item.ix || {}; const mock = item.mock_row === true;
|
|
498
605
|
const j = item.job && jobById(inst, item.job);
|
|
499
606
|
const L = lensOptions(inst, item, lens);
|
|
@@ -503,14 +610,16 @@ function itemCase(inst, item, lens) {
|
|
|
503
610
|
if (ix.why) c.push(['info', ix.why[0], ix.why[1] || '']);
|
|
504
611
|
else if (item.why) c.push(['info', item.why, item.src || '']);
|
|
505
612
|
if (j) c.push(['target', 'Job: ' + jm, 'JTBD']);
|
|
613
|
+
if (item.resolves) c.push(['init', COPY.advances(resolvesTitle(inst, item)), 'team task']);
|
|
506
614
|
if (mock && item.mins) c.push(['clock', 'About ' + (item.mins <= 2 ? 'under 2' : item.mins) + ' min', 'effort']);
|
|
507
615
|
if (item.due) c.push(['cal', 'Due ' + item.due, '']);
|
|
616
|
+
if (item.defer && item.defer > inst.data.today) c.push(['moon', COPY.notBeforeChip(dayLabel(item.defer)), '']);
|
|
508
617
|
if (item.priority && !mock) c.push(['flag', 'Priority ' + item.priority, '']);
|
|
509
618
|
if (item.waiting_on) c.push(['wait', 'Waiting on ' + item.waiting_on, '']);
|
|
510
619
|
return c.slice(0, 3);
|
|
511
620
|
})();
|
|
512
621
|
const model = {
|
|
513
|
-
id: 'task:' + item.id, sit: ix.sit || (j ? COPY.jobLine(j) : (item.why || (mock ? item.title : 'No job to be done yet. File it under one below, or leave it.'))),
|
|
622
|
+
id: 'task:' + item.id, sit: ix.sit || L.sit || (j ? COPY.jobLine(j) : (item.why || (mock ? item.title : 'No job to be done yet. File it under one below, or leave it.'))),
|
|
514
623
|
chg: evidence, conf: ix.conf || L.conf, cta: L.cta, unsure: ix.unsure || '', ign: ix.ign || '', outboxReason: OUT_REASON,
|
|
515
624
|
opts: L.opts.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: !!x.outward })), why: '',
|
|
516
625
|
};
|
|
@@ -532,7 +641,7 @@ function itemCase(inst, item, lens) {
|
|
|
532
641
|
run.accept = (r) => doneAct(inst, item, r, 'Accepted. Recorded as your press.');
|
|
533
642
|
run.keep = () => say({ text: 'Kept.', timeout: 2500 });
|
|
534
643
|
} else {
|
|
535
|
-
model.move = ix.move + '
|
|
644
|
+
model.move = ix.move + ' · swipe right, or slide below';
|
|
536
645
|
model.opts = [{ id: 'agmv', l: 'Let the agent act: ' + ix.move, rec: 1, fx: [fx('spark', 'The suggested move, taken by an agent', 'ok'), fx('shield', 'Inside the repo; outward only as a draft', 'ok')] }, ...model.opts.map((x) => ({ ...x, rec: 0 }))];
|
|
537
646
|
model.cta = 'Slide to let the agent act';
|
|
538
647
|
run.agmv = () => { say({ text: 'The agent is working on it. Anything outward stays a draft for your slide.', timeout: 4000 }); startRun(inst, item, 'res'); }; // the toast is the case's slide; a swipe starts the run without one, as the mockup does
|
|
@@ -558,16 +667,15 @@ function startRun(inst, item, intent) {
|
|
|
558
667
|
}
|
|
559
668
|
|
|
560
669
|
|
|
561
|
-
/** The case file element of an item: c3-case.js plus what is Work's own (the steps, the agent tray, the edit fields). */
|
|
670
|
+
/** The case file element of an item: c3-case.js plus what is Work's own (offline work, the shared actions, the steps, the agent tray, the edit fields). */
|
|
562
671
|
function caseFile(inst, item, lens) {
|
|
563
672
|
const { model, run } = itemCase(inst, item, lens);
|
|
564
673
|
const root = renderCase(model, {
|
|
565
674
|
picked: inst.picks.get(item.id), outbox: false,
|
|
566
675
|
onPick: (id) => inst.picks.set(item.id, id),
|
|
567
676
|
onConfirm: ({ option }) => {
|
|
568
|
-
const row = inst.root.querySelector('[data-pm-item="' + item.id + '"]');
|
|
569
677
|
const fn = run[option.id];
|
|
570
|
-
if (fn) fn(
|
|
678
|
+
if (fn) fn(rowOf(inst, item));
|
|
571
679
|
},
|
|
572
680
|
});
|
|
573
681
|
root.setAttribute('data-pm-case', item.id);
|
|
@@ -578,6 +686,7 @@ function caseFile(inst, item, lens) {
|
|
|
578
686
|
const extras = [];
|
|
579
687
|
const rs = inst.runs.get(item.id);
|
|
580
688
|
const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
|
|
689
|
+
if (item.kind === 'offline') extras.push(offlineBlock(inst, item));
|
|
581
690
|
if (rs && rs.phase === 'work') {
|
|
582
691
|
extras.push(el('div', { class: 'c3ag', 'data-ag': item.id }, el('div', { class: 'fold a', role: 'status' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), rs.steps[Math.min(rs.i, rs.steps.length - 1)] + ' · ' + (Math.min(rs.i, rs.steps.length - 1) + 1) + ' of ' + rs.steps.length,
|
|
583
692
|
el('button', { type: 'button', class: 'chip2 lk', 'data-pm-agent-stop': item.id, onclick: () => { inst.runs.delete(item.id); say({ text: 'Stopped. Nothing was kept.', timeout: 2500 }); draw(inst); } }, c3ic('stop', 'sm'), 'stop'))));
|
|
@@ -592,18 +701,13 @@ function caseFile(inst, item, lens) {
|
|
|
592
701
|
c3ib('Speak is off on this line: nothing on this page records. Type the reply here.', 'mic', { 'data-off': '', onclick: () => say({ text: 'The microphone needs the app. Nothing was recorded: type the reply here.', timeout: 4200 }) }, 'sm'),
|
|
593
702
|
c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
|
|
594
703
|
}
|
|
595
|
-
const showSteps = !(rs && rs.phase) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
|
|
704
|
+
const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
|
|
596
705
|
if (showSteps) extras.push(stepsBlock(inst, item));
|
|
597
706
|
const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
|
|
598
707
|
if (anchor) anchor.after(...extras); else root.prepend(...extras);
|
|
599
|
-
|
|
600
|
-
/* the editable fields stay in the case file behind an Edit button (title, due, priority, waiting on, job) */
|
|
601
|
-
const form =
|
|
602
|
-
editField(inst, item, 'Title', 'Title', 'text', 'title', item.title),
|
|
603
|
-
editField(inst, item, 'Due', 'Due', 'date', 'due', item.due),
|
|
604
|
-
editField(inst, item, 'Priority', 'Priority', 'select', 'priority', item.priority),
|
|
605
|
-
editField(inst, item, 'Waiting on', 'Waiting', 'text', 'waiting_on', item.waiting_on),
|
|
606
|
-
editField(inst, item, 'Job', 'Job', 'select', 'job', item.job));
|
|
708
|
+
root.append(quickActs(inst, item, { noDrop: item.kind === 'offline' }));
|
|
709
|
+
/* the editable fields stay in the case file behind an Edit button (title, due, not before, priority, waiting on, job) */
|
|
710
|
+
const form = editForm(inst, item); form.hidden = !inst.editing.has(item.id);
|
|
607
711
|
const edit = el('button', { type: 'button', class: 'chip2 lk', 'aria-expanded': String(inst.editing.has(item.id)), 'data-pm-edit-toggle': item.id,
|
|
608
712
|
onclick: () => { const on = !inst.editing.has(item.id); if (on) inst.editing.add(item.id); else inst.editing.delete(item.id); form.hidden = !on; edit.setAttribute('aria-expanded', String(on)); } }, c3ic('edit', 'sm'), 'Edit details');
|
|
609
713
|
const ft = root.querySelector('.cft');
|
|
@@ -615,7 +719,6 @@ function caseFile(inst, item, lens) {
|
|
|
615
719
|
/* ── swipe: the mockup's workSwipe (console3.html line 3250) and the agent's swapped glyphs (line 5136) ─────────────────────── */
|
|
616
720
|
|
|
617
721
|
function swipeSpec(inst, item, lens) {
|
|
618
|
-
if (item.noise) { const a = { label: 'Archive', icon: 'check', fn: (r) => doneAct(inst, item, r, 'Archived.') }; return { right: a, left: { ...a, icon: 'moon' } }; } // the mockup archives a noise row either way
|
|
619
722
|
const mock = item.mock_row === true; const today = inst.data.today;
|
|
620
723
|
if (mock && item.agent_move && (item.ix || {}).move) {
|
|
621
724
|
return {
|
|
@@ -623,16 +726,15 @@ function swipeSpec(inst, item, lens) {
|
|
|
623
726
|
left: { label: 'Reply', icon: 'reply', fn: () => { if (inst.runs.get(item.id) && inst.runs.get(item.id).phase === 'work') { say({ text: 'A session is already working on this.', timeout: 2500 }); return; } inst.trays.add(item.id); inst.opens.add(item.id); draw(inst); later(inst, () => { const i = inst.root.querySelector('[data-pm-agent-reply="' + item.id + '"]'); if (i) i.focus(); }, 30); } },
|
|
624
727
|
};
|
|
625
728
|
}
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
729
|
+
const someday = { label: 'Someday', icon: 'moon', fn: (r) => somedayAct(inst, item, r) };
|
|
730
|
+
const inToday = { label: 'In Today', icon: 'sun', fn: (r) => todayAct(inst, item, r) };
|
|
731
|
+
if (lens === 'inbox' || lens === 'upcoming') return { right: inToday, left: someday };
|
|
732
|
+
if (lens === 'today') return { right: { label: 'Done', icon: 'check', fn: (r) => doneAct(inst, item, r, 'Done.') },
|
|
733
|
+
left: item.kind === 'offline' ? { label: 'Not yet', icon: 'moon', fn: (r) => notYetAct(inst, item, r, 'tomorrow') } : { label: 'Tomorrow', icon: 'moon', fn: (r) => deferAct(inst, item, r, addIso(today, 1)) } };
|
|
631
734
|
if (lens === 'waiting') return {
|
|
632
735
|
right: { label: 'Nudge', icon: 'out', fn: () => say({ text: 'No channel connected: a nudge to ' + (item.waiting_on || 'them') + ' is not sent.', timeout: 4000 }) },
|
|
633
736
|
left: { label: 'Keep waiting', icon: 'wait', fn: () => say({ text: 'Still waiting.', timeout: 2500 }) } };
|
|
634
|
-
|
|
635
|
-
return { right: { label: 'To Week', icon: 'cal', fn: (r) => moveAct(inst, item, r, 'week', 'Promoted to Week.') }, left: { label: 'Drop', icon: 'trash', fn: (r) => dropAct(inst, item, r) } };
|
|
737
|
+
return { right: { label: 'Bring back', icon: 'sun', fn: (r) => unparkAct(inst, item, r) }, left: { label: 'Drop', icon: 'trash', fn: (r) => dropAct(inst, item, r) } };
|
|
636
738
|
}
|
|
637
739
|
|
|
638
740
|
/* Open or close a case file in place with c3-expand.js, so the mockup's .xp height transition (grid-template-rows) plays. */
|
|
@@ -659,6 +761,19 @@ const KIND_ICON = { reply: 'reply', decide: 'scale', review: 'eye', do: 'check'
|
|
|
659
761
|
/* The mockup's time-left bar (.fuse) with its text, for an item that carries `left` ({ text, pct, tone }). */
|
|
660
762
|
const fuseBar = (l) => { const c = l.tone === 'warn' ? 'var(--warning)' : 'var(--acc)'; return el('div', { class: 'chips2', style: 'margin-top:6px;gap:8px' }, el('div', { class: 'fuse' }, el('i', { style: 'width:' + Math.round(l.pct * 100) + '%;background:' + c })), el('span', { class: 'mono', style: 'font-size:var(--fs-1);color:' + c }, l.text)); };
|
|
661
763
|
|
|
764
|
+
/* The chips the Work page adds to a row: outside PMOS and the person, Not before, Flagged, how often it moved, Looks done, the team task it advances. */
|
|
765
|
+
function workChips(inst, item) {
|
|
766
|
+
const today = inst.data.today; const c = [];
|
|
767
|
+
if (item.kind === 'offline') c.push(el('span', { class: 'tag wk-off', 'data-pm-offline-tag': item.id }, COPY.offline), el('span', { class: 'chip2' }, c3ic('user', 'sm'), item.person || 'someone'));
|
|
768
|
+
if (item.defer && item.defer > today) c.push(el('span', { class: 'tag wk-nb', 'data-pm-notbefore': item.id }, COPY.notBeforeChip(dayLabel(item.defer))));
|
|
769
|
+
if (item.flagged) c.push(el('span', { class: 'chip2 wk-fl', 'data-pm-flagged': item.id }, c3ic('flag', 'sm'), COPY.flagged));
|
|
770
|
+
if (item.due && String(item.due).slice(0, 10) < today) c.push(el('span', { class: 'st bad' }, c3ic('alert', 'sm'), COPY.overdue(daysBetween(item.due, today))));
|
|
771
|
+
if (item.not_yet_count) c.push(el('span', { class: 'tag' }, COPY.moved(item.not_yet_count)));
|
|
772
|
+
if (item.kind === 'offline' && item.evidence) c.push(el('span', { class: 'st warn', 'data-pm-looks-done': item.id }, c3ic('eye', 'sm'), COPY.looksDone));
|
|
773
|
+
if (item.resolves) c.push(el('span', { class: 'chip2', title: RESOLVES_TIP, 'data-pm-resolves': item.resolves }, c3ic('init', 'sm'), COPY.advances(resolvesTitle(inst, item))));
|
|
774
|
+
return c;
|
|
775
|
+
}
|
|
776
|
+
|
|
662
777
|
/* The mockup's workRow(): li.nrow.wrow > .rv.l, .rv.r (made by the swipe), .ki, .nb (.tt, .chips, .xp > .xi), and the .sg glyphs (line 3258). */
|
|
663
778
|
function itemRow(inst, item, lens) {
|
|
664
779
|
const j = item.job && jobById(inst, item.job);
|
|
@@ -673,9 +788,10 @@ function itemRow(inst, item, lens) {
|
|
|
673
788
|
if (mock && item.agent_move && !(rs && rs.phase === 'done')) chips.push(el('span', { class: 'chip2', style: 'gap:4px', title: 'Swipe right to let the agent act' }, c3ic('spark', 'sm'), 'agent move'));
|
|
674
789
|
if (mock && item.mins) chips.push(el('span', { class: 'chip2', title: 'Effort' }, c3ic('clock', 'sm'), item.mins <= 2 ? 'under 2 min' : item.mins + ' min'));
|
|
675
790
|
if (mock && item.time) chips.push(el('span', { class: 'chip2' }, c3ic('cal', 'sm'), item.time));
|
|
676
|
-
if (item.due
|
|
677
|
-
if (item.priority
|
|
791
|
+
if (item.due) chips.push(el('span', { class: 'chip2' }, c3ic('cal', 'sm'), item.due));
|
|
792
|
+
if (item.priority) chips.push(el('span', { class: 'fl ' + item.priority.toLowerCase() }, item.priority));
|
|
678
793
|
if (item.waiting_on) chips.push(el('span', { class: 'chip2' }, c3ic('wait', 'sm'), COPY.waitingOn(item.waiting_on)));
|
|
794
|
+
chips.push(...workChips(inst, item));
|
|
679
795
|
if ((item.steps || []).length) { const dn = item.steps.filter((s) => s.done).length; const ag = item.steps.filter((s) => s.who !== 'you' && !s.done).length; chips.push(el('span', { class: 'chip2', title: 'Steps' }, c3ic('tree', 'sm'), dn + ' of ' + item.steps.length + ' steps' + (ag ? ' · agent can do ' + ag : ''))); }
|
|
680
796
|
if (j) chips.push(el('span', { class: 'chip2 jc', title: j.want }, c3ic('target', 'sm'), mock && j.mock_name ? j.mock_name : j.want));
|
|
681
797
|
if (mock && item.src) chips.push(el('span', { class: 'tag' }, item.src));
|
|
@@ -683,10 +799,10 @@ function itemRow(inst, item, lens) {
|
|
|
683
799
|
const title = el('span', { class: 'tt', 'data-pm-title': item.id }, item.title);
|
|
684
800
|
const spec = swipeSpec(inst, item, lens);
|
|
685
801
|
const recL = itemCase(inst, item, lens).model.opts.find((o) => o.rec);
|
|
686
|
-
const recSide = agent ? 'r' : (recL && /^(drop|
|
|
802
|
+
const recSide = agent ? 'r' : (recL && /^(drop|keep|wait|defer|someday)/.test(recL.id) ? 'l' : 'r');
|
|
803
|
+
const icon = mock ? (KIND_ICON[item.kind] || 'check') : item.kind === 'offline' ? 'user' : item.flagged ? 'flag' : item.priority === 'P1' ? 'alert' : 'todo';
|
|
687
804
|
const row = el('li', { class: 'nrow wrow', 'data-row': '', 'data-id': item.id, 'data-w': '1', 'data-pm-item': item.id, 'data-lens': lens },
|
|
688
|
-
el('span', { class: 'ki' + (item.priority === 'P1' ? ' hot' : ''), title: item.
|
|
689
|
-
c3ic(mock ? (item.mins <= 2 && item.kind !== 'fyi' ? 'bolt' : (KIND_ICON[item.kind] || (item.kind === 'fyi' ? 'info' : 'check'))) : item.priority === 'P1' ? 'alert' : 'todo')),
|
|
805
|
+
el('span', { class: 'ki' + (item.priority === 'P1' || item.flagged ? ' hot' : ''), title: item.kind === 'offline' ? 'Happens outside PMOS' : item.priority || '' }, c3ic(icon)),
|
|
690
806
|
el('div', { class: 'nb' }, title, mock && item.left ? fuseBar(item.left) : null, chips.length ? el('div', { class: 'chips' }, chips) : null),
|
|
691
807
|
el('span', { class: 'sg', title: agent ? 'Drag right: the agent acts. Drag left: reply.' : 'Drag right: ' + spec.right.label + '. Drag left: ' + spec.left.label + '.' },
|
|
692
808
|
el('span', { class: 'g l' + (recSide === 'l' ? ' y' : '') }, c3ic(spec.left.icon)), el('span', { class: 'g r' + (recSide === 'r' ? ' y' : '') }, c3ic(spec.right.icon))));
|
|
@@ -709,6 +825,87 @@ function itemRow(inst, item, lens) {
|
|
|
709
825
|
return row;
|
|
710
826
|
}
|
|
711
827
|
|
|
828
|
+
/* ── Focus: up to three jobs for today, with the reason (PRD R4) ──────────────────────────────────────────────────── */
|
|
829
|
+
|
|
830
|
+
function focusCard(inst) {
|
|
831
|
+
const F = inst.data.focus || { suggestions: [], accepted: [] };
|
|
832
|
+
const acc = F.accepted || []; const sug = (F.suggestions || []).slice(0, 3);
|
|
833
|
+
const one = (f, pinned) => {
|
|
834
|
+
const editing = inst.focEdit.has(f.key);
|
|
835
|
+
const input = editing ? el('input', { type: 'text', 'aria-label': COPY.editTitle, 'data-pm-focus-input': f.key, autocomplete: 'off' }) : null;
|
|
836
|
+
if (input) input.value = f.title;
|
|
837
|
+
const save = () => focusAccept(inst, { ...f, title: (input && input.value.trim()) || f.title });
|
|
838
|
+
const cancel = () => { inst.focEdit.delete(f.key); draw(inst); };
|
|
839
|
+
const edit = () => { inst.focEdit.add(f.key); draw(inst); later(inst, () => { const i = inst.root.querySelector('[data-pm-focus-input="' + f.key + '"]'); if (i) i.focus(); }, 30); };
|
|
840
|
+
if (input) input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) save(); else if (e.key === 'Escape') cancel(); });
|
|
841
|
+
const job = f.job && jobById(inst, f.job);
|
|
842
|
+
const refs = (f.item_ids || []).map((id) => itemById(inst, id)).filter((i) => i && !i.done && i.title !== f.title); // a one-item job does not list itself
|
|
843
|
+
const btns = editing ? [pill(COPY.save, 'check', { class: 'pill pri', 'data-pm-focus-save': f.key, onclick: save }), pill(COPY.cancel, null, { 'data-pm-focus-cancel': f.key, onclick: cancel })]
|
|
844
|
+
: pinned ? [pill(COPY.editTitle, 'edit', { 'data-pm-focus-edit': f.key, onclick: edit }), pill(COPY.unpin, 'x', { 'data-pm-focus-clear': f.key, onclick: () => focusClear(inst, f) })]
|
|
845
|
+
: [pill(COPY.accept, 'check', { class: 'pill pri', 'data-pm-focus-accept': f.key, onclick: () => focusAccept(inst, f) }), pill(COPY.editTitle, 'edit', { 'data-pm-focus-edit': f.key, onclick: edit }),
|
|
846
|
+
pill(COPY.notToday, 'moon', { 'data-pm-focus-dismiss': f.key, onclick: () => focusDismiss(inst, f) })];
|
|
847
|
+
return el('div', { class: 'wkfc' + (pinned ? ' on' : ''), 'data-pm-focus-card': f.key, 'data-pm-focus-state': pinned ? 'accepted' : 'suggested' },
|
|
848
|
+
el('div', { class: 'chips2' }, el('span', { class: 'tag' + (pinned ? ' wk-on' : '') }, pinned ? COPY.focusPinned : COPY.focusSuggested),
|
|
849
|
+
job ? el('span', { class: 'chip2' }, c3ic('target', 'sm'), job.mock_name || job.want) : null),
|
|
850
|
+
editing ? el('span', { class: 'form' }, input) : el('b', { class: 'wkfc-t', 'data-pm-focus-title': f.key }, f.title),
|
|
851
|
+
f.why ? el('p', { class: 'wkfc-why' }, COPY.why(f.why)) : null,
|
|
852
|
+
refs.length ? el('ul', { class: 'wkfc-r', role: 'list' }, refs.map((i) => el('li', null, c3ic('next', 'sm'), el('span', null, i.title)))) : null,
|
|
853
|
+
el('div', { class: 'wkq' }, btns));
|
|
854
|
+
};
|
|
855
|
+
return el('section', { class: 'card sect wkfoc', 'data-pm-focus': '' },
|
|
856
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('target', 'sm'), COPY.focus), el('span', { class: 'dim wk-s' }, COPY.focusSub)),
|
|
857
|
+
acc.length || sug.length ? el('div', { class: 'wkfg' }, ...acc.map((f) => one(f, true)), ...sug.map((f) => one(f, false))) : el('div', { class: 'dim mono wk-s', 'data-pm-focus-none': '' }, COPY.focusNone));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* ── Upcoming: the next 7 days by day, then Later (PRD R5) ─────────────────────────────────────────────────────────── */
|
|
861
|
+
|
|
862
|
+
function upcomingMain(inst, list) {
|
|
863
|
+
const today = inst.data.today;
|
|
864
|
+
const block = (key, label, sub, its) => el('div', { class: 'wkday', 'data-pm-day': key },
|
|
865
|
+
el('h3', null, label, sub ? el('span', { class: 'dim' }, sub) : null, its.length ? el('span', { class: 'bdg g' }, String(its.length)) : null),
|
|
866
|
+
its.length ? el('ul', { class: 'list', role: 'list' }, its.map((i) => itemRow(inst, i, 'upcoming'))) : el('div', { class: 'wkday-none' }, 'Nothing'));
|
|
867
|
+
const at = (i) => { const a = arrives(i, today); return a ? Math.max(1, daysBetween(today, a)) : 99; };
|
|
868
|
+
const days = [];
|
|
869
|
+
for (let n = 1; n <= 7; n++) { const d = addIso(today, n); days.push(block(d, n === 1 ? 'Tomorrow' : dayLabel(d).slice(0, 3), n === 1 ? dayLabel(d).slice(4) : dayLabel(d).slice(4), list.filter((i) => at(i) === n))); }
|
|
870
|
+
days.push(block('later', 'Later', 'beyond 7 days', list.filter((i) => at(i) > 7)));
|
|
871
|
+
const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic('cal', 'sm'), labelOf('upcoming'), ' ', el('span', { class: 'bdg g' }, String(list.length)))), el('div', { class: 'wkdays' }, days));
|
|
872
|
+
sec.setAttribute('data-pm-list', 'upcoming');
|
|
873
|
+
return sec;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* ── Review: a weekly pass, one item at a time (PRD R7) ────────────────────────────────────────────────────────────── */
|
|
877
|
+
|
|
878
|
+
function reviewMain(inst) {
|
|
879
|
+
const today = inst.data.today; const q = shown(inst, 'review'); const R = inst.rv;
|
|
880
|
+
const inQ = new Set(q.map((i) => i.id));
|
|
881
|
+
if (!R.ids) { R.ids = q.map((i) => i.id); R.handled = new Set(); R.changing = false; }
|
|
882
|
+
q.forEach((i) => { if (!R.ids.includes(i.id)) R.ids.push(i.id); });
|
|
883
|
+
const curId = R.ids.find((id) => !R.handled.has(id) && inQ.has(id)) || null;
|
|
884
|
+
const total = R.ids.length; const doneN = R.ids.filter((id) => R.handled.has(id) || !inQ.has(id)).length;
|
|
885
|
+
const restart = () => { R.ids = null; draw(inst); };
|
|
886
|
+
if (!curId) {
|
|
887
|
+
return el('section', { class: 'card sect wkrv', 'data-pm-review-done': '' }, el('p', { class: 'wkrv-t' }, total ? COPY.reviewDone : labelOf('review')),
|
|
888
|
+
el('p', { class: 'wk-s' }, total ? COPY.reviewDoneText(total) : COPY.reviewNone),
|
|
889
|
+
q.length ? el('div', { class: 'wkq' }, pill(COPY.again, 'undo', { 'data-pm-review-again': '', onclick: restart })) : null);
|
|
890
|
+
}
|
|
891
|
+
const item = q.find((i) => i.id === curId);
|
|
892
|
+
const next = () => { R.handled.add(curId); R.changing = false; draw(inst); };
|
|
893
|
+
const place = workPlace(item, today);
|
|
894
|
+
const upNext = R.ids.filter((id) => id !== curId && !R.handled.has(id) && inQ.has(id)).slice(0, 2).map((id) => (q.find((i) => i.id === id) || {}).title).filter(Boolean);
|
|
895
|
+
return el('section', { class: 'card sect wkrv', 'data-pm-review': curId },
|
|
896
|
+
el('div', { class: 'wkrv-p' }, el('span', { 'data-pm-review-progress': '' }, (doneN + 1) + ' of ' + total), el('span', null, COPY.reviewHead)),
|
|
897
|
+
el('progress', { class: 'wk-bar', value: String(doneN), max: String(Math.max(1, total)), 'aria-label': doneN + ' of ' + total + ' reviewed' }), // Review's progress bar
|
|
898
|
+
el('p', { class: 'wkrv-t', 'data-pm-title': curId }, item.title),
|
|
899
|
+
el('div', { class: 'chips' }, el('span', { class: 'tag' }, COPY.untouched(untouchedDays(item, today))), el('span', { class: 'tag' }, COPY.livesIn(labelOf(place))),
|
|
900
|
+
item.waiting_on ? el('span', { class: 'chip2' }, c3ic('wait', 'sm'), COPY.waitingOn(item.waiting_on)) : null, ...workChips(inst, item)),
|
|
901
|
+
R.changing ? el('div', { class: 'wkrv-x' }, editForm(inst, item), el('div', { class: 'wkq' }, pill(COPY.next, 'next', { class: 'pill pri', 'data-pm-review-next': curId, onclick: next })))
|
|
902
|
+
: el('div', { class: 'wkq' },
|
|
903
|
+
pill(COPY.keep, 'check', { class: 'pill pri', 'data-pm-review-keep': curId, onclick: async () => { if (await act(inst, null, { route: '/keep', body: { id: curId }, text: COPY.kept, leave: false })) { R.handled.add(curId); draw(inst); } } }),
|
|
904
|
+
pill(COPY.change, 'edit', { 'data-pm-review-change': curId, onclick: () => { R.changing = true; draw(inst); } }),
|
|
905
|
+
pill(COPY.drop, 'trash', { 'data-pm-review-drop': curId, onclick: async () => { if (await dropAct(inst, item, null)) { R.handled.add(curId); draw(inst); } } })),
|
|
906
|
+
upNext.length ? el('p', { class: 'dim wk-s' }, COPY.upNext + upNext.join(' · ')) : null);
|
|
907
|
+
}
|
|
908
|
+
|
|
712
909
|
/* ── jobs ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
713
910
|
|
|
714
911
|
function jobEditField(inst, job, key) {
|
|
@@ -724,44 +921,35 @@ function jobEditField(inst, job, key) {
|
|
|
724
921
|
return field(lab, input);
|
|
725
922
|
}
|
|
726
923
|
|
|
727
|
-
/* The mockup's c3jall(): "All N items, across every
|
|
924
|
+
/* The mockup's c3jall(): "All N items, across every view", grouped by view (line 4657). */
|
|
728
925
|
function jobAll(inst, job) {
|
|
729
|
-
const fx2 = DEMO() && window.__c3work2 && window.__c3work2.jobAll && window.__c3work2.jobAll[job.id]; // demo: the mockup's own items of the job, grouped by list
|
|
730
|
-
if (fx2) {
|
|
731
|
-
const n = fx2.reduce((a, g) => a + g[1].length, 0);
|
|
732
|
-
return el('div', { class: 'c3all', 'data-pm-job-all': job.id }, el('div', { class: 'hd dim mono' }, 'All ' + n + ' items, across every list'),
|
|
733
|
-
...fx2.map(([tag, titles, dn]) => el('div', null, el('span', { class: 'tag' }, tag), el('span', null, ...titles.map((t) => el('span', { class: dn ? 'dn' : '' }, t))))));
|
|
734
|
-
}
|
|
735
926
|
const its = (inst.data.items || []).filter((i) => i.job === job.id);
|
|
736
927
|
if (!its.length) return el('div', { class: 'dim mono', style: small }, 'No item uses this job yet.');
|
|
737
928
|
const G = {};
|
|
738
929
|
its.forEach((i) => { const l = i.done ? 'done' : lensOfItem(inst, i.id); (G[l] = G[l] || []).push(i); });
|
|
739
|
-
return el('div', { class: 'c3all', 'data-pm-job-all': job.id }, el('div', { class: 'hd dim mono' }, 'All ' + its.length + ' items, across every
|
|
740
|
-
...[...
|
|
930
|
+
return el('div', { class: 'c3all', 'data-pm-job-all': job.id }, el('div', { class: 'hd dim mono' }, 'All ' + its.length + ' items, across every view'),
|
|
931
|
+
...[...VIEWS, 'done'].filter((l) => G[l]).map((l) => el('div', null, el('span', { class: 'tag' }, l === 'done' ? 'Done' : labelOf(l)), el('span', null, ...G[l].map((i) => el('span', { class: i.done ? 'dn' : '' }, i.title))))));
|
|
741
932
|
}
|
|
742
933
|
|
|
743
934
|
/* The job's case file (mockup C3.cases.job, line 4660): the words, the count, Keep / Delete and move / Delete and leave, slide to confirm. */
|
|
744
935
|
function jobCase(inst, j) {
|
|
745
936
|
const its = (inst.data.items || []).filter((i) => i.job === j.id);
|
|
746
937
|
const done = its.filter((i) => i.done).length;
|
|
747
|
-
const mk = !!j.mock_name; // demo: the mockup's own numbers, wording and Recommended badge
|
|
748
938
|
const others = (inst.data.jobs || []).filter((q) => q.id !== j.id); const near = others[0];
|
|
749
939
|
const nm = (q) => q.mock_name || q.want;
|
|
750
940
|
const del = (reassign) => async () => { const r = await press(inst, JOBS + '/delete', { id: j.id, ...(reassign ? { reassign_to: reassign } : {}) }); if (r) say({ text: COPY.reassigned(r.reassigned || 0), timeout: 4000 }); };
|
|
751
|
-
const model = { id: 'job:' + j.id, sit: COPY.jobLine(j), conf: 90, cta:
|
|
752
|
-
chg: [['target',
|
|
941
|
+
const model = { id: 'job:' + j.id, sit: COPY.jobLine(j), conf: 90, cta: 'Slide to keep it',
|
|
942
|
+
chg: [['target', done + ' of ' + its.length + ' done', 'count across every view', 'ok'], ['info', 'Change the words below. They save at once and you can undo for 7 seconds.', '']],
|
|
753
943
|
opts: [{ id: 'keep', l: 'Keep it', rec: 1, fx: [fx('check', 'Nothing changes', 'ok')] },
|
|
754
|
-
{ id: 'mv', l: near ? 'Delete it and move its ' +
|
|
755
|
-
{ id: 'nj', l: 'Delete it and leave its items without a job', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('alert',
|
|
944
|
+
{ id: 'mv', l: near ? 'Delete it and move its ' + its.length + ' item' + (its.length === 1 ? '' : 's') + ' to "' + nm(near) + '"' : 'Delete it', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('target', its.length + ' items change job', 'warn')] },
|
|
945
|
+
{ id: 'nj', l: 'Delete it and leave its items without a job', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('alert', 'The items have no job', '')] }] };
|
|
756
946
|
const run = { keep: () => say({ text: 'Kept.', timeout: 2500 }), mv: del(near && near.id), nj: del(null) };
|
|
757
947
|
const root = renderCase(model, { onConfirm: ({ option }) => { if (run[option.id]) run[option.id](); } });
|
|
758
948
|
root.setAttribute('data-pm-case', j.id);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
|
|
762
|
-
}
|
|
949
|
+
root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); }); // a job carries no confidence figure: never show a made-up percentage
|
|
950
|
+
root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
|
|
763
951
|
const sit = root.querySelector('.sit');
|
|
764
|
-
sit.after(el('div', { class: 'c3jf' },
|
|
952
|
+
sit.after(el('div', { class: 'c3jf' }, j.mock_name ? jobEditField(inst, j, 'mock_name') : null, jobEditField(inst, j, 'when'), jobEditField(inst, j, 'want'), jobEditField(inst, j, 'so')), jobAll(inst, j));
|
|
765
953
|
return root;
|
|
766
954
|
}
|
|
767
955
|
|
|
@@ -823,36 +1011,41 @@ function jobsCard(inst) {
|
|
|
823
1011
|
|
|
824
1012
|
/* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
825
1013
|
|
|
826
|
-
/* The mockup's workHead() calbar: the .seg
|
|
1014
|
+
/* The mockup's workHead() calbar: the .seg view bar with a count on every view, and the group toggle as an .ib (line 3281). */
|
|
827
1015
|
function lensBar(inst) {
|
|
828
|
-
const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Work
|
|
1016
|
+
const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Work view', 'data-pm-lenses': '' },
|
|
829
1017
|
LENSES.map(([id, label]) => {
|
|
830
|
-
const n = inst.data ?
|
|
1018
|
+
const n = inst.data ? countOf(inst, id) : 0;
|
|
831
1019
|
const b = el('button', { type: 'button', 'aria-pressed': String(inst.lens === id), 'data-lens': id }, c3ic(WLI[id], 'sm'), label, n ? el('span', { class: 'bdg g' }, String(n)) : null);
|
|
832
1020
|
b.addEventListener('click', () => { inst.lens = id; if (inst.ctx.onLens) inst.ctx.onLens(id); draw(inst); }); // onLens: the page's deck follows the lens (demo)
|
|
1021
|
+
/* every view shows its count, also 0 */
|
|
1022
|
+
const badge = b.querySelector('.bdg') || b.appendChild(el('span', { class: 'bdg g' }, '0'));
|
|
1023
|
+
badge.setAttribute('data-pm-count', id);
|
|
833
1024
|
return b;
|
|
834
1025
|
}));
|
|
835
|
-
const grp = c3ib(inst.group ? COPY.groupTime : COPY.groupJob, 'target', { 'aria-pressed': String(inst.group), 'data-pm-group': '' }, 'sm');
|
|
1026
|
+
const grp = c3ib(inst.group ? COPY.groupTime : COPY.groupJob, 'target', { 'aria-pressed': String(inst.group), 'data-pm-group': '', disabled: inst.lens === 'review' }, 'sm');
|
|
836
1027
|
grp.addEventListener('click', () => { inst.group = !inst.group; draw(inst); });
|
|
837
1028
|
return [seg, el('div', { class: 'chips2' }, grp)];
|
|
838
1029
|
}
|
|
839
1030
|
|
|
840
|
-
/* The ring at the right of
|
|
841
|
-
function
|
|
842
|
-
const
|
|
843
|
-
|
|
1031
|
+
/* The ring at the right of Today's header: done today of done today plus what is still in Today. */
|
|
1032
|
+
function todayRing(inst) {
|
|
1033
|
+
const d = inst.data;
|
|
1034
|
+
const doneToday = (d.items || []).filter((i) => i.done && String(i.done_at || '').slice(0, 10) === d.today).length;
|
|
1035
|
+
return c3ring(doneToday, doneToday + shown(inst, 'today').length, 44);
|
|
844
1036
|
}
|
|
845
1037
|
|
|
846
1038
|
/* A .card.sect with an .sh2 title row and a ul.list of rows (line 3262). */
|
|
847
1039
|
function sect(head, rows) { return el('section', { class: 'card sect' }, head, rows); }
|
|
848
1040
|
|
|
849
|
-
const FOCUS_ATTRS = ['data-pm-edit', 'data-pm-step-add', 'data-pm-job-edit', 'data-pm-agent-reply', 'data-pm-job-new', '
|
|
1041
|
+
const FOCUS_ATTRS = ['data-pm-edit', 'data-pm-step-add', 'data-pm-job-edit', 'data-pm-agent-reply', 'data-pm-job-new', 'data-pm-outcome', 'data-pm-wait-on', 'data-pm-focus-input'];
|
|
850
1042
|
|
|
851
1043
|
function draw(inst) {
|
|
1044
|
+
if (inst.lens !== inst.prevLens) { if (inst.lens === 'review') inst.rv.ids = null; inst.prevLens = inst.lens; } // a fresh Review pass each time the view is opened
|
|
852
1045
|
/* keep the focus: a field the PM is typing in, or the row they were on, is found again after the redraw */
|
|
853
1046
|
const a = document.activeElement; let keep = null;
|
|
854
1047
|
if (a && inst.root.contains(a) && a !== document.body) {
|
|
855
|
-
const at = FOCUS_ATTRS.
|
|
1048
|
+
const at = FOCUS_ATTRS.find((k) => a.hasAttribute && a.hasAttribute(k));
|
|
856
1049
|
if (at) keep = { sel: '[' + at + '="' + a.getAttribute(at) + '"]' };
|
|
857
1050
|
else if (a.matches && a.matches('.nrow[data-pm-item]')) keep = { sel: '[data-pm-item="' + a.getAttribute('data-pm-item') + '"]' };
|
|
858
1051
|
}
|
|
@@ -860,35 +1053,34 @@ function draw(inst) {
|
|
|
860
1053
|
inst.bar.replaceChildren(...lensBar(inst));
|
|
861
1054
|
const d = inst.data;
|
|
862
1055
|
if (!d) { inst.body.replaceChildren(el('p', { class: 'dim mono', style: small }, COPY.loading)); return; }
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
el('
|
|
867
|
-
|
|
868
|
-
|
|
1056
|
+
const lens = inst.lens;
|
|
1057
|
+
const list = lens === 'review' ? [] : shown(inst, lens);
|
|
1058
|
+
const lh = el('div', { class: 'lh', 'data-pm-lens-head': lens },
|
|
1059
|
+
el('span', { class: 'ki' }, c3ic(WLI[lens])),
|
|
1060
|
+
el('div', { class: 'wk-min' }, el('b', null, COPY.lens[lens](countOf(inst, lens))), el('div', { class: 'dim' }, COPY.lensSub[lens])),
|
|
1061
|
+
lens === 'today' ? todayRing(inst) : null);
|
|
1062
|
+
const empty = el('li', { class: 'fold' }, c3ic('check', 'sm'), COPY.emptyView[lens] || COPY.empty);
|
|
869
1063
|
let main;
|
|
870
|
-
if (inst
|
|
1064
|
+
if (lens === 'review') main = [reviewMain(inst)];
|
|
1065
|
+
else if (inst.group) {
|
|
871
1066
|
const groups = (d.jobs || []).map((j) => [j, list.filter((i) => i.job === j.id)])
|
|
872
1067
|
.concat([[null, list.filter((i) => !i.job || !jobById(inst, i.job))]]).filter((g) => g[1].length);
|
|
873
1068
|
main = groups.length ? groups.map(([j, its]) => {
|
|
874
1069
|
const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic('target', 'sm'), j ? (j.mock_name || j.want) : COPY.noJobGroup),
|
|
875
1070
|
j ? el('span', { class: 'chips2 dim mono', style: small }, COPY.progress(j.done, j.total)) : null),
|
|
876
|
-
el('ul', { class: 'list', role: 'list' }, its.map((i) => itemRow(inst, i,
|
|
1071
|
+
el('ul', { class: 'list', role: 'list' }, its.map((i) => itemRow(inst, i, lens))));
|
|
877
1072
|
sec.setAttribute('data-pm-group-card', j ? j.id : '');
|
|
878
1073
|
return sec;
|
|
879
|
-
}) : [sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[
|
|
880
|
-
} else
|
|
881
|
-
|
|
1074
|
+
}) : [sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[lens], 'sm'), labelOf(lens))), el('ul', { class: 'list' }, empty))];
|
|
1075
|
+
} else if (lens === 'upcoming') main = [upcomingMain(inst, list)];
|
|
1076
|
+
else {
|
|
1077
|
+
const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[lens], 'sm'), labelOf(lens), ' ', el('span', { class: 'bdg g' }, String(list.length))),
|
|
882
1078
|
el('span', { class: 'fl2', 'data-pm-legend': '' }, COPY.legend.map((w, k) => el('span', null, ['• ', '× ', '> ', '< '][k] + w)))),
|
|
883
|
-
el('ul', { class: 'list', role: 'list' }, list.length ? list.map((i) => itemRow(inst, i,
|
|
884
|
-
sec.setAttribute('data-pm-list',
|
|
1079
|
+
el('ul', { class: 'list', role: 'list' }, list.length ? list.map((i) => itemRow(inst, i, lens)) : empty));
|
|
1080
|
+
sec.setAttribute('data-pm-list', lens);
|
|
885
1081
|
main = [sec];
|
|
886
1082
|
}
|
|
887
|
-
if (
|
|
888
|
-
const nz = d.noise.map((n, i) => ({ id: n.id || 'noise-' + (n.title || i).toLowerCase().replace(/[^a-z0-9]+/g, '-'), title: n.title, src: n.src, kind: 'fyi', mins: 0, mock_row: true, noise: true, steps: [] }));
|
|
889
|
-
main.push(el('section', { class: 'card sect', 'data-pm-noise': '' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('info', 'sm'), 'Noise ', el('span', { class: 'bdg g' }, String(nz.length))), el('span', { class: 'dim', style: small }, 'kept out of your list')),
|
|
890
|
-
el('ul', { class: 'list', role: 'list' }, nz.map((n) => itemRow(inst, n, 'triage')))));
|
|
891
|
-
}
|
|
1083
|
+
if (lens === 'today') main.unshift(focusCard(inst));
|
|
892
1084
|
/* The mockup's vWork() body: .calbody > .lh + .brief.sbb > .bcols > .bcol.l (the list) and .bcol.r (the jobs card). */
|
|
893
1085
|
inst.body.replaceChildren(lh, el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, inst.ctx.plan || null, ...main), el('div', { class: 'bcol r' }, jobsCard(inst)))));
|
|
894
1086
|
if (keep) { const n = inst.root.querySelector(keep.sel); if (n && n.focus) { try { n.focus({ preventScroll: true }); } catch { /* not focusable */ } } }
|