feat(mileage): add brand multi-select filter and customer fuzzy search
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Add brand multi-select filter (BatchMultiSelect) with 17 brand options - Replace customer select with SearchableSelect for fuzzy search - Server-side: add brand to vehicle-info SQL, cache, filters, and monitoring - Customer filter now uses case-insensitive includes match on server side - Add brand column to fullscreen table header and body - Mobile-adaptive: compact inputs, constrained widths, scrollable dropdowns - Bump version to 1.1.8
This commit is contained in:
@@ -37,6 +37,7 @@ function buildFilters(vehicles: CachedVehicle[], targetNames: string[]): Monitor
|
||||
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 brands = Array.from(new Set(vehicles.map(v => v.brand).filter((b): b is string => b !== null))).sort();
|
||||
|
||||
const prefixCount = new Map<string, number>();
|
||||
for (const v of vehicles) {
|
||||
@@ -54,7 +55,7 @@ function buildFilters(vehicles: CachedVehicle[], targetNames: string[]): Monitor
|
||||
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
||||
});
|
||||
|
||||
return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames, regions };
|
||||
return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames, regions, brands };
|
||||
}
|
||||
|
||||
interface MileageRow {
|
||||
@@ -199,6 +200,7 @@ function mergeVehicles(
|
||||
entity: info.entity || null,
|
||||
project: info.project || null,
|
||||
region: regionMap[plate] || null,
|
||||
brand: info.brand_label || null,
|
||||
targetNames: targetNamesByPlate.get(plate) || [],
|
||||
yesterdayKm: yesterdayMap.get(plate) || 0,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const app = new Hono();
|
||||
const EMPTY_RESPONSE: MonitoringResponse = {
|
||||
vehicles: [],
|
||||
stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 },
|
||||
filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [] },
|
||||
filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [], brands: [] },
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
@@ -20,6 +20,7 @@ function applyFilters(vehicles: CachedVehicle[], params: {
|
||||
search: string; dept: string; customer: string; project: string;
|
||||
entity: string; rentStatus: string; plate: string; platePrefix: string;
|
||||
targetNames: string[]; region: string; mileageMin: string; mileageMax: string;
|
||||
brands: string[];
|
||||
}): CachedVehicle[] {
|
||||
let result = vehicles;
|
||||
|
||||
@@ -32,7 +33,7 @@ function applyFilters(vehicles: CachedVehicle[], params: {
|
||||
);
|
||||
}
|
||||
if (params.dept) result = result.filter(v => params.dept === '__EMPTY__' ? !v.department : v.department === params.dept);
|
||||
if (params.customer) result = result.filter(v => params.customer === '__EMPTY__' ? !v.customer : v.customer === params.customer);
|
||||
if (params.customer) result = result.filter(v => params.customer === '__EMPTY__' ? !v.customer : (v.customer || '').toLowerCase().includes(params.customer.toLowerCase()));
|
||||
if (params.project) result = result.filter(v => v.project === params.project);
|
||||
if (params.entity) result = result.filter(v => v.entity === params.entity);
|
||||
if (params.rentStatus) result = result.filter(v => v.rentStatus === params.rentStatus);
|
||||
@@ -46,6 +47,10 @@ function applyFilters(vehicles: CachedVehicle[], params: {
|
||||
const selectedTargets = new Set(params.targetNames);
|
||||
result = result.filter(v => v.targetNames.some(targetName => selectedTargets.has(targetName)));
|
||||
}
|
||||
if (params.brands.length > 0) {
|
||||
const selectedBrands = new Set(params.brands);
|
||||
result = result.filter(v => v.brand != null && selectedBrands.has(v.brand));
|
||||
}
|
||||
if (params.mileageMin) result = result.filter(v => v.dailyKm >= Number(params.mileageMin));
|
||||
if (params.mileageMax) result = result.filter(v => v.dailyKm <= Number(params.mileageMax));
|
||||
|
||||
@@ -112,6 +117,7 @@ app.get('/', async (c) => {
|
||||
region: c.req.query('region') || '',
|
||||
mileageMin: c.req.query('mileageMin') || '',
|
||||
mileageMax: c.req.query('mileageMax') || '',
|
||||
brands: c.req.queries('brand') || [],
|
||||
};
|
||||
|
||||
let allVehicles: CachedVehicle[];
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface CachedVehicle {
|
||||
entity: string | null;
|
||||
project: string | null;
|
||||
region: string | null;
|
||||
brand: string | null;
|
||||
targetNames: string[];
|
||||
yesterdayKm: number;
|
||||
}
|
||||
@@ -37,6 +38,7 @@ export interface MonitoringFilters {
|
||||
platePrefixes: PlatePrefix[];
|
||||
targetNames: string[];
|
||||
regions: string[];
|
||||
brands: string[];
|
||||
}
|
||||
|
||||
/** 监控缓存 */
|
||||
@@ -80,4 +82,5 @@ export interface VehicleInfoRow {
|
||||
rent_status: string | null;
|
||||
entity: string | null;
|
||||
project: string | null;
|
||||
brand_label: string | null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pool from '../../db.js';
|
||||
import type { VehicleInfoRow } from './types.js';
|
||||
|
||||
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目) */
|
||||
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */
|
||||
export const VEHICLE_INFO_SQL = `SELECT
|
||||
vi.plate_number AS plate,
|
||||
vi.vin AS vin,
|
||||
@@ -18,11 +18,34 @@ export const VEHICLE_INFO_SQL = `SELECT
|
||||
ELSE vs.operation_status
|
||||
END AS rent_status,
|
||||
NULLIF(vi.registered_ownership, '') AS entity,
|
||||
COALESCE(c.project_name, vor.project_name) AS project
|
||||
COALESCE(c.project_name, vor.project_name) AS project,
|
||||
CASE vm.brand
|
||||
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
|
||||
WHEN 'yuejin' THEN '跃进'
|
||||
WHEN 'feichi' THEN '飞驰'
|
||||
WHEN 'sulong' THEN '苏龙'
|
||||
WHEN 'higer' THEN '海格'
|
||||
WHEN 'dongfeng' THEN '东风'
|
||||
WHEN 'yutong' THEN '宇通'
|
||||
WHEN 'chufeng' THEN '楚风'
|
||||
WHEN 'tonghua' THEN '通华'
|
||||
WHEN 'maxus' THEN '大通'
|
||||
WHEN 'mingwei' THEN '明威'
|
||||
WHEN 'wanfeng' THEN '万风'
|
||||
WHEN 'shujie' THEN '舒捷'
|
||||
WHEN 'denza' THEN '腾势'
|
||||
WHEN 'hongyan' THEN '红岩'
|
||||
WHEN 'yuanchang brand' THEN '远程牌'
|
||||
WHEN 'others' THEN '其他'
|
||||
ELSE vm.brand
|
||||
END AS brand_label
|
||||
FROM vehicle_info vi
|
||||
LEFT JOIN vehicle_status vs
|
||||
ON vs.vehicle_id = vi.id
|
||||
AND vs.del_flag = 0
|
||||
LEFT JOIN vehicle_model vm
|
||||
ON vm.id = vi.vehicle_model_id
|
||||
AND vm.del_flag = '0'
|
||||
LEFT JOIN vehicle_lease_order_record vor
|
||||
ON vor.vehicle_id = vi.id
|
||||
AND vor.del_flag = '0'
|
||||
|
||||
Reference in New Issue
Block a user