180 lines
6.7 KiB
TypeScript
180 lines
6.7 KiB
TypeScript
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<string, number>();
|
|
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<string, VehicleInfoRow>,
|
|
yesterdayMap: Map<string, number>,
|
|
): CachedVehicle[] {
|
|
const mileageMap = new Map<string, MileageRow>();
|
|
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<void> {
|
|
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<string, number>();
|
|
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<string, Set<string>>();
|
|
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<CachedVehicle[]> {
|
|
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<string, number>();
|
|
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 || []);
|
|
}
|