import type { MileageSourceCategory, MileageSourceGroup, MileageSourceProtocol, MonitoringData, TargetSummary, TargetVehicle, TrendPoint, } from './types'; import { fetchJson } from '../../auth/api-client'; const BASE = '/api/mileage'; export async function fetchMonitoring(params?: { sortBy?: string; sortOrder?: string; limit?: number; page?: number; search?: string; dept?: string; customer?: string; project?: string; entity?: string; rentStatus?: string; platePrefix?: string; targetName?: string; targetNames?: string[]; region?: string; plate?: string; mileageMin?: string; mileageMax?: string; date?: string; startDate?: string; endDate?: string; brands?: string[]; sourcePriority?: MileageSourceGroup[]; }): Promise { const query = new URLSearchParams(); if (params?.sortBy) query.set('sortBy', params.sortBy); if (params?.sortOrder) query.set('sortOrder', params.sortOrder); if (params?.limit) query.set('limit', String(params.limit)); if (params?.page) query.set('page', String(params.page)); if (params?.search) query.set('search', params.search); if (params?.dept) query.set('dept', params.dept); if (params?.customer) query.set('customer', params.customer); if (params?.project) query.set('project', params.project); if (params?.entity) query.set('entity', params.entity); if (params?.rentStatus) query.set('rentStatus', params.rentStatus); if (params?.platePrefix) query.set('platePrefix', params.platePrefix); if (params?.targetName) query.set('targetName', params.targetName); if (params?.targetNames) { params.targetNames.forEach(name => { if (name) query.append('targetName', name); }); } if (params?.region) query.set('region', params.region); if (params?.brands) { params.brands.forEach(brand => { if (brand) query.append('brand', brand); }); } if (params?.plate) query.set('plate', params.plate); if (params?.mileageMin) query.set('mileageMin', params.mileageMin); if (params?.mileageMax) query.set('mileageMax', params.mileageMax); if (params?.date) query.set('date', params.date); if (params?.startDate) query.set('startDate', params.startDate); if (params?.endDate) query.set('endDate', params.endDate); if (params?.sourcePriority?.length) query.set('sourcePriority', params.sourcePriority.join(',')); const qs = query.toString(); return fetchJson(`${BASE}/monitoring${qs ? `?${qs}` : ''}`); } export async function fetchTargets(): Promise { return fetchJson(`${BASE}/targets`); } export async function fetchTargetVehicles(targetId: number, date?: string): Promise { const params = new URLSearchParams(); if (date) params.set('date', date); const qs = params.toString(); return fetchJson(`${BASE}/target/${targetId}/vehicles${qs ? `?${qs}` : ''}`); } export async function fetchTrend(targetId?: number, days = 7): Promise { const params = new URLSearchParams(); if (targetId) params.set('targetId', String(targetId)); params.set('days', String(days)); return fetchJson(`${BASE}/trend?${params.toString()}`); } export interface VehicleRecentDay { date: string; dailyKm: number; isDataSynced: boolean; sourceProtocol: MileageSourceProtocol | null; sourceCategory: Exclude | null; } export interface VehicleRecentResponse { plate: string; start?: string; end?: string; days: VehicleRecentDay[]; sourcePriority?: MileageSourceGroup[]; } export async function fetchVehicleRecent( plate: string, range: { days?: number; start?: string; end?: string; sourcePriority?: MileageSourceGroup[]; } = { days: 15 }, ): Promise { const params = new URLSearchParams(); if (range.start) params.set('start', range.start); if (range.end) params.set('end', range.end); if (range.days != null) params.set('days', String(range.days)); if (range.sourcePriority?.length) params.set('sourcePriority', range.sourcePriority.join(',')); return fetchJson( `${BASE}/vehicle/${encodeURIComponent(plate)}/recent?${params.toString()}` ); } 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); 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 { 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(); 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), }; }