feat(board): v2 kpi cards (area chart, capped chips, done bar)
This commit is contained in:
parent
e5071f8750
commit
9553461130
90
src/server/board/kpis.ts
Normal file
90
src/server/board/kpis.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
capChips, doneStats, backlogSeries, areaPath, laneChips, dayStart,
|
||||
} from './viewmodel.js';
|
||||
|
||||
/** Static skeleton: four KPI cards; values are filled client-side. */
|
||||
export function kpiSkeletonHtml(): string {
|
||||
return `
|
||||
<section class="b2-kpis" aria-label="Kennzahlen">
|
||||
<div class="b2-kpi b2-glass" id="kpiOpen">
|
||||
<div class="b2-lbl">Open</div>
|
||||
<div class="b2-val"><span data-k="count">–</span> <small data-k="total"></small></div>
|
||||
<div class="b2-miniarea" data-k="area"></div>
|
||||
</div>
|
||||
<div class="b2-kpi b2-glass" id="kpiInProgress">
|
||||
<div class="b2-lbl">In Progress</div>
|
||||
<div class="b2-val"><span data-k="count">–</span></div>
|
||||
<div class="b2-chips" data-k="chips"></div>
|
||||
</div>
|
||||
<div class="b2-kpi b2-glass" id="kpiReview">
|
||||
<div class="b2-lbl">Review</div>
|
||||
<div class="b2-val"><span data-k="count">–</span></div>
|
||||
<div class="b2-chips" data-k="chips"></div>
|
||||
</div>
|
||||
<div class="b2-kpi b2-glass" id="kpiDone">
|
||||
<div class="b2-lbl">Done</div>
|
||||
<div class="b2-val"><span data-k="count">–</span> <small data-k="total"></small></div>
|
||||
<div class="b2-cap-r" data-k="cap"></div>
|
||||
<div class="b2-pbar"><i data-k="bar"></i></div>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline script: injects the pure viewmodel helpers (toString) plus the shared
|
||||
* DAY_MS/dayStart they reference, and defines window.__b2UpdateKpis(tasks,
|
||||
* agentColor), called by the data layer on every refresh. agentColor(name)
|
||||
* comes from the columns module (existing per-agent palette).
|
||||
*/
|
||||
export function kpiJs(): string {
|
||||
return `
|
||||
var DAY_MS = 24 * 3600 * 1000;
|
||||
${dayStart.toString()}
|
||||
${capChips.toString()}
|
||||
${doneStats.toString()}
|
||||
${backlogSeries.toString()}
|
||||
${areaPath.toString()}
|
||||
${laneChips.toString()}
|
||||
window.__b2UpdateKpis = function (tasks, agentColor) {
|
||||
function setK(card, key, fn) {
|
||||
var el = document.querySelector('#' + card + ' [data-k="' + key + '"]');
|
||||
if (el) fn(el);
|
||||
}
|
||||
// Open
|
||||
var open = tasks.filter(function (t) { return t.status === 'open'; });
|
||||
setK('kpiOpen', 'count', function (el) { el.textContent = String(open.length); });
|
||||
setK('kpiOpen', 'total', function (el) {
|
||||
el.textContent = '/ ' + tasks.filter(function (t) { return t.status !== 'cancelled'; }).length;
|
||||
});
|
||||
setK('kpiOpen', 'area', function (el) {
|
||||
var p = areaPath(backlogSeries(tasks, 14), 120, 26);
|
||||
el.innerHTML = '<svg width="100%" height="26" viewBox="0 0 120 26" preserveAspectRatio="none">'
|
||||
+ '<path fill="rgba(148,163,184,.18)" d="' + p.area + '"/>'
|
||||
+ '<path class="b2-spark" fill="none" stroke="#94a3b8" stroke-width="1.5" d="' + p.line + '"/></svg>';
|
||||
});
|
||||
// In Progress + Review chips (max 3, then +n)
|
||||
['in_progress', 'review'].forEach(function (status) {
|
||||
var card = status === 'review' ? 'kpiReview' : 'kpiInProgress';
|
||||
var chips = laneChips(tasks, status);
|
||||
var capped = capChips(chips, 3);
|
||||
setK(card, 'count', function (el) { el.textContent = String(chips.length); });
|
||||
setK(card, 'chips', function (el) {
|
||||
el.innerHTML = capped.visible.map(function (c) {
|
||||
var color = agentColor ? agentColor(c.name) : '#a5b4fc';
|
||||
var initial = c.name.charAt(0).toUpperCase();
|
||||
var time = c.minutes == null ? '' : (status === 'review' ? 'prüft ' : '') + c.minutes + 'm';
|
||||
var dot = status === 'in_progress' ? '<span class="b2-cdot"></span>' : '';
|
||||
return '<span class="b2-achip"><i style="background:' + color + '">' + initial + '</i>' + dot + time + '</span>';
|
||||
}).join('') + (capped.hidden > 0 ? '<span class="b2-achip b2-more">+' + capped.hidden + '</span>' : '');
|
||||
});
|
||||
});
|
||||
// Done
|
||||
var ds = doneStats(tasks);
|
||||
setK('kpiDone', 'count', function (el) { el.textContent = String(ds.done); });
|
||||
setK('kpiDone', 'total', function (el) { el.textContent = '/ ' + ds.total; });
|
||||
setK('kpiDone', 'cap', function (el) {
|
||||
el.textContent = ds.pct + '% · +' + ds.doneThisWeek + ' diese Woche';
|
||||
});
|
||||
setK('kpiDone', 'bar', function (el) { el.style.width = ds.pct + '%'; });
|
||||
};`;
|
||||
}
|
||||
@ -44,7 +44,8 @@ export function doneStats(
|
||||
return { done: doneTasks.length, total: relevant.length, pct, doneThisWeek };
|
||||
}
|
||||
|
||||
function dayStart(ts: number): number {
|
||||
/** Local midnight for a timestamp (exported: co-injected into the browser page). */
|
||||
export function dayStart(ts: number): number {
|
||||
const d = new Date(ts);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d.getTime();
|
||||
|
||||
30
tests/boardV2-kpis.test.ts
Normal file
30
tests/boardV2-kpis.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { kpiSkeletonHtml, kpiJs } from '../src/server/board/kpis.js';
|
||||
|
||||
describe('kpiSkeletonHtml', () => {
|
||||
it('renders the four KPI cards with stable ids', () => {
|
||||
const h = kpiSkeletonHtml();
|
||||
for (const id of ['kpiOpen', 'kpiInProgress', 'kpiReview', 'kpiDone']) {
|
||||
expect(h).toContain(`id="${id}"`);
|
||||
}
|
||||
expect(h).toContain('Open');
|
||||
expect(h).toContain('In Progress');
|
||||
expect(h).toContain('Review');
|
||||
expect(h).toContain('Done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('kpiJs', () => {
|
||||
it('injects the pure helpers and an updateKpis entry point', () => {
|
||||
const js = kpiJs();
|
||||
for (const fn of ['capChips', 'doneStats', 'backlogSeries', 'areaPath', 'laneChips']) {
|
||||
expect(js).toContain(`function ${fn}`);
|
||||
}
|
||||
expect(js).toContain('window.__b2UpdateKpis');
|
||||
});
|
||||
it('co-injects the shared constants the helpers depend on', () => {
|
||||
const js = kpiJs();
|
||||
expect(js).toContain('DAY_MS');
|
||||
expect(js).toContain('function dayStart');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user