import pool from '../../db.js'; import mileagePool from '../../mileage-db.js'; import { fetchVehicleInfoMap } from './vehicle-info.js'; import type { CachedVehicle, MonitoringCache, MonitoringFilters, PlatePrefix, VehicleInfoRow } from './types.js'; let monitoringCache: MonitoringCache | null = null; export function getCache(): MonitoringCache | null { return monitoringCache; } const DEPT_ORDER = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; function sortDepartments(departments: string[]): string[] { return departments.sort((a, b) => { const ai = DEPT_ORDER.findIndex(d => a.includes(d)); const bi = DEPT_ORDER.findIndex(d => b.includes(d)); return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi); }); } function buildFilters(vehicles: CachedVehicle[], targetNames: string[]): MonitoringFilters { const departments = sortDepartments( Array.from(new Set(vehicles.map(v => v.department).filter((d): d is string => d !== null))) ); const customers = Array.from(new Set(vehicles.map(v => v.customer).filter((c): c is string => c !== null))); const plates = vehicles.map(v => v.plate); const projects = Array.from(new Set(vehicles.map(v => v.project).filter((p): p is string => p !== null))); const entities = Array.from(new Set(vehicles.map(v => v.entity).filter((e): e is string => e !== null))); const rentStatuses = Array.from(new Set(vehicles.map(v => v.rentStatus).filter((r): r is string => r !== null))); const prefixCount = new Map(); for (const v of vehicles) { const p = v.plate.charAt(0); prefixCount.set(p, (prefixCount.get(p) || 0) + 1); } const platePrefixes: PlatePrefix[] = Array.from(prefixCount.entries()) .map(([prefix, count]) => ({ prefix, count })) .sort((a, b) => b.count - a.count); return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames }; } interface MileageRow { plate: string; vin: string; daily_km: string; total_km: string | null; source: string; } function mergeVehicles( mileageRows: MileageRow[], infoMap: Map, yesterdayMap: Map, ): CachedVehicle[] { const mileageMap = new Map(); for (const row of mileageRows) { const existing = mileageMap.get(row.plate); if (!existing || Number(row.daily_km) > Number(existing.daily_km)) { mileageMap.set(row.plate, row); } } return Array.from(mileageMap.values()).map(m => { const info = infoMap.get(m.plate); const dailyKm = Number(m.daily_km) || 0; const source = m.source || 'NONE'; return { plate: m.plate, vin: m.vin, dailyKm, totalKm: m.total_km !== null ? Number(m.total_km) : null, source, isOnline: source !== 'NONE' && dailyKm > 0, isDataSynced: source !== 'NONE', customer: info?.customer || null, department: info?.department || null, manager: info?.manager || null, rentStatus: info?.rent_status || null, entity: info?.entity || null, project: info?.project || null, yesterdayKm: yesterdayMap.get(m.plate) || 0, }; }); } export async function refreshMonitoringCache(): Promise { try { console.log('[mileage] refreshing monitoring cache...'); const start = Date.now(); const [mileageRows, yesterdayMap, infoMap, targetRows] = await Promise.all([ (async () => { const [dateRows] = await mileagePool.execute( 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' ) as [{ latest: string | null }[], unknown]; const latestDate = dateRows[0]?.latest; if (!latestDate) return []; const [rows] = await mileagePool.execute( 'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?', [latestDate] ) as [MileageRow[], unknown]; return rows; })(), (async () => { const [rows] = await mileagePool.execute( `SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB((SELECT MAX(stat_date) FROM v_vehicle_daily_stats), INTERVAL 1 DAY)` ) as [{ plate: string; daily_km: string }[], unknown]; const map = new Map(); for (const r of rows) { const km = Number(r.daily_km) || 0; const existing = map.get(r.plate) || 0; if (km > existing) map.set(r.plate, km); } return map; })(), fetchVehicleInfoMap(), pool.execute( `SELECT t.id, t.target_name, v.plate_number FROM tab_mileage_assessment_target t JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 WHERE t.is_deleted = 0` ).then(([rows]) => rows as { id: number; target_name: string; plate_number: string }[]), ]); const targetPlatesMap = new Map>(); for (const r of targetRows) { const set = targetPlatesMap.get(r.target_name) || new Set(); set.add(r.plate_number); targetPlatesMap.set(r.target_name, set); } const targetNames = Array.from(targetPlatesMap.keys()); const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap); const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0); const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0); monitoringCache = { vehicles, stats: { totalToday, totalAll, vehicleCount: vehicles.length }, filters: buildFilters(vehicles, targetNames), targetPlatesMap, updatedAt: new Date().toISOString(), }; console.log(`[mileage] cache refreshed: ${vehicles.length} vehicles in ${Date.now() - start}ms`); } catch (e: unknown) { console.error('[mileage] cache refresh error:', e); } } export async function queryDateMileage(dateStr: string): Promise { const [mileageRows, yesterdayRows, infoMap] = await Promise.all([ mileagePool.execute( 'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?', [dateStr] ).then(([r]) => r as MileageRow[]), mileagePool.execute( 'SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)', [dateStr] ).then(([r]) => r as { plate: string; daily_km: string }[]), fetchVehicleInfoMap(), ]); const yesterdayMap = new Map(); for (const r of yesterdayRows) { const km = Number(r.daily_km) || 0; const existing = yesterdayMap.get(r.plate) || 0; if (km > existing) yesterdayMap.set(r.plate, km); } return mergeVehicles(mileageRows, infoMap, yesterdayMap); } export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters { return buildFilters(vehicles, monitoringCache?.filters.targetNames || []); }