diff --git a/.project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md b/.project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md new file mode 100644 index 0000000..f71db7c --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md @@ -0,0 +1,58 @@ +# Task: Add horizontal bars to dashboard rankings + +## Identity + +- Task ID: 20260902-dashboard-ranking-bars-c37a9e +- Mode: Feature +- Branch: main +- Worktree: /Users/inmanx/Documents/lwltAPI +- Base commit: f11aa6416b1e88c00fc6985b71130f1e3ad48b63 +- Owner: codex +- Status: Ready for integration + +## Scope + +- Add horizontal bar visualization to both the task-type ranking and employee-operation ranking on `/operations-dashboard`. +- Preserve the compact fixed-height ranking panels, operation/success count labels, ordering, scrolling, and click-through filters. +- Keep completion rate and all percentage labels out of the leadership presentation. +- Update browser cache versions and focused dashboard regression assertions. + +## Intent And Constraints + +- Use the existing vanilla HTML/CSS/JavaScript stack and dashboard response; do not introduce a chart library or change the operations API. +- Scale bars independently within each ranking so the leading operation count occupies the available width and the remaining rows are immediately comparable. +- Encode total operations as a muted outer bar and successful operations as a darker inner bar on the same scale; keep the exact counts visible as text. +- Preserve leadership-only read access, business-safe copy, task drill-through, employee filtering, and all task/ERP authorization boundaries. +- Do not restart, deploy, mutate runtime tasks, access ERP, or add an organization concept. + +## Outcome + +- Added one reusable ranking-bar renderer that clamps numeric inputs and normalizes total/success widths against the largest operation count in the current ranking. +- Added the same two-layer horizontal bar to every task-type and employee row: the light fill shows operation volume and the dark-green inner fill shows successful volume. +- Retained explicit “操作 N / 成功 N” labels and accessible button names; no percentage or completion-rate text was reintroduced. +- Expanded the compact row grid to include the bar without changing the fixed 304px panel height or internal scrolling. On narrow screens, the bar moves to a second row beneath the identity and counts. +- Preserved task-type and employee click-through behavior, filters, task detail, ranking order, and business-safe dashboard language. +- Updated static asset cache versions and regression assertions for the ranking-bar renderer. +- The running 8786 service serves the static change directly. No restart, deployment, database/task mutation, ERP access, or external send was performed. + +## Verification + +- `node --check LianSyn-platform/app.js`: passed with the bundled Node runtime. +- Focused account/dashboard regression: 8/8 passed. +- `node --run check:repo`: 10/10 passed. +- `node --run check`: passed. +- `node --run test:control-plane`: 153/153 passed. +- `node --run test:legacy`: 256/256 passed. +- `node --run build`: passed. +- `git diff --check`: passed. +- Authenticated browser verification on `http://127.0.0.1:8786/operations-dashboard?bars=1` loaded 359 operations, 251 successes, 23 task types, and 1 participating employee. +- Visual verification confirmed that the task-type ranking visibly compares 43/27/26/24 operation counts, the success layer remains readable inside each total bar, and the employee row shows 359 total operations against 251 successes. +- Interaction verification confirmed that task-type click-through selected “删除订单”, narrowed the table to 43 operations, and kept completion-rate language absent. + +## Follow-ups + +- The current standard data contains one employee, so employee-to-employee bar comparison will become visible once additional accounts have task activity; the same normalization is already applied. + +## Promotion Candidates + +- None. This is a focused visual encoding of the already accepted count-based leadership rankings and does not change product, authorization, data, or API semantics. diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index 8ffbbd1..0de0115 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -1322,6 +1322,25 @@ function renderOperationsDashboardSummary() { } } +function operationsDashboardRankBar(total, completed, maximum) { + const safeMaximum = Math.max(1, Number(maximum || 0)); + const safeTotal = Math.max(0, Number(total || 0)); + const safeCompleted = Math.min(safeTotal, Math.max(0, Number(completed || 0))); + const bar = el('span', 'operations-dashboard-rank-bar'); + bar.setAttribute('aria-hidden', 'true'); + if (safeTotal > 0) { + const totalFill = el('i', 'is-total'); + totalFill.style.width = `${Math.min(100, (safeTotal / safeMaximum) * 100)}%`; + bar.append(totalFill); + } + if (safeCompleted > 0) { + const successFill = el('i', 'is-success'); + successFill.style.width = `${Math.min(100, (safeCompleted / safeMaximum) * 100)}%`; + bar.append(successFill); + } + return bar; +} + function renderOperationsDashboardBusinesses() { const container = $('#operationsDashboardBusinesses'); if (!container) return; @@ -1331,6 +1350,7 @@ function renderOperationsDashboardBusinesses() { container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有任务类型数据。')); return; } + const maximum = Math.max(...businesses.map((item) => Number(item.total || 0)), 1); for (const [index, item] of businesses.entries()) { const button = el('button', 'operations-dashboard-business-row'); button.type = 'button'; @@ -1340,9 +1360,10 @@ function renderOperationsDashboardBusinesses() { el('span', 'operations-dashboard-rank-number', String(index + 1).padStart(2, '0')), el('strong', 'operations-dashboard-rank-name', operationsDashboardBusinessLabel(item.label)) ); + button.append(operationsDashboardRankBar(item.total, item.completed, maximum)); const metrics = el('span', 'operations-dashboard-rank-metrics'); metrics.append( - el('span', '', `操作 ${Number(item.total || 0)}`), + el('span', 'is-total', `操作 ${Number(item.total || 0)}`), el('span', 'is-success', `成功 ${Number(item.completed || 0)}`) ); button.append(metrics); @@ -1364,6 +1385,7 @@ function renderOperationsDashboardUsers() { container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有人员工作数据。')); return; } + const maximum = Math.max(...users.map((item) => Number(item.total || 0)), 1); for (const [index, item] of users.entries()) { const button = el('button', 'operations-dashboard-user-row'); button.type = 'button'; @@ -1379,12 +1401,13 @@ function renderOperationsDashboardUsers() { el('strong', '', item.actor.username), el('small', '', accountRoleLabel(item.actor.role)) ); + const bar = operationsDashboardRankBar(item.total, item.completed, maximum); const metrics = el('div', 'operations-dashboard-user-metrics'); metrics.append( - el('span', '', `操作 ${Number(item.total || 0)}`), + el('span', 'is-total', `操作 ${Number(item.total || 0)}`), el('span', 'is-success', `成功 ${Number(item.completed || 0)}`) ); - button.append(identity, metrics); + button.append(identity, bar, metrics); container.append(button); } } diff --git a/LianSyn-platform/index.html b/LianSyn-platform/index.html index 54e3883..3ab5e93 100644 --- a/LianSyn-platform/index.html +++ b/LianSyn-platform/index.html @@ -5,7 +5,7 @@