feat: polish BI dashboards and bump version
All checks were successful
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

View File

@@ -1,5 +1,5 @@
import { Hono } from 'hono';
import { getCache, queryDateMileage, buildDateFilters } from './cache.js';
import { getCache, queryDateMileage, queryRangeMileage, buildDateFilters } from './cache.js';
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
import type { AuthUser } from '../../auth/types.js';
import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js';
@@ -64,12 +64,40 @@ function parseTargetNames(reqUrl: string): string[] {
return Array.from(new Set(names));
}
function parseYmd(value: string): Date | null {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) return null;
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
date.setHours(0, 0, 0, 0);
return Number.isFinite(date.getTime()) ? date : null;
}
function fmtYmd(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
function normalizeRange(startQuery: string, endQuery: string): { start: string; end: string } | null {
if (!startQuery && !endQuery) return null;
const start = parseYmd(startQuery || endQuery);
const end = parseYmd(endQuery || startQuery);
if (!start || !end) return null;
const a = start <= end ? start : end;
let b = start <= end ? end : start;
const span = Math.round((b.getTime() - a.getTime()) / 86400000) + 1;
if (span > 366) {
b = new Date(a);
b.setDate(a.getDate() + 365);
}
return { start: fmtYmd(a), end: fmtYmd(b) };
}
app.get('/', async (c) => {
const sortBy = c.req.query('sortBy') || 'today';
const sortOrder = c.req.query('sortOrder') || 'desc';
const limit = Number(c.req.query('limit')) || 50;
const page = Number(c.req.query('page')) || 1;
const date = c.req.query('date') || '';
const range = normalizeRange(c.req.query('startDate') || '', c.req.query('endDate') || '');
const filterParams = {
search: c.req.query('search') || '',
@@ -88,8 +116,21 @@ app.get('/', async (c) => {
let allVehicles: CachedVehicle[];
let filters: MonitoringFilters;
let rangeDailyTotals: { date: string; totalKm: number }[] | undefined;
let dateRange: { start: string; end: string } | undefined;
if (date) {
if (range) {
try {
const result = await queryRangeMileage(range.start, range.end);
allVehicles = result.vehicles;
rangeDailyTotals = result.dailyTotals;
dateRange = { start: result.start, end: result.end };
filters = buildDateFilters(allVehicles);
} catch (e: unknown) {
console.error('monitoring range query error:', e);
return c.json(EMPTY_RESPONSE, 500);
}
} else if (date) {
try {
allVehicles = await queryDateMileage(date);
filters = buildDateFilters(allVehicles);
@@ -118,6 +159,12 @@ app.get('/', async (c) => {
}
const filtered = applyFilters(allVehicles, filterParams);
if (rangeDailyTotals && filtered.length !== allVehicles.length) {
rangeDailyTotals = rangeDailyTotals.map(item => ({
...item,
totalKm: filtered.reduce((sum, vehicle) => sum + (vehicle.dailyMileage?.[item.date] || 0), 0),
}));
}
const stats = {
totalToday: filtered.reduce((sum, v) => sum + v.dailyKm, 0),
@@ -140,10 +187,12 @@ app.get('/', async (c) => {
vehicles: maskCustomerNames(paged),
stats,
filters,
rangeDailyTotals,
dateRange,
total,
page,
totalPages: Math.ceil(total / limit),
updatedAt: date || getCache()?.updatedAt || new Date().toISOString(),
updatedAt: dateRange?.end || date || getCache()?.updatedAt || new Date().toISOString(),
});
});