refactor: add weighted mileage completion metric

This commit is contained in:
kkfluous
2026-08-07 13:51:58 +08:00
parent 17c839a35e
commit 13e720c2c3
3 changed files with 56 additions and 1 deletions
+8 -1
View File
@@ -12,6 +12,7 @@ import {
import type { TargetSummary, TargetVehicle, TargetYearlyAssessment, TrendPoint } from './types';
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
import Blur from '../../components/Blur';
import { weightedCompletionRate } from '../../shared/analytics/metrics';
function getDefaultDate(): string {
const now = new Date();
@@ -83,6 +84,12 @@ export default function StatisticsView() {
const selectedDaysLeft = selectedAssessment?.daysLeft ?? selectedTarget?.daysLeft ?? 0;
const selectedDailyTarget = selectedAssessment?.dailyTarget ?? (selectedDaysLeft > 0 ? selectedRemaining / selectedDaysLeft : 0);
const selectedQualifiedRate = selectedAssessment?.qualifiedRate ?? (selectedTarget?.vehicleCount ? selectedTarget.yearQualifiedCount / selectedTarget.vehicleCount * 100 : 0);
const selectedYearCompletionRate = weightedCompletionRate(
targets.flatMap(target => {
const assessment = getTargetAssessment(target, assessmentYearMap[target.id]);
return assessment ? [{ completed: assessment.completed, target: assessment.target }] : [];
}),
);
const latestTrend = trendData[trendData.length - 1];
const previousTrend = trendData[trendData.length - 2];
const trendDelta = latestTrend && previousTrend ? latestTrend.mileage - previousTrend.mileage : 0;
@@ -541,7 +548,7 @@ export default function StatisticsView() {
<span className="text-slate-700">|</span>
<span className="text-slate-500"> <span className="text-white font-black">{targets.reduce((sum, t) => sum + t.vehicleCount, 0)}</span> </span>
<span className="text-slate-700">|</span>
<span className="text-slate-500"> <span className="text-white font-black">{targets.length > 0 ? (targets.reduce((sum, t) => sum + (getTargetAssessment(t, assessmentYearMap[t.id])?.completionRate ?? t.avgCompletion), 0) / targets.length).toFixed(1) : '0.0'}</span> <span className="text-blue-400">%</span></span>
<span className="text-slate-500"> <span className="text-white font-black">{selectedYearCompletionRate.toFixed(1)}</span> <span className="text-blue-400">%</span></span>
</div>
</div>
<div className="flex items-center gap-2">
+30
View File
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { weightedCompletionRate } from './metrics.js';
test('weights completion by target mileage', () => {
const result = weightedCompletionRate([
{ completed: 100, target: 100 },
{ completed: 0, target: 900 },
]);
assert.equal(result, 10);
});
test('allows completion above 100 percent', () => {
const result = weightedCompletionRate([
{ completed: 120, target: 100 },
]);
assert.equal(result, 120);
});
test('ignores invalid and zero targets', () => {
const result = weightedCompletionRate([
{ completed: 50, target: 0 },
{ completed: Number.NaN, target: 100 },
{ completed: 30, target: 60 },
]);
assert.equal(result, 50);
});
+18
View File
@@ -0,0 +1,18 @@
export interface ProgressMeasure {
completed: number;
target: number;
}
export function weightedCompletionRate(measures: ProgressMeasure[]): number {
let completed = 0;
let target = 0;
for (const measure of measures) {
if (!Number.isFinite(measure.target) || measure.target <= 0) continue;
if (!Number.isFinite(measure.completed)) continue;
completed += Math.max(0, measure.completed);
target += measure.target;
}
return target > 0 ? completed / target * 100 : 0;
}