Files
ln-bi/src/server/routes/mileage/daily-report-model.ts
T

173 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 || [],
};
}