refactor: build drillable mileage daily report

This commit is contained in:
kkfluous
2026-08-07 14:27:19 +08:00
parent 91031bd034
commit b2044109e3
4 changed files with 690 additions and 121 deletions
+51 -110
View File
@@ -8,6 +8,16 @@ import type {
TrendPoint,
} from './types';
import { fetchJson } from '../../auth/api-client';
import {
buildDailyReport,
type DailyReportData,
} from './daily-report';
export type {
DailyReportData,
DailyReportModel,
DailyReportVehicle,
} from './daily-report';
const BASE = '/api/mileage';
@@ -123,38 +133,6 @@ export async function fetchVehicleRecent(
);
}
export interface DailyReportModel {
id: number;
name: string;
count: number;
today: number;
total: number;
completion: number;
active: number;
zero: number;
dailyNeed: number;
}
export interface DailyReportVehicle {
plate: string;
model: string;
status: string;
customer: string;
today?: number;
completion?: number;
}
export interface DailyReportData {
reportDate: string;
updatedAt: string;
models: DailyReportModel[];
trend: { date: string; value: number }[];
topVehicles: DailyReportVehicle[];
zeroRisk: DailyReportVehicle[];
qualifiedCount: number;
halfQualifiedCount: number;
}
function reportDateFromUpdatedAt(updatedAt: string): string {
if (/^\d{4}-\d{2}-\d{2}$/.test(updatedAt)) return updatedAt;
const d = new Date(updatedAt);
@@ -162,95 +140,58 @@ function reportDateFromUpdatedAt(updatedAt: string): string {
return new Date().toISOString().slice(0, 10);
}
function compactTargetName(name: string): string {
return name
.replace(/^羚牛/, '')
.replace(/辆/g, '台')
.replace(/4\.5T普货/g, '普货')
.replace(/4\.5T冷链车/g, '冷链车')
.replace(/4\.5T冷链/g, '冷链车');
}
let dailyReportRequest: Promise<DailyReportData> | null = null;
let dailyReportCache: { data: DailyReportData; expiresAt: number } | null = null;
const DAILY_REPORT_CACHE_MS = 60_000;
function normalizeStatus(status: string | null): string {
if (!status) return '未标注';
if (status === '自营' || status === '租赁') return status;
if (/租/.test(status)) return '租赁';
if (/自/.test(status)) return '自营';
if (/库|Inventory/i.test(status)) return '在库';
return status;
}
export async function fetchDailyReport(): Promise<DailyReportData> {
const [targets, trend, monitoring, topMonitoring] = await Promise.all([
async function loadDailyReport(): Promise<DailyReportData> {
const [targets, monitoring] = await Promise.all([
fetchTargets(),
fetchTrend(undefined, 7),
fetchMonitoring({ limit: 1 }),
fetchMonitoring({ sortBy: 'today', sortOrder: 'desc', limit: 5 }),
]);
const reportDate = monitoring.dateRange?.end || reportDateFromUpdatedAt(monitoring.updatedAt);
const targetVehiclesEntries = await Promise.all(
const targetVehicles = await Promise.all(
targets.map(async target => {
const vehicles = await fetchTargetVehicles(target.id);
return [target.id, vehicles] as const;
const vehicles = await fetchTargetVehicles(target.id, reportDate);
return { target, vehicles };
}),
);
const targetVehiclesMap = new Map(targetVehiclesEntries);
const models: DailyReportModel[] = targets.map(target => {
const vehicles = targetVehiclesMap.get(target.id) ?? [];
const active = vehicles.filter(vehicle => vehicle.todayMileage > 0).length;
return {
id: target.id,
name: compactTargetName(target.targetName),
count: target.vehicleCount,
today: target.todayTotal,
total: target.cumulativeTotal,
completion: target.avgCompletion,
active,
zero: Math.max(0, target.vehicleCount - active),
dailyNeed: target.dailyTarget,
};
});
return buildDailyReport(targets, targetVehicles, monitoring, [], reportDate);
}
const targetNameByPlate = new Map<string, string>();
for (const target of targets) {
const vehicles = targetVehiclesMap.get(target.id) ?? [];
for (const vehicle of vehicles) targetNameByPlate.set(vehicle.plateNumber, compactTargetName(target.targetName));
export async function fetchDailyReport(force = false): Promise<DailyReportData> {
if (!force && dailyReportCache && dailyReportCache.expiresAt > Date.now()) {
return dailyReportCache.data;
}
if (!force && dailyReportRequest) return dailyReportRequest;
const request = loadDailyReport();
dailyReportRequest = request;
try {
const data = await request;
dailyReportCache = { data, expiresAt: Date.now() + DAILY_REPORT_CACHE_MS };
return data;
} finally {
if (dailyReportRequest === request) dailyReportRequest = null;
}
}
const topVehicles: DailyReportVehicle[] = topMonitoring.vehicles.map(vehicle => ({
plate: vehicle.plate,
model: targetNameByPlate.get(vehicle.plate) || vehicle.project || '未归入考核',
status: normalizeStatus(vehicle.rentStatus),
today: vehicle.dailyKm,
customer: vehicle.customer || '未绑定客户',
}));
let dailyReportTrendRequest: Promise<TrendPoint[]> | null = null;
let dailyReportTrendCache: { data: TrendPoint[]; expiresAt: number } | null = null;
const zeroRisk = targetVehiclesEntries
.flatMap(([targetId, vehicles]) => {
const target = targets.find(item => item.id === targetId);
const model = target ? compactTargetName(target.targetName) : '未归入考核';
return vehicles
.filter(vehicle => vehicle.todayMileage <= 0 && ['自营', '租赁'].includes(normalizeStatus(vehicle.rentStatus)))
.map(vehicle => ({
plate: vehicle.plateNumber,
model,
status: normalizeStatus(vehicle.rentStatus),
customer: vehicle.customer || '未绑定客户',
completion: vehicle.completionRate,
}));
})
.sort((a, b) => (b.completion ?? 0) - (a.completion ?? 0))
.slice(0, 5);
return {
reportDate: reportDateFromUpdatedAt(monitoring.updatedAt),
updatedAt: monitoring.updatedAt,
models,
trend: trend.map(item => ({ date: item.date, value: item.mileage })),
topVehicles,
zeroRisk,
qualifiedCount: targets.reduce((sum, target) => sum + target.yearQualifiedCount, 0),
halfQualifiedCount: targets.reduce((sum, target) => sum + target.halfQualifiedCount, 0),
};
export async function fetchDailyReportTrend(force = false): Promise<TrendPoint[]> {
if (!force && dailyReportTrendCache && dailyReportTrendCache.expiresAt > Date.now()) {
return dailyReportTrendCache.data;
}
if (!force && dailyReportTrendRequest) return dailyReportTrendRequest;
const request = fetchTrend(undefined, 7);
dailyReportTrendRequest = request;
try {
const data = await request;
dailyReportTrendCache = { data, expiresAt: Date.now() + DAILY_REPORT_CACHE_MS };
return data;
} finally {
if (dailyReportTrendRequest === request) dailyReportTrendRequest = null;
}
}