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(); // 同一车型可能分批交付,因此一个日报日期可能同时落在多个考核年度。 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; }