feat: polish BI dashboards and bump version
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
lingniu
2026-06-27 21:59:33 +08:00
parent 5377d2c225
commit b0caa5afcb
33 changed files with 2363 additions and 483 deletions
+139 -19
View File
@@ -65,6 +65,21 @@ interface MileageRow {
source: string;
}
interface DailyMileageRow {
plate: string;
vin: string | null;
date: string;
daily_km: string | number | null;
source: string | null;
}
export interface RangeMileageResult {
vehicles: CachedVehicle[];
dailyTotals: { date: string; totalKm: number }[];
start: string;
end: string;
}
interface TargetRow {
id: number;
target_name: string;
@@ -160,31 +175,32 @@ function mergeVehicles(
}
}
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';
const gpsTotal = m.total_km !== null ? Number(m.total_km) : null;
const latestPgTotal = latestPgTotalMap.get(m.plate);
const bizTotal = bizTotalMap.get(m.plate);
return Array.from(infoMap.values()).map(info => {
const m = mileageMap.get(info.plate);
const plate = info.plate;
const dailyKm = Number(m?.daily_km) || 0;
const source = m?.source || 'NONE';
const gpsTotal = m?.total_km != null ? Number(m.total_km) : null;
const latestPgTotal = latestPgTotalMap.get(plate);
const bizTotal = bizTotalMap.get(plate);
return {
plate: m.plate,
vin: m.vin,
plate,
vin: m?.vin || info.vin || '',
dailyKm,
totalKm: gpsTotal !== null ? gpsTotal : (latestPgTotal ?? bizTotal ?? null),
source,
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: regionMap[m.plate] || null,
targetNames: targetNamesByPlate.get(m.plate) || [],
yesterdayKm: yesterdayMap.get(m.plate) || 0,
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: regionMap[plate] || null,
targetNames: targetNamesByPlate.get(plate) || [],
yesterdayKm: yesterdayMap.get(plate) || 0,
};
});
}
@@ -281,6 +297,110 @@ export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]
);
}
function datesBetween(start: string, end: string): string[] {
const result: string[] = [];
const [sy, sm, sd] = start.split('-').map(Number);
const [ey, em, ed] = end.split('-').map(Number);
const cursor = new Date(sy, sm - 1, sd);
const last = new Date(ey, em - 1, ed);
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')}`);
cursor.setDate(cursor.getDate() + 1);
}
return result;
}
export async function queryRangeMileage(startDate: string, endDate: string): Promise<RangeMileageResult> {
const days = datesBetween(startDate, endDate);
const [dailyRows, yesterdayRows, infoMap, targetRows, bizTotalMap, latestPgTotalMap] = await Promise.all([
mileagePool.execute(
`SELECT plate,
DATE_FORMAT(stat_date, '%Y-%m-%d') AS date,
vin,
daily_km,
source
FROM v_vehicle_daily_stats
WHERE stat_date >= ? AND stat_date <= ?
ORDER BY stat_date, plate`,
[startDate, endDate]
).then(([r]) => r as DailyMileageRow[]),
mileagePool.execute(
'SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)',
[startDate]
).then(([r]) => r as { plate: string; daily_km: string }[]),
fetchVehicleInfoMap(),
fetchTargetRows(),
fetchBizTotalMileageMap(),
fetchLatestPgTotalMileageMap(endDate),
]);
const perVehicleDaily = new Map<string, Record<string, number>>();
const perVehicleSum = new Map<string, { plate: string; vin: string; daily_km: string; total_km: null; source: string }>();
const dailyTotals = new Map<string, number>();
const bestDailyRows = new Map<string, DailyMileageRow>();
for (const day of days) dailyTotals.set(day, 0);
for (const row of dailyRows) {
const key = `${row.plate}\u0000${row.date}`;
const km = Math.max(0, Number(row.daily_km) || 0);
const existing = bestDailyRows.get(key);
if (!existing || km > Math.max(0, Number(existing.daily_km) || 0)) {
bestDailyRows.set(key, row);
}
}
for (const row of bestDailyRows.values()) {
const km = Math.max(0, Number(row.daily_km) || 0);
const date = row.date;
const plate = row.plate;
dailyTotals.set(date, (dailyTotals.get(date) || 0) + km);
const daily = perVehicleDaily.get(plate) || {};
daily[date] = km;
perVehicleDaily.set(plate, daily);
const existing = perVehicleSum.get(plate);
perVehicleSum.set(plate, {
plate,
vin: existing?.vin || row.vin || '',
daily_km: String((Number(existing?.daily_km) || 0) + km),
total_km: null,
source: existing?.source !== 'NONE' && existing?.source ? existing.source : (row.source || 'NONE'),
});
}
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);
}
const vehicles = mergeVehicles(
Array.from(perVehicleSum.values()),
infoMap,
yesterdayMap,
bizTotalMap,
latestPgTotalMap,
buildPlateTargetNamesMap(targetRows),
).map(vehicle => {
const dailyMileage = perVehicleDaily.get(vehicle.plate) || {};
const completedDailyMileage: Record<string, number> = {};
for (const day of days) completedDailyMileage[day] = dailyMileage[day] || 0;
return { ...vehicle, dailyMileage: completedDailyMileage };
});
return {
vehicles,
dailyTotals: days.map(date => ({ date, totalKm: dailyTotals.get(date) || 0 })),
start: startDate,
end: endDate,
};
}
export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters {
return buildFilters(vehicles, monitoringCache?.filters.targetNames || []);
}