diff --git a/src/modules/assets/AssetsModule.tsx b/src/modules/assets/AssetsModule.tsx index 1af81e4..f304dcd 100644 --- a/src/modules/assets/AssetsModule.tsx +++ b/src/modules/assets/AssetsModule.tsx @@ -223,11 +223,18 @@ export default function AssetsModule() { setModalLoading(true); const cat = showPlateNumbers.category; - // Weekly categories use the dedicated weekly-detail endpoint - const weeklyTypes: Record = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced', Pending: 'pending' }; + // Weekly categories use the dedicated weekly-detail endpoint. + // Pending 不属于 weekly:weekly-detail 不支持 model/batch/location 过滤, + // 走下面的 /list 路径才能按型号/区域等维度过滤。 + const weeklyTypes: Record = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced' }; if (cat && weeklyTypes[cat]) { setModalVehicles([]); - fetchWeeklyDetail(weeklyTypes[cat]) + fetchWeeklyDetail(weeklyTypes[cat], { + model: showPlateNumbers.model, + batch: showPlateNumbers.batch, + location: showPlateNumbers.location, + source: showPlateNumbers.source, + }) .then(setModalWeeklyDetail) .catch(() => setModalWeeklyDetail([])) .finally(() => setModalLoading(false)); @@ -241,8 +248,10 @@ export default function AssetsModule() { if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch; if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model; if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location; + if (showPlateNumbers.source) params.source = showPlateNumbers.source; if (cat === 'Inventory') params.category = 'Inventory'; if (cat === 'Operating') params.category = 'Operating'; + if (cat === 'Pending') params.category = 'Pending'; if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager; if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer; if (showPlateNumbers.department) params.department = showPlateNumbers.department; diff --git a/src/modules/assets/api.ts b/src/modules/assets/api.ts index c1f90e5..092b291 100644 --- a/src/modules/assets/api.ts +++ b/src/modules/assets/api.ts @@ -50,6 +50,7 @@ export async function fetchVehicleList(params: { department?: string; attendance?: string; subject?: string | null; + source?: string; }): Promise { const query = new URLSearchParams(); if (params.batch) query.set('batch', params.batch); @@ -65,6 +66,7 @@ export async function fetchVehicleList(params: { if (params.department) query.set('department', params.department); if (params.attendance) query.set('attendance', params.attendance); if (params.subject) query.set('subject', params.subject); + if (params.source) query.set('source', params.source); return fetchJson(`${BASE}/list?${query.toString()}`); } @@ -112,6 +114,14 @@ export async function fetchRegionChart( ); } -export async function fetchWeeklyDetail(type: string): Promise { - return fetchJson(`${BASE}/weekly-detail?type=${type}`); +export async function fetchWeeklyDetail( + type: string, + filters?: { model?: string; batch?: string; location?: string; source?: string }, +): Promise { + const params = new URLSearchParams({ type }); + if (filters?.model && filters.model !== 'All') params.set('model', filters.model); + if (filters?.batch && filters.batch !== 'All') params.set('batch', filters.batch); + if (filters?.location && filters.location !== 'All') params.set('location', filters.location); + if (filters?.source) params.set('source', filters.source); + return fetchJson(`${BASE}/weekly-detail?${params.toString()}`); } diff --git a/src/server/routes/vehicles.ts b/src/server/routes/vehicles.ts index 748819f..2836923 100644 --- a/src/server/routes/vehicles.ts +++ b/src/server/routes/vehicles.ts @@ -420,7 +420,7 @@ interface WeeklyStats { // 交车单 SQL const DELIVERED_SQL = `SELECT take.id, DATE(take.handover_date) AS handover_date, - truck.id AS truck_id, truck.plate_number, + CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, dic_contract_type.dic_name AS contract_type, customer.customer_name FROM tab_truck_rent_take take @@ -439,7 +439,7 @@ WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL // 还车单 SQL const RETURNED_SQL = `SELECT r.id, DATE(r.return_date) AS handover_date, - truck.id AS truck_id, truck.plate_number, + CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, dic_contract_type.dic_name AS contract_type, customer.customer_name FROM tab_truck_rent_return r @@ -457,7 +457,7 @@ WHERE r.is_deleted = 0 AND r.return_date IS NOT NULL`; // 替换车单 SQL const REPLACED_SQL = `SELECT take.id, DATE(take.handover_date) AS handover_date, - truck.id AS truck_id, truck.plate_number, + CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, dic_contract_type.dic_name AS contract_type, customer.customer_name FROM tab_truck_rent_take take @@ -880,6 +880,21 @@ app.get('/customer-stats', async (c) => { return c.json(result); }); +// Location 过滤器:支持展示区域(嘉兴/广东/北京/新疆/其他)、库存区域(江浙沪/其它)、 +// 城市(嘉兴市)、宏观区域(华东/华南/...)。 +// '其他' 在两个体系里都存在(资产表的"库存-其他" vs 区域表的"其他"宏观区域), +// 用 source 区分:source==='asset' 时按 v.location 匹配,其它情况按宏观区域匹配。 +function filterByLocation(vehicles: Vehicle[], location: string, source?: string): Vehicle[] { + const macroRegions = ['华东', '华南', '华北', '华中', '西南', '西北']; + const isMacro = macroRegions.includes(location) || (location === '其他' && source !== 'asset'); + if (isMacro) { + return vehicles.filter((v) => mapMacroRegion(v.province, v.city) === location); + } + const inventoryRegionMap: Record = { '江浙沪': '嘉兴', '其它': '其他' }; + const mappedLocation = inventoryRegionMap[location] || location; + return vehicles.filter((v) => v.location === mappedLocation || v.city === location || resolveCity(v.city, v.province) === location); +} + // Vehicle type filter map (same logic as /by-type) const VEHICLE_TYPE_FILTERS: Record boolean> = { '4.5T普货': (v) => v.type === '4.5T' && !v.model.includes('冷链'), @@ -925,15 +940,7 @@ app.get('/list', async (c) => { filtered = filtered.filter((v) => v.model === model); } if (location && location !== 'All') { - // Support: display regions (嘉兴/广东), inventory regions (江浙沪), cities (嘉兴市), macro regions (华东/华南) - const macroRegions = ['华东', '华南', '华北', '华中', '西南', '西北']; - if (macroRegions.includes(location) || location === '其他') { - filtered = filtered.filter((v) => mapMacroRegion(v.province, v.city) === location); - } else { - const inventoryRegionMap: Record = { '江浙沪': '嘉兴', '其它': '其他' }; - const mappedLocation = inventoryRegionMap[location] || location; - filtered = filtered.filter((v) => v.location === mappedLocation || v.city === location || resolveCity(v.city, v.province) === location); - } + filtered = filterByLocation(filtered, location, c.req.query('source')); } if (status && status !== 'All') { filtered = filtered.filter((v) => v.status === status); @@ -943,6 +950,8 @@ app.get('/list', async (c) => { filtered = filtered.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal'); } else if (category === 'Operating') { filtered = filtered.filter((v) => v.status === 'Operating'); + } else if (category === 'Pending') { + filtered = filtered.filter((v) => v.status === 'Pending'); } } if (manager) { @@ -1023,8 +1032,11 @@ app.get('/inventory-stats', async (c) => { }); // GET /api/vehicles/weekly-detail?type=delivered|returned|replaced|pending +// Optional filters: model, batch, location, source — 按缓存车辆集合的 truck_id 交集过滤 app.get('/weekly-detail', async (c) => { const type = c.req.query('type'); + const { model, batch, location } = c.req.query(); + const source = c.req.query('source'); let sql: string; if (type === 'delivered') { sql = `${DELIVERED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`; @@ -1033,17 +1045,33 @@ app.get('/weekly-detail', async (c) => { } else if (type === 'replaced') { sql = `${REPLACED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`; } else if (type === 'pending') { - sql = `SELECT truck.id AS truck_id, truck.plate_number, NULL AS handover_date, NULL AS contract_type, NULL AS customer_name + sql = `SELECT CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, NULL AS handover_date, NULL AS contract_type, NULL AS customer_name FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.truck_rent_status=7`; } else if (type === 'new') { - sql = `SELECT truck.id AS truck_id, truck.plate_number, truck.create_time AS handover_date, NULL AS contract_type, NULL AS customer_name + sql = `SELECT CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, truck.create_time AS handover_date, NULL AS contract_type, NULL AS customer_name FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.create_time >= ${WEEK_START_SQL} AND truck.create_time < ${WEEK_END_SQL} ORDER BY truck.create_time DESC`; } else { return c.json([]); } const [rows] = await pool.query(sql); - const masked = (rows as any[]).map(r => ({ ...r, customer_name: maskCustomerName(r.customer_name) })); + let result = rows as any[]; + + // 按型号/批次/区域过滤:借助缓存车辆集,取 truck_id 交集 + const hasModelFilter = model && model !== 'All'; + const hasBatchFilter = batch && batch !== 'All'; + const hasLocationFilter = location && location !== 'All'; + if (hasModelFilter || hasBatchFilter || hasLocationFilter) { + const vehicles = await getVehiclesForUser(c); + let pool2 = vehicles; + if (hasModelFilter) pool2 = pool2.filter((v) => v.model === model); + if (hasBatchFilter) pool2 = pool2.filter((v) => (v.contractNo || '未知') === batch); + if (hasLocationFilter) pool2 = filterByLocation(pool2, location, source); + const truckSet = new Set(pool2.map((v) => String(v.id))); + result = result.filter((r: any) => truckSet.has(String(r.truck_id))); + } + + const masked = result.map(r => ({ ...r, customer_name: maskCustomerName(r.customer_name) })); return c.json(masked); });