create-pmos 0.514.0 → 0.515.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 +14 -48
- 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/briefing-premium-insights-test.js +12 -12
- package/workspace/briefing-premium-metrics-test.js +1 -1
- package/workspace/changelog.json +13 -0
- package/workspace/guide/index.md +2 -1
- package/workspace/guide/intelligence-okr-goals.md +39 -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/routes-insights-premium.js +27 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/cozy-insights.css +26 -1
- package/workspace/public/pm/premium-insights.js +167 -32
- package/workspace/test-files.json +1 -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.';
|
|
@@ -1931,15 +2058,23 @@ function drawList() {
|
|
|
1931
2058
|
+ 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
2059
|
const empty = R.missing ? '<div class="ins-empty"><b>The report files could not be read</b>, so none is listed.</div>'
|
|
1933
2060
|
: 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
|
-
const
|
|
1937
|
-
|
|
2061
|
+
/* Intelligence redesign A (IgA5Reports): what needs you first (a draft waiting for your ✓, a proposal ready), then the ready-made set folded
|
|
2062
|
+
* (its switches stay under the fold), then Yours, then the dashboards. */
|
|
2063
|
+
const waits = reps.filter((G) => repState(G).draft), waitAsks = asks.filter((a) => a.state === 'ready');
|
|
2064
|
+
const rest = reps.filter((G) => !waits.includes(G)), restAsks = asks.filter((a) => !waitAsks.includes(a));
|
|
2065
|
+
const head3 = '<div class="ins-rl ins-rlh" role="presentation"><span>Report</span><span>Next</span><span>Status</span></div>';
|
|
2066
|
+
const needs = waits.length + waitAsks.length ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${waits.length + waitAsks.length}</span></div><div class="ins-rtbl" id="repNeeds">${head3}<div role="list">${waits.map(repRowHtml).join('')}${waitAsks.map(askRowHtml).join('')}</div></div>` : '';
|
|
2067
|
+
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>`
|
|
2068
|
+
: !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>' : '';
|
|
2069
|
+
const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length ? dashes.length + ' · a dashboard is a saved view of Goals' : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dashes.map(dashCard).join('')}</div>`;
|
|
2070
|
+
const lib = RL.libHtml(LIB.L, { open: LIB.open, yours: false });
|
|
2071
|
+
$('#repBody').innerHTML = `${oneNote()}${needs}<details class="ins-libfold"${LIB.fold ? ' open' : ''}><summary><b>Drafting on their moment</b> <span class="note">ready-made reports, nothing to set up</span></summary><div id="readyLib">${lib}</div></details><div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
|
|
2072
|
+
const fd = $('#repBody .ins-libfold'); if (fd) fd.addEventListener('toggle', () => { LIB.fold = fd.open; });
|
|
1938
2073
|
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
2074
|
}
|
|
1940
2075
|
/* 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.innerHTML = RL.libHtml(LIB.L, { open: LIB.open, yours:
|
|
2076
|
+
const LIB = { L: null, read: false, open: new Set(), fold: false };
|
|
2077
|
+
const drawLib = () => { const el = $('#readyLib'); if (el) el.innerHTML = RL.libHtml(LIB.L, { open: LIB.open, yours: false }); };
|
|
1943
2078
|
RL.wireLib($('#repBody'), { get ws() { return M && M.board ? M.board.ws : ''; }, open: LIB.open, toast: (t) => toast(t), redraw: drawLib,
|
|
1944
2079
|
reload: async () => { LIB.L = await RL.readLib(M.board.ws); drawLib(); } });
|
|
1945
2080
|
$('#repBody').addEventListener('click', async (e) => {
|