import type { DailyMileageReport, MileageReportGroup, MileageReportInsight, MileageReportQualityNote, MileageReportSource, MileageReportStatus, MileageReportTotals, MileageReportTrendPoint, } from '../../../shared/mileage/daily-report.js'; export function roundMileage(value: number): number { return Math.round((Number(value) || 0) * 10) / 10; } export function isOperatingStatus(status: string | null | undefined): boolean { return status === '租赁' || status === '自营'; } export function mileageBand( status: string | null | undefined, dailyMileage: number, ): 'LOW' | 'HIGH' | 'INVENTORY' { if (!isOperatingStatus(status)) return 'INVENTORY'; return dailyMileage <= 100 ? 'LOW' : 'HIGH'; } export function compactMileageTargetName(name: string): string { return name .replace(/^交投/, '') .replace(/^羚牛/, '') .replace(/辆/g, '台') .replace(/4\.5T冷链车/g, '冷链') .replace(/4\.5T冷链/g, '冷链') .replace(/4\.5T普货/g, '普货') .replace(/18T$/, '18T双飞翼'); } function percentChange(current: number, previous: number): number | null { if (previous <= 0) return null; return Math.round(((current - previous) / previous) * 1000) / 10; } function sum(groups: MileageReportGroup[], key: keyof MileageReportGroup): number { return groups.reduce((total, group) => total + Number(group[key] || 0), 0); } export function buildReportTotals(groups: MileageReportGroup[]): MileageReportTotals { const dailyMileage = roundMileage(sum(groups, 'dailyMileage')); const yesterdayMileage = roundMileage(sum(groups, 'yesterdayMileage')); const operatingCount = sum(groups, 'operatingCount'); const operatingMileage = roundMileage(sum(groups, 'operatingMileage')); const dailyRequiredMileage = roundMileage(sum(groups, 'dailyRequiredMileage')); return { vehicleCount: sum(groups, 'vehicleCount'), operatingCount, inventoryCount: sum(groups, 'inventoryCount'), dailyMileage, operatingMileage, inventoryMileage: roundMileage(sum(groups, 'inventoryMileage')), yesterdayMileage, dayOverDayDelta: roundMileage(dailyMileage - yesterdayMileage), dayOverDayRate: percentChange(dailyMileage, yesterdayMileage), averageOperatingMileage: operatingCount > 0 ? roundMileage(operatingMileage / operatingCount) : 0, lowMileageCount: sum(groups, 'lowMileageCount'), highMileageCount: sum(groups, 'highMileageCount'), qualifiedCount: sum(groups, 'qualifiedCount'), halfQualifiedCount: sum(groups, 'halfQualifiedCount'), dailyRequiredMileage, dailyTaskGap: roundMileage(dailyMileage - dailyRequiredMileage), }; } function describeDelta(value: number, rate: number | null): string { const direction = value >= 0 ? '增加' : '减少'; const rateText = rate == null ? '' : `,${Math.abs(rate).toFixed(1)}%`; return `较前一日${direction} ${Math.abs(value).toLocaleString('zh-CN', { maximumFractionDigits: 1 })} km${rateText}`; } export function buildReportInsights( groups: MileageReportGroup[], totals: MileageReportTotals, ): MileageReportInsight[] { if (groups.length === 0) return []; const largest = [...groups].sort((a, b) => b.dailyMileage - a.dailyMileage)[0]; const weakest = [...groups].sort((a, b) => a.dayOverDayDelta - b.dayOverDayDelta)[0]; const lowRate = totals.operatingCount > 0 ? totals.lowMileageCount / totals.operatingCount : 0; const insights: MileageReportInsight[] = [ { tone: totals.dayOverDayDelta >= 0 ? 'positive' : 'warning', title: totals.dayOverDayDelta >= 0 ? '总里程回升' : '总里程回落', detail: describeDelta(totals.dayOverDayDelta, totals.dayOverDayRate), }, { tone: 'neutral', title: `${largest.displayName}贡献最高`, detail: `当日 ${largest.dailyMileage.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} km,占全量 ${totals.dailyMileage > 0 ? ((largest.dailyMileage / totals.dailyMileage) * 100).toFixed(1) : '0.0'}%`, }, ]; if (weakest.dayOverDayDelta < 0) { insights.push({ tone: 'warning', title: `${weakest.displayName}降幅最大`, detail: describeDelta(weakest.dayOverDayDelta, weakest.dayOverDayRate), }); } else { insights.push({ tone: 'positive', title: '各车型未出现环比下降', detail: `增幅最低为 ${weakest.displayName},仍增加 ${weakest.dayOverDayDelta.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} km`, }); } if (totals.inventoryMileage > 0) { insights.push({ tone: 'critical', title: '库存车辆产生里程', detail: `${totals.inventoryCount} 台库存车辆合计产生 ${totals.inventoryMileage.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} km,建议核对状态`, }); } else { insights.push({ tone: lowRate >= 0.5 ? 'warning' : 'neutral', title: `${(lowRate * 100).toFixed(1)}% 运营车辆不超过 100km`, detail: `${totals.lowMileageCount} 台低里程,${totals.highMileageCount} 台超过 100km`, }); } return insights.slice(0, 4); } export function buildOverallTrend(groups: MileageReportGroup[]): MileageReportTrendPoint[] { const totals = new Map(); for (const group of groups) { for (const point of group.trend) { totals.set(point.date, (totals.get(point.date) || 0) + point.totalMileage); } } return Array.from(totals.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([date, totalMileage]) => ({ date, totalMileage: roundMileage(totalMileage) })); } export function buildDailyMileageReport(input: { reportDate: string; status: MileageReportStatus; source: MileageReportSource; generatedAt: string; sourceUpdatedAt: string | null; groups: MileageReportGroup[]; qualityNotes?: MileageReportQualityNote[]; }): DailyMileageReport { const groups = input.groups.map(group => ({ ...group, dayOverDayDelta: roundMileage(group.dailyMileage - group.yesterdayMileage), dayOverDayRate: percentChange(group.dailyMileage, group.yesterdayMileage), })); const totals = buildReportTotals(groups); return { version: 1, reportDate: input.reportDate, title: `广州现代 ${totals.vehicleCount} 台车辆运营日报`, status: input.status, source: input.source, generatedAt: input.generatedAt, sourceUpdatedAt: input.sourceUpdatedAt, totals, trend: buildOverallTrend(groups), groups, insights: buildReportInsights(groups, totals), qualityNotes: input.qualityNotes || [], }; }