refactor: add weighted mileage completion metric
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user