173 lines
6.4 KiB
TypeScript
173 lines
6.4 KiB
TypeScript
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<string, number>();
|
||
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 || [],
|
||
};
|
||
}
|