feat(mileage): add daily operations report
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ export default function MileageModule() {
|
||||
eyebrow="MILEAGE BI"
|
||||
meta="实时监控 · 统计报表 · 每日汇报"
|
||||
compactInfo
|
||||
hideHeader
|
||||
>
|
||||
<div className="sticky top-0 z-30 -mx-3 bg-[var(--app-bg)] px-3 pb-2 pt-1 shadow-[0_8px_12px_-12px_rgba(15,23,42,0.08)] md:-mx-6 md:top-12 md:px-6">
|
||||
<SegmentedNav tabs={MILEAGE_TABS} active={activeSubTab} onChange={setActiveSubTab} />
|
||||
|
||||
+18
-128
@@ -8,6 +8,20 @@ import type {
|
||||
TrendPoint,
|
||||
} from './types';
|
||||
import { fetchJson } from '../../auth/api-client';
|
||||
import type {
|
||||
DailyMileageReport,
|
||||
MileageReportHistoryResponse,
|
||||
} from '../../shared/mileage/daily-report';
|
||||
|
||||
export type {
|
||||
DailyMileageReport,
|
||||
MileageReportBreakdown,
|
||||
MileageReportGroup,
|
||||
MileageReportHistoryItem,
|
||||
MileageReportHistoryResponse,
|
||||
MileageReportInsight,
|
||||
MileageReportVehicle,
|
||||
} from '../../shared/mileage/daily-report';
|
||||
|
||||
const BASE = '/api/mileage';
|
||||
|
||||
@@ -123,134 +137,10 @@ 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 async function fetchDailyMileageReport(date: string): Promise<DailyMileageReport> {
|
||||
return fetchJson<DailyMileageReport>(`${BASE}/reports?date=${encodeURIComponent(date)}`);
|
||||
}
|
||||
|
||||
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);
|
||||
if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
||||
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, '冷链车');
|
||||
}
|
||||
|
||||
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([
|
||||
fetchTargets(),
|
||||
fetchTrend(undefined, 7),
|
||||
fetchMonitoring({ limit: 1 }),
|
||||
fetchMonitoring({ sortBy: 'today', sortOrder: 'desc', limit: 5 }),
|
||||
]);
|
||||
|
||||
const targetVehiclesEntries = await Promise.all(
|
||||
targets.map(async target => {
|
||||
const vehicles = await fetchTargetVehicles(target.id);
|
||||
return [target.id, vehicles] as const;
|
||||
}),
|
||||
);
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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 || '未绑定客户',
|
||||
}));
|
||||
|
||||
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 fetchDailyMileageReportHistory(limit = 90): Promise<MileageReportHistoryResponse> {
|
||||
return fetchJson<MileageReportHistoryResponse>(`${BASE}/reports/history?limit=${limit}`);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface MonitoringVehicle {
|
||||
entity: string | null;
|
||||
project: string | null;
|
||||
region: string | null;
|
||||
inventoryLocation: string | null;
|
||||
brand: string | null;
|
||||
targetNames: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user