225 lines
7.7 KiB
TypeScript
225 lines
7.7 KiB
TypeScript
import { sourceCategoryFromProtocol, type OneOsProtocol } from './source-policy.js';
|
|
import type { OneOsDailyMileage } from './oneos-model.js';
|
|
import type {
|
|
CachedVehicle,
|
|
MonitoringFilters,
|
|
PlatePrefix,
|
|
VehicleInfoRow,
|
|
} from './types.js';
|
|
|
|
const REGION_ORDER = ['华东区域', '华南区域', '西南区域', '西北区域', '华北区域', '华中区域', '东北区域'];
|
|
const DEPARTMENT_ORDER = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
|
|
|
|
export interface MileageRow {
|
|
plate: string;
|
|
vin: string;
|
|
daily_km: string;
|
|
total_km: string | null;
|
|
mileage_anomaly?: string | null;
|
|
source: string;
|
|
source_protocol: OneOsProtocol | null;
|
|
data_time: string | null;
|
|
calculated_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface DailyMileageRow {
|
|
plate: string;
|
|
vin: string | null;
|
|
date: string;
|
|
daily_km: string | number | null;
|
|
mileage_anomaly?: string | null;
|
|
source: string | null;
|
|
source_protocol: OneOsProtocol | null;
|
|
data_time: string | null;
|
|
calculated_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface TargetRow {
|
|
id: number;
|
|
target_name: string;
|
|
plate_number: string;
|
|
}
|
|
|
|
function sortDepartments(departments: string[]): string[] {
|
|
return departments.sort((a, b) => {
|
|
const ai = DEPARTMENT_ORDER.findIndex(value => a.includes(value));
|
|
const bi = DEPARTMENT_ORDER.findIndex(value => b.includes(value));
|
|
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
});
|
|
}
|
|
|
|
export function buildMonitoringFilters(
|
|
vehicles: CachedVehicle[],
|
|
targetNames: string[],
|
|
): MonitoringFilters {
|
|
const departments = sortDepartments(
|
|
Array.from(new Set(vehicles.map(vehicle => vehicle.department)
|
|
.filter((value): value is string => value !== null))),
|
|
);
|
|
const customers = Array.from(new Set(vehicles.map(vehicle => vehicle.customer)
|
|
.filter((value): value is string => value !== null)));
|
|
const plates = vehicles.map(vehicle => vehicle.plate);
|
|
const projects = Array.from(new Set(vehicles.map(vehicle => vehicle.project)
|
|
.filter((value): value is string => value !== null)));
|
|
const entities = Array.from(new Set(vehicles.map(vehicle => vehicle.entity)
|
|
.filter((value): value is string => value !== null)));
|
|
const rentStatuses = Array.from(new Set(vehicles.map(vehicle => vehicle.rentStatus)
|
|
.filter((value): value is string => value !== null)));
|
|
const brands = Array.from(new Set(vehicles.map(vehicle => vehicle.brand)
|
|
.filter((value): value is string => value !== null))).sort();
|
|
|
|
const prefixCount = new Map<string, number>();
|
|
for (const vehicle of vehicles) {
|
|
const prefix = vehicle.plate.charAt(0);
|
|
prefixCount.set(prefix, (prefixCount.get(prefix) || 0) + 1);
|
|
}
|
|
const platePrefixes: PlatePrefix[] = Array.from(prefixCount.entries())
|
|
.map(([prefix, count]) => ({ prefix, count }))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
const regionSet = new Set(vehicles.map(vehicle => vehicle.region)
|
|
.filter((value): value is string => value !== null));
|
|
const regions = Array.from(regionSet).sort((a, b) => {
|
|
const ai = REGION_ORDER.indexOf(a);
|
|
const bi = REGION_ORDER.indexOf(b);
|
|
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
});
|
|
|
|
return {
|
|
departments,
|
|
customers,
|
|
plates,
|
|
projects,
|
|
entities,
|
|
rentStatuses,
|
|
platePrefixes,
|
|
targetNames,
|
|
regions,
|
|
brands,
|
|
};
|
|
}
|
|
|
|
export function toMileageRows(rows: OneOsDailyMileage[]): MileageRow[] {
|
|
return rows.map(row => ({
|
|
plate: row.plateNumber,
|
|
vin: row.vin,
|
|
daily_km: String(row.dailyMileageKm ?? 0),
|
|
total_km: row.totalMileageKm == null ? null : String(row.totalMileageKm),
|
|
source: row.status !== 'NO_DATA' ? 'ONEOS_API' : 'NONE',
|
|
mileage_anomaly: row.status === 'DATA_ANOMALY' ? row.dataQuality || 'DATA_ANOMALY' : null,
|
|
source_protocol: row.sourceProtocol,
|
|
data_time: row.dataTime,
|
|
calculated_at: row.calculatedAt,
|
|
updated_at: row.updatedAt,
|
|
}));
|
|
}
|
|
|
|
export function previousMileageDate(date: string): string {
|
|
const [year, month, day] = date.split('-').map(Number);
|
|
const value = new Date(Date.UTC(year, month - 1, day));
|
|
value.setUTCDate(value.getUTCDate() - 1);
|
|
return value.toISOString().slice(0, 10);
|
|
}
|
|
|
|
export function dailyMileageMap(rows: OneOsDailyMileage[]): Map<string, number> {
|
|
const map = new Map<string, number>();
|
|
for (const row of rows) {
|
|
if (row.status !== 'NORMAL') continue;
|
|
const km = Math.max(0, Number(row.dailyMileageKm) || 0);
|
|
const existing = map.get(row.plateNumber) || 0;
|
|
if (km > existing) map.set(row.plateNumber, km);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
export function buildTargetPlatesMap(targetRows: TargetRow[]): Map<string, Set<string>> {
|
|
const result = new Map<string, Set<string>>();
|
|
for (const row of targetRows) {
|
|
const plates = result.get(row.target_name) || new Set<string>();
|
|
plates.add(row.plate_number);
|
|
result.set(row.target_name, plates);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function buildPlateTargetNamesMap(targetRows: TargetRow[]): Map<string, string[]> {
|
|
const result = new Map<string, string[]>();
|
|
for (const row of targetRows) {
|
|
const names = result.get(row.plate_number) || [];
|
|
names.push(row.target_name);
|
|
result.set(row.plate_number, names);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function mergeMonitoringVehicles(
|
|
mileageRows: MileageRow[],
|
|
infoMap: Map<string, VehicleInfoRow>,
|
|
yesterdayMap: Map<string, number>,
|
|
targetNamesByPlate: Map<string, string[]>,
|
|
regionByPlate: Record<string, string>,
|
|
): 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(infoMap.values()).map(info => {
|
|
const mileage = mileageMap.get(info.plate);
|
|
const dailyKm = Number(mileage?.daily_km) || 0;
|
|
const source = mileage?.source || 'NONE';
|
|
return {
|
|
plate: info.plate,
|
|
vin: mileage?.vin || info.vin || '',
|
|
dailyKm,
|
|
mileageAnomaly: mileage?.mileage_anomaly || null,
|
|
// Cumulative mileage is never backfilled from a different source here.
|
|
totalKm: mileage?.total_km != null ? Number(mileage.total_km) : null,
|
|
source,
|
|
sourceProtocol: mileage?.source_protocol || null,
|
|
sourceCategory: sourceCategoryFromProtocol(mileage?.source_protocol || null),
|
|
dataTime: mileage?.data_time || null,
|
|
calculatedAt: mileage?.calculated_at || null,
|
|
updatedAt: mileage?.updated_at || null,
|
|
isOnline: source !== 'NONE' && dailyKm > 0,
|
|
isDataSynced: source !== 'NONE',
|
|
customer: info.customer || null,
|
|
department: info.department || null,
|
|
manager: info.manager || null,
|
|
managerId: info.manager_id || null,
|
|
rentStatus: info.rent_status || null,
|
|
entity: info.entity || null,
|
|
project: info.project || null,
|
|
region: regionByPlate[info.plate] || null,
|
|
inventoryLocation: info.inventory_location || null,
|
|
brand: info.brand_label || null,
|
|
targetNames: targetNamesByPlate.get(info.plate) || [],
|
|
yesterdayKm: yesterdayMap.get(info.plate) || 0,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function mileageDatesBetween(start: string, end: string): string[] {
|
|
const result: string[] = [];
|
|
const [startYear, startMonth, startDay] = start.split('-').map(Number);
|
|
const [endYear, endMonth, endDay] = end.split('-').map(Number);
|
|
const cursor = new Date(startYear, startMonth - 1, startDay);
|
|
const last = new Date(endYear, endMonth - 1, endDay);
|
|
cursor.setHours(0, 0, 0, 0);
|
|
last.setHours(0, 0, 0, 0);
|
|
while (cursor <= last) {
|
|
result.push([
|
|
cursor.getFullYear(),
|
|
String(cursor.getMonth() + 1).padStart(2, '0'),
|
|
String(cursor.getDate()).padStart(2, '0'),
|
|
].join('-'));
|
|
cursor.setDate(cursor.getDate() + 1);
|
|
}
|
|
return result;
|
|
}
|