create-pmos 0.514.0 → 0.516.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 +35 -95
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/briefing-premium-calendar-test.js +6 -3
- package/workspace/briefing-premium-insights-test.js +12 -12
- package/workspace/briefing-premium-metrics-test.js +1 -1
- package/workspace/changelog.json +34 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/agent-capabilities.md +73 -0
- package/workspace/guide/index.md +4 -1
- package/workspace/guide/intelligence-okr-goals.md +39 -0
- package/workspace/guide/reports-needs-you.md +34 -0
- package/workspace/lib/board-metrics-premium.js +4 -1
- package/workspace/lib/insights-goals.js +3 -1
- package/workspace/lib/insights-premium.js +53 -7
- package/workspace/lib/okr-goals.js +200 -0
- package/workspace/lib/report-acts.js +189 -0
- package/workspace/lib/routes-insights-premium.js +44 -5
- package/workspace/lib/routes-ready-reports.js +8 -1
- package/workspace/lib/routes-report-setups.js +21 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +9 -5
- package/workspace/public/pm/cozy-insights.css +60 -1
- package/workspace/public/pm/home-app.js +3 -1
- package/workspace/public/pm/premium-insights.js +269 -36
- package/workspace/public/premium-intelligence.html +1 -1
- package/workspace/test-files.json +1 -0
- package/workspace/test-lists/reports-needs-you.json +5 -0
|
@@ -134,12 +134,13 @@ function periods() {
|
|
|
134
134
|
const d = new Date(today()), y = d.getUTCFullYear(), q = Math.floor(d.getUTCMonth() / 3), h = d.getUTCMonth() < 6 ? 0 : 1;
|
|
135
135
|
const span = (y0, m0, y1, m1) => ({ from: Date.UTC(y0, m0, 1), to: Date.UTC(y1, m1, 0) });
|
|
136
136
|
const lq = q ? { y, q: q - 1 } : { y: y - 1, q: 3 };
|
|
137
|
-
|
|
137
|
+
const F = M && M.okrFile && M.okrFile.period; // Intelligence redesign A: the OKR file's own period opens first (H2 2026 for IDPF)
|
|
138
|
+
return (F ? [{ key: 'okr', label: F.label, short: F.short, name: 'The OKR period', said: 'this OKR period', from: F.from, to: F.to }] : []).concat([
|
|
138
139
|
Object.assign({ key: 'q', label: 'Q' + (q + 1) + ' ' + y, short: 'Q' + (q + 1), name: 'This quarter', said: 'this quarter' }, span(y, q * 3, y, q * 3 + 3)),
|
|
139
140
|
Object.assign({ key: 'lq', label: 'Q' + (lq.q + 1) + ' ' + lq.y, short: 'Q' + (lq.q + 1), name: 'Last quarter', said: 'last quarter' }, span(lq.y, lq.q * 3, lq.y, lq.q * 3 + 3)),
|
|
140
141
|
Object.assign({ key: 'h', label: 'H' + (h + 1) + ' ' + y, short: 'H' + (h + 1), name: 'This half', said: 'this half' }, span(y, h * 6, y, h * 6 + 6)),
|
|
141
142
|
Object.assign({ key: 'y', label: String(y), short: String(y), name: 'This year', said: 'this year' }, span(y, 0, y + 1, 0)),
|
|
142
|
-
];
|
|
143
|
+
]);
|
|
143
144
|
}
|
|
144
145
|
const inPer = (g, p = per) => {
|
|
145
146
|
if (!p) return true;
|
|
@@ -256,10 +257,37 @@ async function openPortfolio(only = pickedWs(), keepTab = false) {
|
|
|
256
257
|
for (const c of m.charts || []) charts[m.board.ws + '~' + c.key] = c;
|
|
257
258
|
for (const g of m.goals || []) goals.push(Object.assign({}, g, { id: m.board.ws + '~' + g.id, rid: g.id, key: g.key ? m.board.ws + '~' + g.key : null, prod: m.board.name || others[i].name, ws: m.board.ws, moves: g.moves || [] }));
|
|
258
259
|
});
|
|
259
|
-
|
|
260
|
+
const rows = [prodRow(M)].concat(got.filter(Boolean).map(prodRow));
|
|
261
|
+
PF = { goals, charts, products, unread, rows };
|
|
260
262
|
dash = 'all'; if (!keepTab) setTab('goals'); redraw();
|
|
261
263
|
if (unread.length) toast('Not read: ' + unread.join(', ') + '. Their goals are not in Portfolio.');
|
|
262
264
|
}
|
|
265
|
+
/** All products (IgA6Portfolio): one row per product, each number from that product's own model: its North Star, its key results read of the
|
|
266
|
+
* OKR file's total, its late initiatives, its outcome checks overdue, and the one next step Claude would take. PURE over a model. */
|
|
267
|
+
function prodRow(m) {
|
|
268
|
+
const F = m.okrFile, ns = (m.goals || []).find((g) => g.role === 'north-star' && !g.program && !g.initiative) || null;
|
|
269
|
+
const late = (m.lateRows || []).length, over = (m.outcomes || []).filter((r) => r.promise && r.promise.overdue).length;
|
|
270
|
+
const wait = F ? Object.entries(F.needs || {}).sort((a, b) => b[1] - a[1]) : [];
|
|
271
|
+
const next = !F ? 'No OKR file is read here, so there is no key result to list.'
|
|
272
|
+
: wait.length ? 'Connect ' + wait[0][0] + ': it unlocks ' + (wait[0][1] === 1 ? '1 key result.' : wait[0][1] + ' key results.')
|
|
273
|
+
: over ? 'Judge the outcome check that is overdue.' : late ? 'Chase the ' + (late === 1 ? 'late initiative.' : late + ' late initiatives.') : '';
|
|
274
|
+
return { ws: m.board.ws, name: m.board.name || m.board.ws, initials: String(m.board.name || '?').split(/\s+/).map((w) => w[0]).join('').slice(0, 2).toUpperCase(),
|
|
275
|
+
ns: ns ? ns.title + (ns.nowTxt ? ' · ' + ns.nowTxt : ' · not read') : 'none', total: F ? F.total : null, read: F ? F.read : null, late, over, next };
|
|
276
|
+
}
|
|
277
|
+
function allProductsHtml() {
|
|
278
|
+
if (!PF || !PF.rows || PF.rows.length < 2) return '';
|
|
279
|
+
const R = PF.rows, cell = (n) => (n ? `<b>${n}</b>` : '<span class="note">—</span>');
|
|
280
|
+
return `<div class="ins-tbl ins-allp" role="table" aria-label="All products"><div class="ins-tr ins-th ins-pcols" role="row"><div role="columnheader">All products <span>${R.length}</span></div><div role="columnheader">North Star</div><div role="columnheader">Key results read</div><div role="columnheader">Late</div><div role="columnheader">Checks overdue</div><div role="columnheader">Next, by Claude</div></div>`
|
|
281
|
+
+ R.map((r) => `<div class="ins-tr ins-row ins-pcols" role="row" data-prod="${esc(r.ws)}" tabindex="0" aria-label="${esc(r.name)}"><div class="nm" role="cell"><span class="ins-pav" aria-hidden="true">${esc(r.initials)}</span><b>${esc(r.name)}</b></div>`
|
|
282
|
+
+ `<div role="cell" class="note">${esc(r.ns)}</div><div role="cell">${r.total === null ? '<span class="note">no OKR file</span>' : `<b>${r.read}</b> <span class="note">of ${r.total}</span>`}</div><div role="cell">${cell(r.late)}</div><div role="cell">${cell(r.over)}</div>`
|
|
283
|
+
+ `<div role="cell">${r.next ? `<span class="ins-dv"><span class="sp" aria-hidden="true">${SV(I.spark)}</span><span>${esc(r.next)}</span></span>` : ''}</div></div>`).join('') + '</div>';
|
|
284
|
+
}
|
|
285
|
+
/** The ✦ line of All products: the totals of the rows, said once. */
|
|
286
|
+
function allProductsLine() {
|
|
287
|
+
const R = (PF && PF.rows) || [], withF = R.filter((r) => r.total !== null), T = withF.reduce((a, r) => a + r.total, 0), rd = withF.reduce((a, r) => a + r.read, 0);
|
|
288
|
+
const late = R.reduce((a, r) => a + r.late, 0), over = R.reduce((a, r) => a + r.over, 0);
|
|
289
|
+
return 'Across your ' + R.length + ' products: ' + [withF.length ? rd + ' of ' + T + ' key results have a reading' : '', late ? late + (late === 1 ? ' initiative is late' : ' initiatives are late') : '', over ? over + (over === 1 ? ' outcome check is overdue' : ' outcome checks are overdue') : ''].filter(Boolean).join(', ') + '.';
|
|
290
|
+
}
|
|
263
291
|
/** With several products ticked, Outcomes and Library are still read one product at a time: say which (principle 7, honest state). */
|
|
264
292
|
const oneNote = () => (PF ? `<p class="note ins-onenote">Shows ${esc(M.board.name || 'the first product')} only: this tab reads one product at a time.</p>` : '');
|
|
265
293
|
const allGoals = () => (PF ? PF.goals : M.goals);
|
|
@@ -441,16 +469,18 @@ function stripList() {
|
|
|
441
469
|
// Your impact (batch 16 item 7, GmA4Moments): a key result that moved after your work, as one more ✦ line, with its source to open
|
|
442
470
|
const im = PF ? null : IM.intelItem(), mine = im ? [im] : [];
|
|
443
471
|
const fcg = PF ? null : fcGrillItem();
|
|
472
|
+
// Intelligence redesign A (IgA1Open): after the first line, the outcome checks that are due and the late initiatives, one at a time
|
|
473
|
+
const more = PF ? [] : ((M.okr && M.okr.more) || []).map((x) => ({ text: x.text, why: x.why || '', outcome: x.outcome || null }));
|
|
444
474
|
const ok = PF ? [] : [Object.assign({ okr: true, text: '', why: '' }, (() => { const L = okrLine(); return { text: L.head, why: L.sub, tone: L.tone, keepAll: L.keepAll, start: !!L.start, retry: !!L.retry }; })())].concat(fcg ? [fcg] : []);
|
|
445
|
-
if (I0.none) return ok.concat(PF ? [] : RI.stripItems(), mine);
|
|
475
|
+
if (I0.none) return ok.concat(more, PF ? [] : RI.stripItems(), mine);
|
|
446
476
|
const finds = [{ text: I0.text, why: I0.why || '', chart: I0.chart, move: I0.move || null, calm: !!I0.calm }].concat((I0.more || []).map((x) => ({ text: x.text, why: x.why || '', chart: x.chart, move: x.move || null })));
|
|
447
477
|
// a calm finding ("Nothing stands out") says what the ✦ line already says: it is left out
|
|
448
|
-
return ok.concat(finds.filter((f) => !(ok.length && f.calm)), PF ? [] : RI.stripItems(), mine);
|
|
478
|
+
return ok.concat(finds.filter((f) => !(ok.length && f.calm)), more, PF ? [] : RI.stripItems(), mine); // the outcome check due and the late work follow the readings' own findings
|
|
449
479
|
}
|
|
450
480
|
function drawStrip() {
|
|
451
481
|
const G = goalsShown(), behind = G.filter((g) => g.tone === 'late' || g.tone === 'red').length;
|
|
452
482
|
$('#insEye').textContent = ''; $('#insAct').innerHTML = ''; $('#insPg').innerHTML = ''; $('#insMv').hidden = true; $('#insMv').innerHTML = ''; $('#insHero').classList.remove('mv', 'crit');
|
|
453
|
-
if (PF) { $('#brief').textContent = 'Portfolio: ' + nGoals(G.length) + ' across ' + PF.products.length + (PF.products.length === 1 ? ' product' : ' products') + (behind ? ', ' + behind + ' behind.' : '.'); $('#insWhy').textContent = 'The finding under it is read one product at a time.'; $('#askWhy').href = homeAsk('insights', 'Which product needs me most this quarter?'); $('#askWhy').hidden = false; return; }
|
|
483
|
+
if (PF) { $('#brief').textContent = PF.rows && PF.rows.length > 1 ? allProductsLine() : 'Portfolio: ' + nGoals(G.length) + ' across ' + PF.products.length + (PF.products.length === 1 ? ' product' : ' products') + (behind ? ', ' + behind + ' behind.' : '.'); $('#insWhy').textContent = 'The finding under it is read one product at a time.'; $('#askWhy').href = homeAsk('insights', 'Which product needs me most this quarter?'); $('#askWhy').hidden = false; return; }
|
|
454
484
|
const L = stripList(), open = L.map((f, i) => i).filter((i) => !insSkip.has(i));
|
|
455
485
|
if (!open.includes(insIdx)) insIdx = open.length ? open[0] : 0;
|
|
456
486
|
const f = L[insIdx];
|
|
@@ -949,7 +979,7 @@ const nowOf = (g) => ((g.failed || (g.held || []).length) && g.lastRead && g.las
|
|
|
949
979
|
function goalRow(g) {
|
|
950
980
|
const sel = selecting && picked.includes(g.id), warn = gcSay(g), crit = OK.dotOf(g) === 'broken';
|
|
951
981
|
return `<div class="ins-tr ins-row ${COLS}${sel ? ' sel' : ''}${crit ? ' crit' : ''}" role="row" data-goal="${esc(g.id)}" tabindex="0" aria-label="${esc(g.title)}"${selecting ? ` aria-selected="${sel}"` : ''}>`
|
|
952
|
-
+ `<div class="nm" role="cell">${crit ? OK.dotHtml('broken') : ''}<span><b>${markHtml(g.title)}</b><small><span class="cz-pill q ins-type">${esc(g.type)}</span><span class="ins-sub" title="${esc(goalSub(g))}">${esc(PF && g.prod ? [g.prod, goalSub(g)].filter(Boolean).join(' · ') : goalSub(g))}</span></small></span></div>`
|
|
982
|
+
+ `<div class="nm" role="cell">${crit ? OK.dotHtml('broken') : ''}<span><b>${g.krTok ? `<span class="ins-krt">${esc(g.krTok)}</span>` : ''}${markHtml(g.title)}</b><small><span class="cz-pill q ins-type">${esc(g.type)}</span><span class="ins-sub" title="${esc(goalSub(g))}">${esc(PF && g.prod ? [g.prod, goalSub(g)].filter(Boolean).join(' · ') : goalSub(g))}</span></small></span></div>`
|
|
953
983
|
+ `<div class="val" role="cell"><b>${esc(nowOf(g))}</b> <span class="note">${esc(g.targetTxt ? 'of ' + g.targetTxt : 'no target')}</span>${warn ? `<div class="note ins-warn">${esc(warn)}</div>` : GC.firstOf(g) ? '' : barHtml(g)}</div>`
|
|
954
984
|
+ `<div class="sofar" role="cell">${soFarHtml(g)}</div>`
|
|
955
985
|
+ forecastHtml(g)
|
|
@@ -977,12 +1007,62 @@ const GRP_PILL = (L) => {
|
|
|
977
1007
|
const scored = L.filter((g) => g.tone !== 'q'), bad = scored.filter((g) => g.tone === 'late' || g.tone === 'red').length;
|
|
978
1008
|
return !scored.length ? '' : bad ? `<span class="cz-pill late">${bad} of ${L.length} behind</span>` : '<span class="cz-pill ok">on track</span>';
|
|
979
1009
|
};
|
|
1010
|
+
/* ── the OKR file as the goal list (Intelligence redesign A, IgA1Open): the file's key results by objective. A key result with a reader (a KR
|
|
1011
|
+
* metric record attached by its kr field or its id) is that goal's row; one without says what it needs, never a number. A record that reads no
|
|
1012
|
+
* key result of the file keeps its row under "Other goals" (principle 13: nothing is dropped). ── */
|
|
1013
|
+
const okrOn = () => !!(!PF && M.okrFile && M.okrFile.objectives && M.okrFile.objectives.length);
|
|
1014
|
+
const okrKrs = () => (okrOn() ? M.okrFile.objectives.flatMap((o) => o.krs.map((k) => Object.assign({ obj: o }, k))) : []);
|
|
1015
|
+
const krGoal = (k) => (k.goal ? M.goals.find((g) => g.id === k.goal) || null : null);
|
|
1016
|
+
const krHit = (k) => hit(k.token + ' ' + k.title, k.obj.title, ...k.moves.map((m) => m.title));
|
|
1017
|
+
/** The rows of one key result: its reader's goal row, with the key result's id before the title and the file's movers when the record has none. */
|
|
1018
|
+
function krRow(k) {
|
|
1019
|
+
const g = krGoal(k);
|
|
1020
|
+
if (g) return goalRow(Object.assign({}, g, { krTok: k.token, moves: g.moves && g.moves.length ? g.moves : k.moves }));
|
|
1021
|
+
const decision = k.decision && !k.need ? true : k.decision;
|
|
1022
|
+
const now = decision ? `<b>Not yet</b>${k.due ? `<div class="note">due ${esc(md(day(k.due)))}</div>` : ''}`
|
|
1023
|
+
: `<span class="ins-need">${OK.dotHtml('q')}<span class="note">${esc(k.need ? 'needs ' + k.need : 'needs a source')}</span></span>`;
|
|
1024
|
+
const mv = k.moves.length ? k.moves.filter((r) => r.status !== 'cancelled').slice(0, 3).map((r) => initRow(r)).join('') + (k.moves.length > 3 ? `<span class="note">and ${k.moves.length - 3} more</span>` : '') : `<span class="note">${M.initiativesRead ? 'No initiative yet' : 'The initiatives are not read yet'}</span>`;
|
|
1025
|
+
return `<div class="ins-tr ins-row ${COLS} ins-krq" role="row" data-kr="${esc(k.key)}" tabindex="0" aria-label="${esc(k.token + ' ' + k.title)}">`
|
|
1026
|
+
+ `<div class="nm" role="cell"><span><b><span class="ins-krt">${esc(k.token)}</span>${esc(k.title)}</b><small><span class="ins-sub" title="${esc(k.line || '')}">${esc(k.line || '')}</span></small></span></div>`
|
|
1027
|
+
+ `<div class="val" role="cell">${now}</div><div class="sofar" role="cell"><span class="note">—</span></div><div class="pace" role="cell"><span class="note">—</span></div>`
|
|
1028
|
+
+ `<div class="mv" role="cell">${mv}</div></div>`;
|
|
1029
|
+
}
|
|
1030
|
+
/** The key-result table from the OKR file: by objective, then the records that read none of its key results. */
|
|
1031
|
+
function okrTableHtml(head) {
|
|
1032
|
+
const P = M.okrFile.period, shown = !P || !per || (P.from <= per.to && P.to >= per.from), filtered = !noFilter();
|
|
1033
|
+
const readers = new Set(M.okrFile.readers || []);
|
|
1034
|
+
const K = shown ? okrKrs().filter((k) => krHit(k) && (!filtered || (krGoal(k) && goalsShown().some((g) => g.id === k.goal)))) : [];
|
|
1035
|
+
const other = goalsShown().filter((g) => !readers.has(g.id));
|
|
1036
|
+
const by = new Map(); for (const k of K) { if (!by.has(k.obj.id)) by.set(k.obj.id, []); by.get(k.obj.id).push(k); }
|
|
1037
|
+
const groups = [...by.values()].map((L) => `<div class="ins-grp" role="row"><span role="cell"><b>${esc(L[0].obj.token)}</b> ${esc(L[0].obj.title)}</span></div>` + L.map(krRow).join('')).join('')
|
|
1038
|
+
+ (other.length ? `<div class="ins-grp" role="row"><span role="cell">Other goals</span> <span class="q">recorded here, not in the OKR file</span></div>` + other.map(goalRow).join('') : '');
|
|
1039
|
+
const n = K.length + other.length;
|
|
1040
|
+
return head(n) + (n ? groups : `<div class="ins-empty">No key result matches${find ? ' “' + esc(find) + '”' : ''}${filtered ? ' this filter' : ''} in ${esc(per.label)}. <button type="button" class="lk" id="goalsAll">Show every goal</button></div>`);
|
|
1041
|
+
}
|
|
1042
|
+
/** A key result with no reader, in the Viewer: its words from the OKR file, what it needs, and the initiatives that move it. */
|
|
1043
|
+
function openKr(key) {
|
|
1044
|
+
const k = okrKrs().find((x) => x.key === key); if (!k) return;
|
|
1045
|
+
viewer({ kind: 'Intelligence · ' + k.obj.token, title: k.title, icon: 'chart', id: 'kr:' + key, eye: k.token, render: (el) => {
|
|
1046
|
+
el.innerHTML = `<div class="ins-gd"><div class="ins-sayc"><p class="ins-sayv">${esc(k.line || k.title)}</p>`
|
|
1047
|
+
+ `<p class="note">${esc(k.decision && !k.need ? 'A decision: it is done when it is decided' + (k.due ? ', due ' + md(day(k.due)) : '') + '.' : k.need ? 'Nothing reads it yet: it needs ' + k.need + '. I will not guess a number.' : 'The OKR file names no source for it.')}</p></div>`
|
|
1048
|
+
+ (k.source ? `<h3 class="ins-h3">How I would read it</h3><p class="note">${esc('The OKR file says: ' + k.source + '.')}</p>` : '')
|
|
1049
|
+
+ (k.need && !k.decision ? `<div class="ins-krcon"><span class="sp" aria-hidden="true">${SV(I.spark)}</span><span>${esc('Connect ' + k.need + ' and I test a read before I keep it.')}</span><button type="button" class="cz-ib sm solid" data-krcon aria-label="Open Settings, Connectors" title="Open Connectors">${SV(I.chk)}</button></div>` : '')
|
|
1050
|
+
+ `<h3 class="ins-h3">Moved by</h3>${k.moves.length ? `<div class="ins-mv">${k.moves.map((r) => initRow(r)).join('')}</div>` : '<p class="note">No initiative is anchored to it yet.</p>'}</div>`;
|
|
1051
|
+
el.addEventListener('click', (e) => { if (e.target.closest('[data-krcon]')) import('./cozy-settings.js').then((S) => S.openSettings('connectors')).catch(() => toast('Open Settings › Connectors.')); });
|
|
1052
|
+
} });
|
|
1053
|
+
}
|
|
980
1054
|
function drawGoals() {
|
|
981
1055
|
const G = goalsShown(), T = $('#goalTbl');
|
|
982
1056
|
document.body.classList.toggle('ins-trv', dash === TRENDS);
|
|
983
1057
|
if (dash === TRENDS) { T.innerHTML = bmHtml(true); T.hidden = false; $('#goalCards').innerHTML = ''; return; }
|
|
984
1058
|
const head = `<div class="ins-tr ins-th ${COLS}" role="row"><div role="columnheader">Key results <span>${G.length} · ${esc(per ? per.label : '')}</span></div><div role="columnheader">Now, of target <span class="ins-lg"><span><i class="ins-tdk"></i>today</span></span></div><div role="columnheader">So far</div><div role="columnheader">By its date</div><div role="columnheader" class="ins-mvh">Moved by <span class="ins-lg"><span><i class="ins-trk"><i class="k"></i></i>Skipped</span><span><i class="ins-trk"><i class="p m"></i></i>Milestone</span></span></div></div>`;
|
|
985
1059
|
const D = curDash();
|
|
1060
|
+
if (okrOn() && D.id === 'all') {
|
|
1061
|
+
T.hidden = false;
|
|
1062
|
+
const head2 = (n) => head.replace(/Key results <span>[^<]*<\/span>/, `Key results <span>${n} · ${esc(per ? per.label : '')}</span>`);
|
|
1063
|
+
T.innerHTML = okrTableHtml(head2) + budgetsHtml(D);
|
|
1064
|
+
drawCards(G, true); IP2.drawn(); return;
|
|
1065
|
+
}
|
|
986
1066
|
if (!M.goals.filter(isKr).length) {
|
|
987
1067
|
// the drafts table above says what is drafted: no empty card under it (zero is silence)
|
|
988
1068
|
const B = budgetsHtml(D);
|
|
@@ -996,7 +1076,7 @@ function drawGoals() {
|
|
|
996
1076
|
const objs = [...new Set(M.goals.filter(isKr).map((g) => g.pillar || ''))];
|
|
997
1077
|
const head1 = (k) => (PF ? esc(k || 'A product') : k ? 'Objective ' + (objs.indexOf(k) + 1) + ' · ' + esc(k) : 'No objective');
|
|
998
1078
|
const groups = [...by.entries()].map(([k, L]) => `<div class="ins-grp" role="row"><span role="cell">${head1(k)}</span> <span class="q">${esc(per ? per.label : '')}</span>${GRP_PILL(L)}</div>` + L.map(goalRow).join('')).join('');
|
|
999
|
-
T.innerHTML = head + (G.length ? groups : `<div class="ins-empty">No goal matches${find ? ' “' + esc(find) + '”' : ''}${noFilter() ? '' : ' this filter'} in ${esc(per.label)}. <button type="button" class="lk" id="goalsAll">Show every goal</button></div>`) + budgetsHtml(D);
|
|
1079
|
+
T.innerHTML = allProductsHtml() + head + (G.length ? groups : `<div class="ins-empty">No goal matches${find ? ' “' + esc(find) + '”' : ''}${noFilter() ? '' : ' this filter'} in ${esc(per.label)}. <button type="button" class="lk" id="goalsAll">Show every goal</button></div>`) + budgetsHtml(D);
|
|
1000
1080
|
drawCards(G);
|
|
1001
1081
|
IP2.drawn();
|
|
1002
1082
|
}
|
|
@@ -1016,24 +1096,38 @@ function cardPill(g) {
|
|
|
1016
1096
|
if (t === 'ok') return g.done !== null && g.timeGone != null && g.done > g.timeGone ? ['ok', 'Ahead'] : ['ok', 'On track'];
|
|
1017
1097
|
return null;
|
|
1018
1098
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1099
|
+
/** A key result with no reader, as a phone card: its id and name, what it needs (or Not yet for a decision), its first mover. */
|
|
1100
|
+
const krCard = (k) => `<div class="ins-card" role="listitem" data-kr="${esc(k.key)}" tabindex="0" aria-label="${esc(k.token + ' ' + k.title)}"><div class="ins-ch"><b><span class="ins-krt">${esc(k.token)}</span>${esc(k.title)}</b></div>`
|
|
1101
|
+
+ `<div class="ins-cs"><span class="note">${esc(k.decision && !k.need ? 'Not yet' + (k.due ? ' · due ' + md(day(k.due)) : '') : k.need ? 'needs ' + k.need : 'needs a source')}</span></div>`
|
|
1102
|
+
+ (k.moves[0] ? `<div class="ins-c1">${initRow(k.moves[0], false)}</div>` : '') + '</div>';
|
|
1103
|
+
function drawCards(G, okr = false) {
|
|
1104
|
+
if (okr) { // the phone (IgA7Phone): a card per key result that is read or due as a decision; the ones that wait on a source fold under one line
|
|
1105
|
+
const K = okrKrs().filter(krHit), shown = K.filter((k) => krGoal(k) || (k.decision && !k.need)), wait = K.filter((k) => !shown.includes(k));
|
|
1106
|
+
const nw = wait.filter((k) => k.need).length;
|
|
1107
|
+
$('#goalCards').innerHTML = shown.map((k) => (krGoal(k) ? cardOf(Object.assign({}, krGoal(k), { krTok: k.token })) : krCard(k))).join('')
|
|
1108
|
+
+ (wait.length ? `<details class="ins-cfold"><summary><b>${wait.length === 1 ? '1 more key result' : wait.length + ' more key results'}</b> <span class="note">${nw ? nw + ' wait on a source' : ''}</span></summary>${wait.map(krCard).join('')}</details>` : '');
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
$('#goalCards').innerHTML = (PF ? allProductsHtml() : '') + G.map(cardOf).join('') || (M.goals.filter(isKr).length ? '<p class="note">No goal in this period.</p>' : '');
|
|
1112
|
+
}
|
|
1113
|
+
function cardOf(g) {
|
|
1021
1114
|
const p = cardPill(g), tg = g.targetTxt ? (g.type === 'Count' ? 'of ' : 'target ') + g.targetTxt + (g.byAt ? ' by ' + md(day(g.byAt)) : '') : 'no target', first = GC.firstOf(g);
|
|
1022
1115
|
const gr = first ? `<span class="note">${esc(GC.firstMeta(g).split(' · ').slice(1).join(' · '))}</span>` : isBlocks(g) ? blocksHtml(g, 190, false) : g.type === 'Stay within' && g.series.length && g.target !== null ? bandSvg(g.series, g.target, g.limit !== 'floor', g.tone, g.title, 190, 30) : g.key ? sparkHtml(g, 190, 30) : '';
|
|
1023
|
-
return `<div class="ins-card" role="listitem" data-goal="${esc(g.id)}" tabindex="0" aria-label="${esc(g.title)}"><div class="ins-ch"><b>${esc(g.title)}</b>${p ? `<span class="cz-pill ${p[0]}">${p[1]}</span>` : ''}</div>`
|
|
1116
|
+
return `<div class="ins-card" role="listitem" data-goal="${esc(g.id)}" tabindex="0" aria-label="${esc(g.title)}"><div class="ins-ch"><b>${g.krTok ? `<span class="ins-krt">${esc(g.krTok)}</span>` : ''}${esc(g.title)}</b>${p ? `<span class="cz-pill ${p[0]}">${p[1]}</span>` : ''}</div>`
|
|
1024
1117
|
+ `<div class="ins-cs"><span class="cz-pill q ins-type">${esc(g.type)}</span><span class="note">${esc(tg)}</span></div>`
|
|
1025
1118
|
+ `<div class="ins-cv"><b>${esc(nowOf(g))}</b><span class="ins-cg">${gr}</span></div>${first || gcSay(g) ? '' : barHtml(g, ' ins-wide')}`
|
|
1026
1119
|
+ (g.moves[0] ? `<div class="ins-c1">${initRow(g.moves[0], false)}</div>` : '') + '</div>' + (PF ? '' : draftsFor(g.id).map(drHtml).join(''));
|
|
1027
|
-
}).join('') || (M.goals.filter(isKr).length ? '<p class="note">No goal in this period.</p>' : '');
|
|
1028
1120
|
}
|
|
1029
1121
|
IP2.mount({ page: $('#stage'), table: $('#goalTbl'), selecting: () => selecting });
|
|
1030
|
-
$('#goalCards').addEventListener('click', (e) => { if (e.target.closest('a, .okr-dr')) return; const c = e.target.closest('[data-goal]'); if (c) pressGoal(c.dataset.goal); });
|
|
1122
|
+
$('#goalCards').addEventListener('click', (e) => { if (e.target.closest('a, .okr-dr, summary')) return; const pp = e.target.closest('[data-prod]'); if (pp) { location.assign(setQ({ ws: pp.dataset.prod, board: null, goal: null })); return; } const q = e.target.closest('[data-kr]'); if (q) { openKr(q.dataset.kr); return; } const c = e.target.closest('[data-goal]'); if (c) pressGoal(c.dataset.goal); });
|
|
1031
1123
|
$('#goalTbl').addEventListener('click', (e) => {
|
|
1032
|
-
if (e.target.closest('#goalsAll')) { find = ''; $('#findIn').value = ''; clearFilter(); per = periods()
|
|
1124
|
+
if (e.target.closest('#goalsAll')) { find = ''; $('#findIn').value = ''; clearFilter(); per = periods().find((p) => p.key === 'y'); redraw(); return; }
|
|
1033
1125
|
if (e.target.closest('a')) return;
|
|
1034
1126
|
const tile = e.target.closest('[data-tile], [data-mt]'); if (tile) { if (tgOpen) closeTarget(); openTile(tile.dataset.tile || tile.dataset.mt); return; }
|
|
1035
1127
|
if (e.target.closest('.okr-dr')) return; // a draft under a row has its own presses
|
|
1036
1128
|
const bu = e.target.closest('[data-budget]'); if (bu) { openBudget(bu.dataset.budget); return; }
|
|
1129
|
+
const kq = e.target.closest('[data-kr]'); if (kq) { openKr(kq.dataset.kr); return; }
|
|
1130
|
+
const pr = e.target.closest('[data-prod]'); if (pr) { location.assign(setQ({ ws: pr.dataset.prod, board: null, goal: null })); return; }
|
|
1037
1131
|
const r = e.target.closest('[data-goal]'); if (r) pressGoal(r.dataset.goal);
|
|
1038
1132
|
});
|
|
1039
1133
|
$('#goalTbl').addEventListener('keydown', (e) => { if (e.key !== 'Enter' && e.key !== ' ') return; const r = e.target.closest && e.target.closest('.ins-row, [data-tile]'); if (!r || e.target !== r) return; e.preventDefault(); r.click(); });
|
|
@@ -1342,6 +1436,8 @@ $('#goalCards').addEventListener('click', (e) => { if (e.target.closest('a')) re
|
|
|
1342
1436
|
/* ── Delivery trends: six tiles from the Board's measures (AgInsGoals, IsTarget): a label, the value, a small line with its end dot and the
|
|
1343
1437
|
* target's line under it. A press opens the target popover over the tiles. In the Delivery trends view the tiles stand three to a row. ─── */
|
|
1344
1438
|
const TILES = [['blocked', 'Blocked tickets'], ['late', 'Late initiatives'], ['idle', 'Idle 30+ days'], ['scope', 'Scope added'], ['merges', 'Merged a week'], ['corrections', 'Corrections / 100 runs']];
|
|
1439
|
+
/* Intelligence redesign A (IgA4Trends): merges and corrections measure PMOS's own agents, so they show only on the PMOS workspace's Intelligence. */
|
|
1440
|
+
const tilesOn = () => TILES.filter(([k]) => (M && M.self) || (k !== 'merges' && k !== 'corrections'));
|
|
1345
1441
|
const TILE_DIR = { merges: 'at least' }; // more merges is better; every other measure is better lower
|
|
1346
1442
|
const TILE_DEF = { late: 'Open initiatives the workflow engine reports as late today. Earlier weeks count the initiatives late today whose end date had already passed; the Board keeps no history of a status.', scope: 'Initiatives whose planned start falls in the week.' };
|
|
1347
1443
|
const numsOf = (ser) => (ser || []).filter((v) => typeof v === 'number' && Number.isFinite(v));
|
|
@@ -1350,7 +1446,7 @@ const numsOf = (ser) => (ser || []).filter((v) => typeof v === 'number' && Numbe
|
|
|
1350
1446
|
function targetOf(key, t) {
|
|
1351
1447
|
const TS = ((M.trends || {}).tiles || {})[key] || null, dir = (TS && TS.dir) || TILE_DIR[key] || 'at most';
|
|
1352
1448
|
const tg = dir === 'none' || t.tg == null || !Number.isFinite(Number(t.tg)) ? null : Number(t.tg);
|
|
1353
|
-
if (tg === null) return { dir, tg: null, met: null, miss2: false, line: '
|
|
1449
|
+
if (tg === null) return { dir, tg: null, met: null, miss2: false, line: '', set: TS }; // zero is silence: no "No target" (principle 3)
|
|
1354
1450
|
const miss = (v) => (dir === 'at least' ? v < tg : v > tg), S = numsOf(t.ser), v0 = S.length ? S[S.length - 1] : Number(t.v);
|
|
1355
1451
|
const met = Number.isFinite(v0) ? !miss(v0) : null, miss2 = S.length >= 2 ? miss(S[S.length - 1]) && miss(S[S.length - 2]) : met === false;
|
|
1356
1452
|
return { dir, tg, met, miss2, set: TS, line: 'Target: ' + dir + ' ' + tg + (met ? ' · met' : TS && TS.at ? ' · set ' + md(day(TS.at)) : '') };
|
|
@@ -1369,6 +1465,8 @@ function tileOf(key) {
|
|
|
1369
1465
|
const b = (BM.tiles || []).find((x) => x.key === key);
|
|
1370
1466
|
if (!b) return { v: 'Not read', s: '', abs: true, T: { line: '' } };
|
|
1371
1467
|
if (b.absent) return { v: b.absent, s: '', abs: true, why: b.why || '', def: b.def, T: { line: '' } };
|
|
1468
|
+
// a month of history makes "no activity on record" mean idle; before that the tile says when it is known, not a count (IgA4Trends)
|
|
1469
|
+
if (b.knownFrom) return { v: '—', s: 'known from ' + md(day(b.knownFrom)), abs: true, why: 'History starts ' + md(day(b.historyFrom)) + ', so the first month is known from ' + md(day(b.knownFrom)) + '.', def: b.def, T: { line: 'known from ' + md(day(b.knownFrom)) } };
|
|
1372
1470
|
t = { v: b.value, ser: b.ser, s: b.targetTxt || '', note: b.text || '', tg: b.tg != null ? b.tg : null, base: !!b.look || b.kind === 'warn' || b.kind === 'bad',
|
|
1373
1471
|
change: b.change || null, wk: b.wk || null, nt: b.nt || null, def: b.def, dec: b.dec || 0 };
|
|
1374
1472
|
}
|
|
@@ -1385,7 +1483,7 @@ function miniSpark(ser, W = 150, H = 18) {
|
|
|
1385
1483
|
}
|
|
1386
1484
|
let tgOpen = null; // the tile whose target popover is open
|
|
1387
1485
|
/** The six tiles with their words (label, value, change) as the trends view reads them. */
|
|
1388
|
-
const trendTiles = () =>
|
|
1486
|
+
const trendTiles = () => tilesOn().map(([k, l]) => Object.assign({ key: k, label: l }, tileOf(k)));
|
|
1389
1487
|
const CHAT = 'M4 5h16v11H9l-5 4z';
|
|
1390
1488
|
/** The ✦ month line (canvas MxA): the Board's measures in one sentence; the chat icon takes it to Strategist. Nothing read, no line. */
|
|
1391
1489
|
function monthStrip() {
|
|
@@ -1418,7 +1516,7 @@ function bmHtml(big) {
|
|
|
1418
1516
|
: '<div class="ins-grp" role="row"><h2 id="hBm" role="cell">Delivery trends</h2> <span class="q">the Board shows today’s counts as chips · press a tile to set its target</span></div>';
|
|
1419
1517
|
const mv = (t) => (big && !t.abs && t.change && t.change.d !== 0 ? `<span class="mv">${esc(t.change.txt)}</span>` : '');
|
|
1420
1518
|
return `<section class="ins-trends${big ? ' big' : ''}" id="cBm" aria-labelledby="hBm">${head}${big ? monthStrip() : ''}`
|
|
1421
|
-
+ `<div class="ins-tiles" id="bmBody">${
|
|
1519
|
+
+ `<div class="ins-tiles" id="bmBody">${tilesOn().map(([k, l]) => { const t = tileOf(k), on = tgOpen === k; return `<div class="ins-tile${t.warn ? ' warn' : ''}${t.abs ? ' abs' : ''}${on ? ' sel' : ''}" data-tile="${k}" role="button" tabindex="0" aria-expanded="${on}" aria-label="${esc(l + ', ' + t.v + (t.s ? ', ' + t.s : '') + (t.change && t.change.d !== 0 ? ', ' + t.change.txt : '') + (t.T.line ? ', ' + t.T.line : ''))}"${t.s ? ` title="${esc(t.s)}"` : ''}><span class="l">${esc(l)}</span><span class="v">${esc(t.v)}</span>${big ? miniSpark(t.ser, 300, 34) : miniSpark(t.ser)}${mv(t)}<span class="tg">${on ? 'Setting target…' : esc(t.T.line)}</span></div>`; }).join('')}</div>`
|
|
1422
1520
|
+ (big ? movedCard() + movedGoals() : '') + '</section>';
|
|
1423
1521
|
}
|
|
1424
1522
|
/** A tile in the Viewer (MxB): its weeks as bars, a blank week dashed with its reason, the definition, where it is counted from, and the target. */
|
|
@@ -1547,21 +1645,26 @@ function draftOf(r) {
|
|
|
1547
1645
|
let decOpen = false;
|
|
1548
1646
|
function outRow(r) {
|
|
1549
1647
|
const due = dueOf(r), P = promiseOf(r), dr = draftOf(r), ship = day(r.endAt);
|
|
1550
|
-
const
|
|
1648
|
+
const pr = r.promise || null;
|
|
1649
|
+
const promised0 = P ? `<b>${esc(P.g.title + (P.promised !== null ? ' ' + sgn(P.promised, P.g.unit) : ''))}</b><div class="note">${esc(P.g.byAt ? 'by ' + md(day(P.g.byAt)) : 'no date')}</div>` : `<span class="note">${r.anchor ? 'Anchored to ' + esc(r.anchor) + ', which is not a goal with readings.' : 'No goal is anchored, so nothing was promised to measure.'}</span>`;
|
|
1650
|
+
// the D57 promise (outcome_metric) is what the row promised; the anchor goal's numbers stay under it when there is one
|
|
1651
|
+
const promised = pr && pr.metric ? `<b>${esc(pr.metric)}</b>${P ? `<div class="note">${esc(P.g.title + (P.promised !== null ? ' ' + sgn(P.promised, P.g.unit) : ''))}</div>` : ''}` : P ? promised0 : '<span class="cz-pill q">No outcome promised</span>';
|
|
1652
|
+
const check = pr && pr.horizon ? `<span class="ins-chk">${OK.dotHtml(pr.overdue ? 'behind' : 'q')}<b>${esc(md(day(pr.horizon)))}</b></span>${pr.overdue ? '<div class="note">overdue</div>' : ''}` : '<span class="note">No check date</span>';
|
|
1551
1653
|
const got = P ? `<span class="ins-gotw">${bulletHtml(P)}<b>${esc(P.moved === null ? 'no reading yet' : sgn(P.moved, P.g.unit))}</b></span>${guardOf(r) ? `<div class="note">${esc(guardOf(r).name + ': ' + guardOf(r).text)}</div>` : ''}` : '';
|
|
1552
1654
|
let verdict, act = '';
|
|
1553
1655
|
if (r.group === 'verdict') {
|
|
1554
|
-
|
|
1656
|
+
const dd = pr && pr.horizon ? day(pr.horizon) : due;
|
|
1657
|
+
verdict = dr ? `<span class="ins-dv"><span class="sp" aria-hidden="true">${SV(I.spark)}</span><span><b>${esc(dr.verdict)}.</b> ${esc(dr.text)}</span></span>` : `<span class="cz-pill you">To judge${dd !== null ? (pr && pr.overdue ? ' · was due ' : ' · due ') + esc(md(dd)) : ''}</span>`;
|
|
1555
1658
|
act = `<button type="button" class="cz-ib sm" data-verdict="${esc(r.id)}" aria-haspopup="dialog" aria-label="Decide: Keep, Iterate or Stop" title="Decide">${SV(I.pen)}</button>`
|
|
1556
1659
|
+ (dr ? `<button type="button" class="cz-ib sm solid" data-keep="${esc(r.id)}" aria-label="Record the verdict: Keep" title="Record the verdict: Keep">${SV(I.chk)}</button>` : '');
|
|
1557
1660
|
} else if (r.group === 'decided') {
|
|
1558
1661
|
const v = r.verdict;
|
|
1559
1662
|
verdict = v ? `<span class="cz-pill ${v.verdict === 'Stop' ? 'q' : 'ok'}">${v.verdict === 'Keep' ? SV(I.chk) + 'Kept' : v.verdict === 'Iterate' ? 'Changed' : 'Stopped'}${v.at ? ' · ' + esc(md(day(v.at))) : ''}</span><div class="note">${esc(v.learned)}</div>`
|
|
1560
|
-
:
|
|
1663
|
+
: '<span class="cz-pill q">Stopped</span><div class="note">No learning recorded.</div>'; // only a stopped initiative is decided without a verdict
|
|
1561
1664
|
if (v && v.at === M.today) act = `<button type="button" class="cz-ib sm" data-vundo="${esc(r.id)}" aria-label="Undo the verdict" title="Undo">${SV(I.undo)}</button>`;
|
|
1562
1665
|
} else verdict = `<span class="cz-pill q">${esc(due !== null ? 'Judged ' + md(due) : r.idx === 6 ? 'Being monitored' : 'Too early')}</span>`;
|
|
1563
1666
|
return `<div class="ins-tr ins-row ins-ocols" role="row" data-ini="${esc(r.id)}"><div class="nm" role="cell"><a href="${iniHref(r.id)}"><b>${esc(r.title)}</b></a><small>${esc(ship !== null ? 'shipped ' + md(ship) : 'no ship date recorded')}</small></div>`
|
|
1564
|
-
+ `<div role="cell" class="pace">${promised}</div><div role="cell" class="pace">${got}</div><div role="cell" class="pace">${verdict}</div><div role="cell" class="ins-act">${act}</div></div>`;
|
|
1667
|
+
+ `<div role="cell" class="pace">${promised}</div><div role="cell" class="pace">${check}</div><div role="cell" class="pace">${got}</div><div role="cell" class="pace">${verdict}</div><div role="cell" class="ins-act">${act}</div></div>`;
|
|
1565
1668
|
}
|
|
1566
1669
|
/** Did it work? (Iq8Overview): only the work to judge and the work in its window; the decided ones fold under one line that counts them. The
|
|
1567
1670
|
* section is not drawn when nothing has shipped. */
|
|
@@ -1571,9 +1674,10 @@ function drawOutcomes() {
|
|
|
1571
1674
|
if (!M.initiativesRead || (!live.length && !dec.length)) { sec.hidden = true; $('#outTbl').innerHTML = ''; return; }
|
|
1572
1675
|
sec.hidden = false;
|
|
1573
1676
|
const nj = live.filter((r) => r.group === 'verdict').length, nw = live.length - nj;
|
|
1574
|
-
const vOf = (r) => (r.verdict ? r.verdict.verdict :
|
|
1575
|
-
const
|
|
1576
|
-
const
|
|
1677
|
+
const vOf = (r) => (r.verdict ? r.verdict.verdict : 'Stop'), cnt = (k) => dec.filter((r) => vOf(r) === k).length; // kept only from a recorded verdict
|
|
1678
|
+
const np = live.filter((r) => !(r.promise && r.promise.metric) && !r.goal).length;
|
|
1679
|
+
const head = `<div class="ins-tr ins-th ins-ocols" role="row"><div role="columnheader">Did it work? <span>${esc([nj ? nj + ' to judge' : '', nw ? nw + ' in window' : '', np ? np + ' without a promise' : ''].filter(Boolean).join(' · '))}</span></div><div role="columnheader">Promised</div><div role="columnheader">Check</div><div role="columnheader">Got</div><div role="columnheader">Verdict</div><div></div></div>`;
|
|
1680
|
+
const foot = dec.length ? `<button type="button" class="ins-foot" id="decFold" aria-expanded="${decOpen}">Decided <b>${dec.length}</b> · ${esc([cnt('Keep') ? cnt('Keep') + ' kept' : '', cnt('Iterate') ? cnt('Iterate') + ' changed' : '', cnt('Stop') ? cnt('Stop') + ' stopped' : ''].filter(Boolean).join(', '))}${SV(decOpen ? I.up : I.down)}</button>` : '';
|
|
1577
1681
|
$('#outTbl').innerHTML = oneNote() + head + live.map(outRow).join('') + foot + (decOpen ? dec.map(outRow).join('') : '');
|
|
1578
1682
|
}
|
|
1579
1683
|
$('#outTbl').addEventListener('click', async (e) => {
|
|
@@ -1635,9 +1739,19 @@ function goalPeriods() {
|
|
|
1635
1739
|
out.push({ kind: 'Year', label: String(y), by: Date.UTC(y, 12, 0) }, { kind: 'Year', label: String(y + 1), by: Date.UTC(y + 1, 12, 0) });
|
|
1636
1740
|
return out;
|
|
1637
1741
|
}
|
|
1742
|
+
/** + drafts the goal (IgA1Open): the first key result of the OKR file that nothing reads yet, its numbers from the file's line, its due date and
|
|
1743
|
+
* its objective. It is a draft in the same form (the form is the ✎ editor); Save keeps it as that key result's reader, never a second goal. */
|
|
1744
|
+
function krDraft() {
|
|
1745
|
+
const k = okrKrs().find((x) => !x.goal && !x.decision && /\d/.test(x.line || '')); if (!k) return null;
|
|
1746
|
+
const nums = String(k.line).split('→').map((x) => { const m = /(-?−?\d[\d,]*(?:\.\d+)?)\s*(%)?/.exec(x); return m ? { v: Number(m[1].replace(/,/g, '').replace('−', '-')), pct: !!m[2] } : null; });
|
|
1747
|
+
const two = nums.length >= 2 && nums[0] && nums[nums.length - 1];
|
|
1748
|
+
return { k, name: k.title, obj: (k.obj.token + ' ' + k.obj.title).trim(), base: two ? nums[0].v : null, target: two ? nums[nums.length - 1].v : null, pct: !!(nums[0] && nums[0].pct), due: k.due };
|
|
1749
|
+
}
|
|
1638
1750
|
function openNewGoal() {
|
|
1639
1751
|
const objs = [...new Set(M.goals.map((g) => g.pillar).filter(Boolean))];
|
|
1640
|
-
const
|
|
1752
|
+
const DR0 = krDraft();
|
|
1753
|
+
if (DR0 && !objs.includes(DR0.obj)) objs.unshift(DR0.obj);
|
|
1754
|
+
const GP = goalPeriods(), others = M.goals.filter((g) => g.type !== 'Done or not').map((g) => g.title); // a done-or-not goal is never a guardrail
|
|
1641
1755
|
/* the objective is eyebrow text with a quiet chevron (AgInsNewGoal: "New goal · Objective 1"); its menu names each objective */
|
|
1642
1756
|
const objSel = `<span class="ins-mw"><button type="button" class="ins-objb" id="ngObjB" aria-haspopup="menu" aria-expanded="false" aria-controls="ngObjMenu" aria-label="Objective: ${objs.length ? 'Objective 1 · ' + esc(objs[0]) : 'No objective'}">${objs.length ? 'Objective 1' : 'No objective'}<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 10l5 5 5-5"/></svg></button>`
|
|
1643
1757
|
+ `<div class="ins-menu" id="ngObjMenu" role="menu" aria-label="Objective" hidden>${objs.map((o, i) => `<button type="button" role="menuitemradio" aria-checked="${i === 0}" data-obj="${esc(o)}" data-said="Objective ${i + 1}">Objective ${i + 1} · ${esc(o)}</button>`).join('')}<button type="button" role="menuitemradio" aria-checked="${!objs.length}" data-obj="" data-said="No objective">No objective</button><button type="button" role="menuitemradio" aria-checked="false" data-obj="+" data-said="A new objective">A new objective…</button></div></span>`
|
|
@@ -1663,6 +1777,19 @@ function openNewGoal() {
|
|
|
1663
1777
|
});
|
|
1664
1778
|
$('#ngPer').dataset.kind = 'Quarter';
|
|
1665
1779
|
goalHint();
|
|
1780
|
+
if (DR0) fillDraft(DR0);
|
|
1781
|
+
}
|
|
1782
|
+
/** Fill the drafted key result into the form; the PM checks it and saves, or edits any field first. */
|
|
1783
|
+
function fillDraft(D) {
|
|
1784
|
+
const all = Array.from(document.querySelectorAll('#sideBody button')), btn = (t) => all.find((x) => x.textContent.trim() === t);
|
|
1785
|
+
$('#ngName').value = D.name; $('#ngName').dataset.kr = D.k.obj.token + '.' + D.k.token;
|
|
1786
|
+
if (D.pct && btn('Percentage')) btn('Percentage').click();
|
|
1787
|
+
if (D.base !== null) $('#ngBase').value = String(D.base);
|
|
1788
|
+
if (D.target !== null) $('#ngTgt').value = String(D.target);
|
|
1789
|
+
if (D.base !== null && D.target !== null && D.target < D.base && btn('Down')) btn('Down').click();
|
|
1790
|
+
if (D.due && btn('Custom')) { btn('Custom').click(); $('#ngBy').value = D.due; }
|
|
1791
|
+
const t = all.find((x) => x.dataset && x.dataset.src === 'typed'); if (t) t.click(); // no reader is tested yet: you type the reading in
|
|
1792
|
+
$('#sideMsg').textContent = '✦ Drafted from your OKR file (' + D.k.token + '). Check it, then save.' + (D.k.need ? ' It needs ' + D.k.need + ' to be read by itself.' : '');
|
|
1666
1793
|
}
|
|
1667
1794
|
/** The form follows its type: a measured goal has Better is, a baseline and a target; Stay within a limit and the reading now; Done or not its steps. */
|
|
1668
1795
|
function goalHint() {
|
|
@@ -1688,7 +1815,7 @@ async function saveGoal() {
|
|
|
1688
1815
|
const src = $('#sideBody [data-src][aria-pressed="true"]');
|
|
1689
1816
|
const body = { ws: M.board.ws, name: v('#ngName'), type: ty, direction: segVal('Better is'), baseline: v('#ngBase'), target: v('#ngTgt'),
|
|
1690
1817
|
due: pk === 'Custom' ? v('#ngBy') : $('#ngPer').value, period_kind: always ? null : pk, period_label: pk === 'Custom' ? 'Custom' : ($('#ngPer').selectedOptions[0] || {}).textContent || pk,
|
|
1691
|
-
objective: obj || '', source: src ? src.dataset.src : '', owner: v('#ngOwner'), guardrail: $('#ngGuard').value,
|
|
1818
|
+
objective: obj || '', source: src ? src.dataset.src : '', owner: v('#ngOwner'), guardrail: $('#ngGuard').value, kr: $('#ngName').dataset.kr || undefined,
|
|
1692
1819
|
steps: ty === 'Done or not' ? $('#ngSteps').value.split('\n').map((x) => x.trim()).filter(Boolean) : undefined,
|
|
1693
1820
|
limit: ty === 'Stay within' ? (segVal('Limit') === 'Floor' ? 'floor' : 'ceiling') : undefined, unit: ty === 'Stay within' ? v('#ngUnit') : undefined, always_on: always };
|
|
1694
1821
|
$('#sideGo').disabled = true; m.textContent = 'Saving.';
|
|
@@ -1775,9 +1902,15 @@ async function saveNewDash() {
|
|
|
1775
1902
|
let chartDash = null;
|
|
1776
1903
|
function chartHost(D) { return D && D.saved ? D : savedDash().find((d) => d.id === M.pinned) || savedDash()[0] || null; }
|
|
1777
1904
|
function openChartEdit(goalId, D0) {
|
|
1778
|
-
|
|
1905
|
+
let D = chartHost(D0 || curDash());
|
|
1779
1906
|
const g = M.goals.find((x) => x.id === goalId) || M.goals.find((x) => x.key) || M.goals[0];
|
|
1780
1907
|
if (!g) { toast('There is no goal to chart yet. Add one with New goal.'); return; }
|
|
1908
|
+
// Reports and dashboards redesign R (RgR3Dash): a key result nothing reads opens its connector card, never "no reading yet, make a dashboard first"
|
|
1909
|
+
const kw = !g.readings ? okrKrs().find((k) => k.goal === g.id && k.need && !k.decision) : null;
|
|
1910
|
+
if (kw) { openKr(kw.key); return; }
|
|
1911
|
+
// with no saved dashboard, the chart goes on the one drafted from the OKR file; Save chart keeps that dashboard first
|
|
1912
|
+
if (!D) { const dd = draftedDash(); if (dd && dd.charts.length) D = { id: null, drafted: true, saved: false, name: dd.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: dd.charts, main: true }; }
|
|
1913
|
+
chartDash = D;
|
|
1781
1914
|
const on = D && D.charts.includes(g.id), cs = (D && (M.chartSet || {})[D.id] || {})[g.id] || {};
|
|
1782
1915
|
openSide({ kind: 'chart', eye: D ? 'Chart on ' + D.name : 'Chart · no dashboard yet', title: g.title, go: 'Save chart', onGo: saveChart,
|
|
1783
1916
|
goOff: !D ? 'Make a dashboard first, with New dashboard' : '',
|
|
@@ -1835,8 +1968,13 @@ function chartSvg(o) {
|
|
|
1835
1968
|
return `<svg class="ins-ch2" viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(o.pts.length + ' readings as a line')}">${tgt}${pv}<polygon class="ar" points="${line[0].split(',')[0]},${H} ${line.join(' ')} ${last[0]},${H}"/>${pj}<polyline class="ln" points="${line.join(' ')}"/>${ship}<circle class="dt" cx="${last[0]}" cy="${last[1]}" r="3.6" style="fill:${TONE_BAR[o.tone] || TONE_BAR.q}"/></svg>`;
|
|
1836
1969
|
}
|
|
1837
1970
|
async function saveChart() {
|
|
1838
|
-
|
|
1971
|
+
let D = chartDash; const id = $('#chGoal').value, m = $('#sideMsg'); if (!D) return;
|
|
1839
1972
|
$('#sideGo').disabled = true; m.textContent = 'Saving.';
|
|
1973
|
+
if (D.drafted) {
|
|
1974
|
+
const k = await saveDash({ name: D.name, purpose: D.purpose, charts: D.charts.includes(id) ? D.charts : D.charts.concat(id), main: true });
|
|
1975
|
+
if (!k || k.ok !== true || !k.dashboard) { $('#sideGo').disabled = false; m.textContent = (k && k.error) || 'The dashboard could not be kept. Nothing changed.'; return; }
|
|
1976
|
+
D = Object.assign({}, k.dashboard, { saved: true }); chartDash = D;
|
|
1977
|
+
}
|
|
1840
1978
|
const res = await pmPost('/api/insights-records/chart', { ws: M.board.ws, board: M.board.id, dash: D.id, goal: id, form: segVal('Form'), range: segVal('Range'), compare: $('#chCmp').value,
|
|
1841
1979
|
target: $('#chTgt').getAttribute('aria-checked') === 'true', forecast: $('#chFc').getAttribute('aria-checked') === 'true', note: $('#chNote').value.trim(), add: !D.charts.includes(id) });
|
|
1842
1980
|
$('#sideGo').disabled = false;
|
|
@@ -1886,7 +2024,7 @@ function reportGroups() {
|
|
|
1886
2024
|
for (const f of R.missing ? [] : R.rows) { if (!by.has(f.cadence)) by.set(f.cadence, []); by.get(f.cadence).push(f); }
|
|
1887
2025
|
const out = [], shown = (fs) => fs && fs.some((x) => !x.archived);
|
|
1888
2026
|
for (const s of R.setupsOk === false ? [] : (R.setups || [])) { const fs = by.get(s.id); by.delete(s.id); out.push(shown(fs) ? { kind: 'file', x: { id: s.id, name: s.name, setup: s, files: fs } } : { kind: 'setup', x: s }); }
|
|
1889
|
-
for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: id, setup: null, files: fs } });
|
|
2027
|
+
for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: (R.names && R.names[id]) || id, setup: null, files: fs } }); // redesign R: a definition report is named by its title
|
|
1890
2028
|
return out;
|
|
1891
2029
|
}
|
|
1892
2030
|
/** A schedule said short, for the status pill: "Every other Monday at 09:00" is "Every other Monday". */
|
|
@@ -1926,23 +2064,118 @@ function drawList() {
|
|
|
1926
2064
|
const reps = reportGroups().filter((g) => hit(g.x.name)), dashes = M.dashboards.filter((d) => hit(dashName(d)));
|
|
1927
2065
|
const nRep = reps.length + asks.length;
|
|
1928
2066
|
$('#repCap').textContent = String(nRep + dashes.length);
|
|
1929
|
-
$('#newRep').href = homeAsk('insights', 'Make a new report: ');
|
|
2067
|
+
$('#newRep').href = homeAsk('insights', 'Make a new report: '); // with no product picked, Home still makes it; with one, + opens the drafted report (below)
|
|
1930
2068
|
const fbox = found.length ? `<div class="ins-fd" aria-label="Found in your work"><div class="ins-fdh"><b>Found in your work · ${found.length}</b><span class="note">Things you write by hand often</span></div>`
|
|
1931
2069
|
+ found.map((f) => `<div class="ins-fcard" data-f="${esc(f.id)}"><button type="button" class="nm" data-fx="open" aria-label="${esc(f.name)}: what PMOS found"><b>${esc(f.name)}</b><small>${esc(f.why)}</small></button><button type="button" class="ins-btn sm dark" data-fx="setup">Set it up</button><button type="button" class="ins-btn sm ghost" data-fx="dismiss">Not a report</button></div>`).join('') + '</div>' : '';
|
|
1932
2070
|
const empty = R.missing ? '<div class="ins-empty"><b>The report files could not be read</b>, so none is listed.</div>'
|
|
1933
2071
|
: R.setupsOk === false ? '<div class="ins-empty"><b>The report setups could not be read</b>, so what you set up is not listed.</div>' : '';
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2072
|
+
/* Reports and dashboards redesign R (RgR1Panel; A's IgA5Reports): Needs you first, each waiting report with one line of its headline and its one ✓
|
|
2073
|
+
* (Issue · Confirm the definition · Move to <product> · Resume, lib/report-acts.js), then Drafting on their moment and Weekly updates (the
|
|
2074
|
+
* ready-made switches, folded), then Yours, then the dashboards. Needs you is first on a phone too, before the one-product note. */
|
|
2075
|
+
const acts = R.acts || {};
|
|
2076
|
+
const waits = reps.filter((G) => acts[G.x.id]), waitAsks = asks.filter((a) => a.state === 'ready');
|
|
2077
|
+
const rest = reps.filter((G) => !waits.includes(G)), restAsks = asks.filter((a) => !waitAsks.includes(a));
|
|
2078
|
+
const head3 = '<div class="ins-rl ins-rlh" role="presentation"><span>Report</span><span>Next</span><span>Status</span></div>';
|
|
2079
|
+
const nNeed = waits.length + waitAsks.length;
|
|
2080
|
+
const needs = nNeed ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${nNeed}</span></div><div class="ins-nlist" id="repNeeds" role="list">${waits.map(needRowHtml).join('')}${waitAsks.map(askNeedHtml).join('')}</div>` : '';
|
|
2081
|
+
const table = rest.length + restAsks.length ? `<div class="ins-sec"><h3>Yours</h3><span class="q">${rest.length + restAsks.length}</span></div><div class="ins-rtbl" id="repAsks">${head3}<div role="list">${rest.map(repRowHtml).join('')}${restAsks.map(askRowHtml).join('')}</div></div>`
|
|
2082
|
+
: !empty && !needs ? '<div class="ins-empty"><b>No report is set up and no report file is on record.</b> Reports are made in Home.</div>' : '';
|
|
2083
|
+
const dd = savedDash().length ? null : draftedDash();
|
|
2084
|
+
const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length && !dd ? dashes.length + ' · a dashboard is a saved view of Goals' : dd ? '1 drafted · ' + esc(dd.name) : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dd ? draftedCard(dd) : ''}${dashes.map(dashCard).join('')}</div>`;
|
|
2085
|
+
$('#repBody').innerHTML = `${needs}${oneNote()}${libFolds(LIB.L) || '<div id="readyLib"></div>'}<div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
|
|
2086
|
+
wireFolds();
|
|
1938
2087
|
if (!LIB.read && M.board && M.board.ws) { LIB.read = true; RL.readLib(M.board.ws).then((L) => { LIB.L = L; if (!$('#libSide').hidden) drawLib(); }); }
|
|
1939
2088
|
}
|
|
2089
|
+
const wireFolds = () => { for (const fd of $$('#repBody .ins-libfold')) fd.addEventListener('toggle', () => { LIB.fold[fd.dataset.fold] = fd.open; }); };
|
|
2090
|
+
/** The ready-made switches as two folds (RgR1Panel): the moment reports (up to four peek lines: what each drafts from), then the weekly updates
|
|
2091
|
+
* (one line: how many are on and off). The switches themselves are inside, unchanged (pm/ready-lib.js). Zero is silence: an empty fold is not drawn. */
|
|
2092
|
+
function libFolds(L) {
|
|
2093
|
+
if (!L || !(L.groups || []).length) return '';
|
|
2094
|
+
const onlyKind = (weekly) => Object.assign({}, L, { lead: weekly ? L.lead : null, overlap: weekly ? L.overlap : null,
|
|
2095
|
+
groups: L.groups.map((g) => Object.assign({}, g, { rows: (g.rows || []).filter((r) => (r.kind === 'weekly') === weekly), more: weekly ? g.more : null }))
|
|
2096
|
+
.filter((g) => g.rows.length || (weekly && g.more && g.more.count)) });
|
|
2097
|
+
const all = L.groups.flatMap((g) => (g.rows || []).concat((g.more && g.more.rows) || []).map((r) => Object.assign({ level: g.level, gname: g.name }, r)));
|
|
2098
|
+
const moments = all.filter((r) => r.kind !== 'weekly' && r.on), wk = all.filter((r) => r.kind === 'weekly'), wkOn = wk.filter((r) => r.on).length;
|
|
2099
|
+
const peek = moments.slice(0, 4).map((r) => `<li><b>${esc(r.name)}</b>${r.level === 'initiative' ? '' : ' · ' + esc(r.gname)}${r.sub ? ` <span class="note">${esc(r.sub)}</span>` : ''}</li>`).join('');
|
|
2100
|
+
const mHtml = RL.libHtml(onlyKind(false), { open: LIB.open, yours: false }), wHtml = RL.libHtml(onlyKind(true), { open: LIB.open, yours: false });
|
|
2101
|
+
const foldBox = (k, title, sub, pk, body) => (body ? `<details class="ins-libfold" data-fold="${k}"${LIB.fold[k] ? ' open' : ''}><summary><b>${esc(title)}</b> <span class="note">${esc(sub)}</span></summary>${pk}<div class="ins-libin">${body}</div></details>` : '');
|
|
2102
|
+
return `<div id="readyLib">${foldBox('moment', 'Drafting on their moment', 'nothing to set up', peek ? `<ul class="ins-mlist">${peek}</ul>` : '', mHtml)}`
|
|
2103
|
+
+ `${foldBox('weekly', 'Weekly updates', wk.length ? (wkOn ? wkOn + ' on' : 'none on') + (wk.length - wkOn ? ' · ' + (wk.length - wkOn) + ' off' : '') : '', '', wHtml)}</div>`;
|
|
2104
|
+
}
|
|
2105
|
+
/** A waiting report (RgR1Panel): its name, when it was drafted and for whom, one line of its headline, and its one ✓. */
|
|
2106
|
+
function needRowHtml(G) {
|
|
2107
|
+
const st = repState(G), a = (M.reports.acts || {})[G.x.id], s = st.s, d = st.draft;
|
|
2108
|
+
const about = (M.reports.about || {})[G.x.id];
|
|
2109
|
+
const sub = [d ? 'Drafted ' + md(day(d.slot)) : s && s.paused ? 'Paused' : '', s && s.audience ? 'for ' + s.audience : '', !s && a && a.act === 'confirm' ? 'its definition waits for your word' : '',
|
|
2110
|
+
s && s.next && !s.paused ? 'next ' + s.next : '', about ? 'in ' + (M.board.name || 'this product') + ', about ' + about.name : ''].filter(Boolean).join(' · ');
|
|
2111
|
+
const lead = d && d.lead ? d.lead : s && s.latest && s.latest.lead ? s.latest.lead : '';
|
|
2112
|
+
const key = G.kind + ':' + G.x.id;
|
|
2113
|
+
return `<div class="ins-nrow${selRow === key ? ' sel' : ''}" role="listitem"><button type="button" class="nm" ${G.kind === 'setup' ? `data-setup="${esc(G.x.id)}"` : `data-rpt="${esc(G.x.id)}"`}><b>${esc(G.x.name)}</b><small>${esc(sub)}</small>${lead ? `<span class="hl">${esc(lead)}</span>` : ''}</button>`
|
|
2114
|
+
+ `<span class="ac"><span class="note">${esc(a.label.charAt(0).toLowerCase() + a.label.slice(1))}</span><button type="button" class="cz-ib sm solid" data-nact="${esc(G.x.id)}" aria-label="${esc(a.label + ': ' + G.x.name)}" title="${esc(a.label)}">${SV(I.chk, 2.4)}</button></span></div>`;
|
|
2115
|
+
}
|
|
2116
|
+
/** A proposal from Home that is ready: what was asked, and its ✓, which opens the proposal (Set it up is one press there). */
|
|
2117
|
+
const askNeedHtml = (a) => `<div class="ins-nrow" role="listitem"><button type="button" class="nm" data-ask="${esc(a.id)}"><b>${esc(a.proposal && a.proposal.name ? a.proposal.name : a.text)}</b><small>${esc('You asked in Home: ' + a.text)}</small></button>`
|
|
2118
|
+
+ `<span class="ac"><span class="note">set it up</span><button type="button" class="cz-ib sm solid" data-ask="${esc(a.id)}" aria-label="${esc('Open the proposal: ' + a.text)}" title="Open the proposal">${SV(I.chk, 2.4)}</button></span></div>`;
|
|
2119
|
+
/** The one ✓ of a waiting report: Issue, Confirm the definition, Move to <product> or Resume. One press, then the panel reads again. */
|
|
2120
|
+
async function needPress(b) {
|
|
2121
|
+
const id = b.dataset.nact, a = (M.reports.acts || {})[id]; if (!a) return;
|
|
2122
|
+
const G = reportGroups().find((x) => x.x.id === id), name = G ? G.x.name : id;
|
|
2123
|
+
b.disabled = true;
|
|
2124
|
+
const r = a.act === 'issue' ? (a.sha ? await pmPost('/api/report/issue', { ws: M.board.ws, cadence: id, slot: a.slot, sha: a.sha }) : { ok: false, error: 'The draft could not be read, so it was not issued. Open it and issue it from the reader.' })
|
|
2125
|
+
: a.act === 'confirm' ? await pmPost('/api/report-setups/confirm', { ws: M.board.ws, id })
|
|
2126
|
+
: a.act === 'move' ? await pmPost('/api/report-setups/move', { ws: M.board.ws, id, to: a.to })
|
|
2127
|
+
: await pmPost('/api/report-setups/resume', { ws: M.board.ws, id });
|
|
2128
|
+
if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'Nothing changed. The server did not say why.'); return; }
|
|
2129
|
+
toast(a.act === 'issue' ? 'Issued ' + name + '. Send it from your own mail.' : a.act === 'confirm' ? 'Confirmed the definition of ' + name + '. Its drafts follow it as it is.'
|
|
2130
|
+
: a.act === 'move' ? 'Moved ' + name + ' to ' + a.toName + '. It drafts there from now on.' : 'Resumed ' + name + '.');
|
|
2131
|
+
await load();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/* ── the first dashboard, drafted from the OKR file (RgR3Dash) ──────────────────────────────────────────────────────────────────────
|
|
2135
|
+
* With no saved dashboard, the panel offers one tile per key result of the OKR file (A's model.okrFile): a key result with a reader shows its
|
|
2136
|
+
* reading with the reader and its date; one without shows what it needs and the connector ✓. Nothing is a number without a reading. ✓ Keep saves
|
|
2137
|
+
* it as a dashboard of the read key results (POST /api/premium-dashboards/save); the waiting ones join it once a reader reads them. */
|
|
2138
|
+
function draftedDash() {
|
|
2139
|
+
const F = M.okrFile; if (!F || !F.total) return null;
|
|
2140
|
+
const tiles = F.objectives.flatMap((o) => o.krs.map((k) => {
|
|
2141
|
+
const g = k.goal ? M.goals.find((x) => x.id === k.goal) : null, rd = g && g.reading;
|
|
2142
|
+
if (k.read && g) return { k, read: true, big: g.nowTxt || (rd && rd.txt) || '—', dot: rd && rd.dot === 'behind' ? 'risk' : rd && rd.dot === 'broken' ? 'late' : 'ok', meta: [rd && rd.source ? rd.source : 'its reader', rd && rd.at ? md(day(rd.at)) : ''].filter(Boolean).join(' · ') };
|
|
2143
|
+
if (k.decision) return { k, read: false, big: 'Not yet', dot: 'none', meta: k.due ? 'a decision, due ' + md(day(k.due)) : 'a decision' };
|
|
2144
|
+
return { k, read: false, big: '—', dot: 'none', meta: k.need ? 'needs ' + k.need : 'no source named', need: k.need || '' };
|
|
2145
|
+
}));
|
|
2146
|
+
return { name: (F.period ? F.period.label + ' ' : '') + 'key results', tiles, charts: tiles.filter((t) => t.read).map((t) => t.k.goal), nRead: tiles.filter((t) => t.read).length };
|
|
2147
|
+
}
|
|
2148
|
+
const okrTileHtml = (t) => `<div class="ins-dtile${t.read ? '' : ' w'}" role="listitem"><span class="lb"><span class="kr">${esc(t.k.token)}</span>${esc(t.k.title)}</span><b class="big">${esc(t.big)}</b>`
|
|
2149
|
+
+ `<span class="ft"><span class="note"><span class="ins-tdot ${t.dot}" aria-hidden="true"></span>${esc(t.meta)}</span>${t.need ? `<button type="button" class="cz-ib sm" data-ddash="con" aria-label="${esc('Connect ' + t.need)}" title="${esc('Connect ' + t.need)}">${SV(I.chk, 2.4)}</button>` : ''}</span></div>`;
|
|
2150
|
+
function draftedCard(D) {
|
|
2151
|
+
return `<div class="ins-dcard ins-ddraft" role="listitem"><span class="ins-dh"><b>${esc(D.name)}</b><span class="cz-pill q">Drafted</span></span>`
|
|
2152
|
+
+ `<p class="note">${esc('I drafted it from your OKR file: one tile per key result. ' + (D.nRead ? D.nRead + ' read' + (D.tiles.length - D.nRead ? ', ' + (D.tiles.length - D.nRead) + ' wait on a source, and each tile says which.' : '.') : 'None has a reading yet, and each tile says what it needs.'))}</p>`
|
|
2153
|
+
+ `<div class="ins-dtiles" role="list">${D.tiles.slice(0, 8).map(okrTileHtml).join('')}</div>`
|
|
2154
|
+
+ `<span class="ins-dact">${D.nRead ? `<span class="note">keep it, and it opens first</span><button type="button" class="cz-ib sm solid" data-ddash="keep" aria-label="Keep this dashboard" title="Keep this dashboard">${SV(I.chk, 2.4)}</button>` : '<span class="note">kept once one key result has a reading</span>'}</span></div>`;
|
|
2155
|
+
}
|
|
2156
|
+
async function draftedPress(b) {
|
|
2157
|
+
if (b.dataset.ddash === 'con') { import('./cozy-settings.js').then((S) => S.openSettings('connectors')).catch(() => toast('Open Settings › Connectors.')); return; }
|
|
2158
|
+
const D = draftedDash(); if (!D || !D.charts.length) return;
|
|
2159
|
+
b.disabled = true;
|
|
2160
|
+
const r = await saveDash({ name: D.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: D.charts, main: true });
|
|
2161
|
+
if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'The dashboard could not be saved. Nothing changed.'); return; }
|
|
2162
|
+
toast('Kept ' + D.name + '. It opens first.'); await load();
|
|
2163
|
+
}
|
|
1940
2164
|
/* the ready-made group before Yours (batch 17 item 1, PbA6): read once per open of the panel, again after a switch */
|
|
1941
|
-
const LIB = { L: null, read: false, open: new Set() };
|
|
1942
|
-
const drawLib = () => { const el = $('#readyLib'); if (el) el.
|
|
2165
|
+
const LIB = { L: null, read: false, open: new Set(), fold: {} };
|
|
2166
|
+
const drawLib = () => { const el = $('#readyLib'); if (el) { el.outerHTML = libFolds(LIB.L) || '<div id="readyLib"></div>'; wireFolds(); } };
|
|
2167
|
+
/* + in the panel header (RgR4Make): the drafted report of this view, never a blank chat. With all products on screen there is no one product to
|
|
2168
|
+
* draft for, so it keeps its link to Home. */
|
|
2169
|
+
$('#newRep').addEventListener('click', (e) => {
|
|
2170
|
+
if (PF || !M || !M.board || !M.board.ws) return;
|
|
2171
|
+
e.preventDefault();
|
|
2172
|
+
const D = curDash(); RI.openDraft(D && D.saved ? 'dashboard:' + D.id : 'view');
|
|
2173
|
+
});
|
|
1943
2174
|
RL.wireLib($('#repBody'), { get ws() { return M && M.board ? M.board.ws : ''; }, open: LIB.open, toast: (t) => toast(t), redraw: drawLib,
|
|
1944
2175
|
reload: async () => { LIB.L = await RL.readLib(M.board.ws); drawLib(); } });
|
|
1945
2176
|
$('#repBody').addEventListener('click', async (e) => {
|
|
2177
|
+
const na = e.target.closest('[data-nact]'); if (na) { if (!na.disabled) needPress(na); return; }
|
|
2178
|
+
const dk = e.target.closest('[data-ddash]'); if (dk) { draftedPress(dk); return; }
|
|
1946
2179
|
const d = e.target.closest('[data-d]'); if (d) { setView(d.dataset.d); return; }
|
|
1947
2180
|
const fx = e.target.closest('[data-fx]');
|
|
1948
2181
|
if (fx) {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
<!-- Reports and dashboards (⋯; the old Library): Found in your work, requests from Home, the reports, then the dashboards -->
|
|
70
70
|
<aside class="ins-side ins-lib" id="libSide" role="dialog" aria-modal="false" aria-labelledby="libTitle" tabindex="-1" hidden>
|
|
71
|
-
<div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report
|
|
71
|
+
<div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report: Claude drafts it from this view" title="New report: Claude drafts it from this view"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></a></span><button type="button" class="cz-ib" id="libX" aria-label="Close" title="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
|
|
72
72
|
<div class="ins-sb" id="repBody"></div>
|
|
73
73
|
<span class="vh" id="repCap"></span>
|
|
74
74
|
</aside>
|