Files
ln-bi/src/modules/mileage/daily-report/utils.ts
T

171 lines
6.4 KiB
TypeScript

import type {
MileageReportGroup,
MileageReportVehicle,
} from '../api';
export type VehicleFilter = 'ALL' | 'LOW' | 'HIGH' | 'INVENTORY';
export type VehicleSortKey = 'dailyMileage' | 'sevenDayMileage' | 'completionRate';
export type SortDirection = 'asc' | 'desc';
const REPORT_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const HIDDEN_QUALITY_NOTE = '日行驶总里程延续人工汇报口径,包含库存车辆产生的里程。';
export function shanghaiYmd(offsetDays = 0, now = new Date()): string {
// 日报按上海自然日归档,不能依赖浏览器所在时区计算“昨天”。
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(now);
const date = new Date(`${parts}T00:00:00Z`);
date.setUTCDate(date.getUTCDate() + offsetDays);
return date.toISOString().slice(0, 10);
}
export function dateShift(date: string, days: number): string {
const value = new Date(`${date}T00:00:00Z`);
value.setUTCDate(value.getUTCDate() + days);
return value.toISOString().slice(0, 10);
}
export function monthShift(month: string, months: number): string {
const value = new Date(`${month}-01T00:00:00Z`);
value.setUTCMonth(value.getUTCMonth() + months);
return value.toISOString().slice(0, 7);
}
export function reportDateLabel(value: string): string {
const date = new Date(`${value}T00:00:00Z`);
return new Intl.DateTimeFormat('zh-CN', {
timeZone: 'UTC',
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short',
}).format(date).replace(/日(?=周)/, '日 ');
}
export function initialReportDate(search: string, fallbackDate = shanghaiYmd(-1)): string {
const value = new URLSearchParams(search).get('mileageReportDate');
return REPORT_DATE_PATTERN.test(value || '') ? value! : fallbackDate;
}
export function fmtKm(value: number): string {
if (Math.abs(value) >= 10000) return `${(value / 10000).toFixed(2)}万`;
return value.toLocaleString('zh-CN', { maximumFractionDigits: 1 });
}
export function fmtDate(value: string): string {
const [, month, day] = value.split('-');
return `${Number(month)}${Number(day)}日`;
}
export function fmtDateTime(value: string | null): string {
if (!value) return '数据时间未返回';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}
export function deltaLabel(value: number, rate: number | null): string {
const sign = value >= 0 ? '+' : '';
return `${sign}${fmtKm(value)} km${rate == null ? '' : ` · ${sign}${rate.toFixed(1)}%`}`;
}
export function currentAssessmentYearLabel(periods: string[], reportDate: string): string | null {
const report = new Date(`${reportDate}T00:00:00Z`);
if (Number.isNaN(report.getTime())) return null;
const years = new Set<number>();
// 同一车型可能分批交付,因此一个日报日期可能同时落在多个考核年度。
for (const period of periods) {
const dates = Array.from(period.matchAll(/(\d{4})[/-](\d{1,2})[/-](\d{1,2})/g));
const start = dates[0];
const end = dates[1];
if (!start || !end) continue;
const startDate = new Date(Date.UTC(Number(start[1]), Number(start[2]) - 1, Number(start[3])));
const endDate = new Date(Date.UTC(Number(end[1]), Number(end[2]) - 1, Number(end[3])));
if (report < startDate || report > endDate) continue;
let yearNumber = report.getUTCFullYear() - startDate.getUTCFullYear() + 1;
if (
report.getUTCMonth() < startDate.getUTCMonth()
|| (report.getUTCMonth() === startDate.getUTCMonth() && report.getUTCDate() < startDate.getUTCDate())
) yearNumber -= 1;
if (yearNumber > 0) years.add(yearNumber);
}
if (years.size === 0) return null;
const labels = Array.from(years).sort((a, b) => a - b).map(year => String(year));
return `考核第${labels.join('/')}年`;
}
export function dailyMileageStatus(group: Pick<
MileageReportGroup,
'dailyRequiredMileage' | 'remainingMileage' | 'dailyMileage'
>): {
label: string;
detail: string;
tone: 'positive' | 'warning' | 'neutral';
} {
if (group.dailyRequiredMileage <= 0) {
if (group.remainingMileage <= 0) {
return { label: '本年度已完成', detail: '当前年度剩余任务为 0,无需设置每日应完成里程。', tone: 'positive' };
}
return { label: '暂无法计算', detail: '缺少有效的当前考核年度截止日,暂不设每日应完成里程。', tone: 'warning' };
}
const gap = group.dailyMileage - group.dailyRequiredMileage;
const rate = (group.dailyMileage / group.dailyRequiredMileage) * 100;
if (gap >= 0) {
return { label: '今日已达标', detail: `超出应完成 ${fmtKm(gap)} km,完成 ${rate.toFixed(1)}%。`, tone: 'positive' };
}
return { label: '今日未达标', detail: `距应完成差 ${fmtKm(Math.abs(gap))} km,完成 ${rate.toFixed(1)}%。`, tone: 'warning' };
}
export function sevenDayMileageOf(vehicle: MileageReportVehicle): number {
return (vehicle.trend || []).reduce((total, point) => total + point.totalMileage, 0);
}
function vehicleSortValue(vehicle: MileageReportVehicle, sortKey: VehicleSortKey): number {
return sortKey === 'sevenDayMileage' ? sevenDayMileageOf(vehicle) : vehicle[sortKey];
}
export function filterAndSortVehicles(
vehicles: MileageReportVehicle[],
filter: VehicleFilter,
search: string,
sortKey: VehicleSortKey,
sortDirection: SortDirection,
): MileageReportVehicle[] {
const query = search.trim().toLowerCase();
return vehicles
.filter(vehicle => {
if (filter !== 'ALL' && vehicle.mileageBand !== filter) return false;
if (!query) return true;
return vehicle.plate.toLowerCase().includes(query)
|| (vehicle.customer || '').toLowerCase().includes(query)
|| (vehicle.department || '').toLowerCase().includes(query);
})
.sort((left, right) => {
const difference = vehicleSortValue(left, sortKey) - vehicleSortValue(right, sortKey);
if (difference !== 0) return sortDirection === 'asc' ? difference : -difference;
return left.plate.localeCompare(right.plate, 'zh-CN');
});
}
export function roundMileageForDisplay(value: number): number {
return Math.round(value * 10) / 10;
}
export function isVisibleQualityNote(message: string): boolean {
return message !== HIDDEN_QUALITY_NOTE;
}