feat: publish mileage assessment freshness

This commit is contained in:
kkfluous
2026-08-07 17:49:52 +08:00
parent 8a7ce7eccf
commit bca0f7fe82
6 changed files with 32 additions and 15 deletions
+2 -13
View File
@@ -13,6 +13,7 @@ import type { TargetSummary, TargetVehicle, TargetYearlyAssessment, TrendPoint }
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
import Blur from '../../components/Blur';
import { weightedCompletionRate } from '../../shared/analytics/metrics';
import { formatMileageSnapshotDateTime } from './data-freshness';
import MileageAttributionView from './MileageAttributionView';
import {
filterAttributionVehicles,
@@ -30,11 +31,6 @@ function getDefaultDate(): string {
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
function getCurrentDateLabel(): string {
const now = new Date();
return `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
}
function fmtKm(value: number): string {
if (value >= 10000) return (value / 10000).toFixed(2) + '万';
return value.toLocaleString();
@@ -53,12 +49,6 @@ function resolveAssessmentYear(target: TargetSummary, requestedYear?: number): n
return getTargetAssessment(target, requestedYear)?.yearNumber;
}
function fmtDateLabel(date: string | null): string {
if (!date) return '';
const [year, month, day] = date.split('-');
return `${year}.${Number(month)}.${Number(day)}`;
}
function shortTargetName(name: string): string {
// Extract the number and a short description
const match = name.match(/(\d+)[辆台](.+)/);
@@ -74,7 +64,6 @@ function shortTargetName(name: string): string {
}
export default function StatisticsView() {
const currentDateLabel = getCurrentDateLabel();
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
parseMileageDrillContext(window.location.search)
));
@@ -642,7 +631,7 @@ export default function StatisticsView() {
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}</p>
<p className="text-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
<p className="text-[8px] font-bold text-slate-300">
{assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}
{formatMileageSnapshotDateTime(target.snapshotUpdatedAt)}
</p>
</div>
<div className="space-y-0.5">
+7 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { formatMileageSnapshotTime } from './data-freshness.js';
import { formatMileageSnapshotDateTime, formatMileageSnapshotTime } from './data-freshness.js';
test('formats snapshot instants in Asia/Shanghai', () => {
assert.equal(formatMileageSnapshotTime('2026-08-07T07:45:06.000Z'), '15:45:06');
@@ -10,3 +10,9 @@ test('keeps historical date snapshots explicit', () => {
assert.equal(formatMileageSnapshotTime('2026-08-06'), '2026-08-06');
assert.equal(formatMileageSnapshotTime(null), '--:--:--');
});
test('formats assessment snapshot instants with the business date and minute', () => {
assert.equal(formatMileageSnapshotDateTime('2026-08-07T09:45:01.000Z'), '2026.8.7 17:45');
assert.equal(formatMileageSnapshotDateTime(null), '--');
assert.equal(formatMileageSnapshotDateTime('not-a-date'), 'not-a-date');
});
+18
View File
@@ -12,3 +12,21 @@ export function formatMileageSnapshotTime(value: string | null): string {
hourCycle: 'h23',
}).format(date);
}
export function formatMileageSnapshotDateTime(value: string | null): string {
if (!value) return '--';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
hourCycle: 'h23',
}).formatToParts(date);
const part = (type: Intl.DateTimeFormatPartTypes) => parts.find(item => item.type === type)?.value || '';
return `${part('year')}.${part('month')}.${part('day')} ${part('hour')}:${part('minute')}`;
}
+1
View File
@@ -64,6 +64,7 @@ export interface MonitoringData {
export interface TargetSummary {
id: number;
targetName: string;
snapshotUpdatedAt: string | null;
vehicleCount: number;
totalMileagePerVehicle: number;
annualMileagePerVehicle: number;
+3 -1
View File
@@ -26,7 +26,8 @@ app.get('/', async (c) => {
SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
SUM(current_year_mileage_task) as current_year_target,
SUM(current_year_mileage) as current_year_completed,
MAX(current_year_assessment_end_date) as year_end_date
MAX(current_year_assessment_end_date) as year_end_date,
DATE_FORMAT(MAX(update_time), '%Y-%m-%dT%H:%i:%s+08:00') as snapshot_updated_at
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
GROUP BY target_id
`) as [any[], unknown];
@@ -199,6 +200,7 @@ app.get('/', async (c) => {
return {
id: t.id,
targetName: t.target_name,
snapshotUpdatedAt: s.snapshot_updated_at || null,
vehicleCount: Number(s.total) || t.vehicle_count,
totalMileagePerVehicle: Number(t.total_mileage_per_vehicle),
annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle),