import { Hono } from 'hono'; import pool from '../db.js'; import type { VehicleRow, Vehicle, SummaryData, TypeSummary, ModelSummary, BatchSummary, BatchGroup, InventoryTypeSummary, } from '../types.js'; const app = new Hono(); const MAIN_SQL = `SELECT truck.id AS id, truck.plate_number AS 车牌号, truck.vin AS vin, truck.brand AS 车辆品牌, truck.model AS 车辆型号, truck.color AS 车辆颜色, truck.rent_from_company AS 租赁公司, dic_ascription_status.dic_name AS 车辆归属状态Label, dic_type.dic_name AS 车辆型号Label, truck.stock_area AS 库存区域, truck.truck_rent_status AS 车辆租赁状态, dic_status.dic_name AS 车辆租赁状态Label, truck.is_operation AS 是否营运, info.province AS 省, info.city AS 市, info.lat AS 纬度, info.lng AS 经度, dic_brand.dic_name AS 车辆品牌Label, si.contract_id AS 合同ID, COALESCE(c.contract_no, si.contract_no) AS 合同编码, cus.customer_name AS 客户名称, org.org_name AS 合同归属公司, dep.dep_name AS 合同归属部门, org_truck.org_name AS 主体, c.project_name AS 项目名称, u.user_name AS 客户经理 FROM tab_truck truck LEFT JOIN tab_truck_remote_sync_realtime_info info ON info.id = truck.id LEFT JOIN tab_dic dic_type ON dic_type.parent_code = 'dic_truck_type' AND dic_type.dic_code = truck.model AND dic_type.is_deleted = 0 LEFT JOIN tab_dic dic_status ON dic_status.parent_code = 'dic_truck_rent_status' AND dic_status.dic_code = truck.truck_rent_status AND dic_status.is_deleted = 0 LEFT JOIN tab_dic dic_brand ON dic_brand.parent_code = 'dic_vehicle_brand' AND dic_brand.dic_code = truck.brand AND dic_brand.is_deleted = 0 LEFT JOIN tab_truck_status_info si ON si.truck_id = truck.id AND si.is_deleted = 0 LEFT JOIN tab_contract c ON c.id = si.contract_id AND c.is_deleted = 0 LEFT JOIN tab_customer cus ON cus.id = c.customer_id AND cus.is_deleted = 0 LEFT JOIN tab_org org ON org.id = c.org_id AND org.is_deleted = 0 LEFT JOIN tab_org org_truck ON org_truck.id = truck.org_id AND org_truck.is_deleted = 0 LEFT JOIN tab_dic dic_ascription_status ON dic_ascription_status.parent_code = 'dic_truck_ascription_status' AND dic_ascription_status.dic_code = truck.ascription_status AND dic_ascription_status.is_deleted = 0 LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0 LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 WHERE truck.is_deleted = 0 AND truck.is_operation = 1`; // Region mapping: province/city -> display region const REGIONS = ['嘉兴', '广东', '北京', '新疆', '其他'] as const; const INVENTORY_REGIONS = ['江浙沪', '广东', '新疆', '其它'] as const; function mapRegion(province: string | null, city: string | null): string { if (!province && !city) return '其他'; const loc = (city || province || '').trim(); if (loc.includes('嘉兴') || loc.includes('浙江') || loc.includes('上海') || loc.includes('江苏')) return '嘉兴'; if (loc.includes('广东') || loc.includes('广州') || loc.includes('深圳') || loc.includes('佛山') || loc.includes('东莞')) return '广东'; if (loc.includes('北京')) return '北京'; if (loc.includes('新疆') || loc.includes('乌鲁木齐')) return '新疆'; // Also check province const prov = (province || '').trim(); if (prov.includes('浙江') || prov.includes('上海') || prov.includes('江苏')) return '嘉兴'; if (prov.includes('广东')) return '广东'; if (prov.includes('北京')) return '北京'; if (prov.includes('新疆')) return '新疆'; return '其他'; } function mapInventoryRegion(region: string): string { if (region === '嘉兴') return '江浙沪'; if (region === '广东') return '广东'; if (region === '新疆') return '新疆'; return '其它'; } // Map rental status to frontend status // Actual DB values: 在库(0), 自营(1), 租赁(2), 待交车(7), 挂靠(8), 异动(12) function mapStatus(rentStatus: string | null): 'Operating' | 'Inventory' | 'Abnormal' { if (!rentStatus) return 'Inventory'; const s = rentStatus.trim(); if (s === '租赁' || s === '自营' || s === '挂靠') return 'Operating'; if (s === '在库' || s === '待交车') return 'Inventory'; if (s === '异动') return 'Abnormal'; return 'Inventory'; } // Map ownership status // Actual DB values: 自有(0), 外租(1), 挂靠(2) function mapOwnership(ascriptionLabel: string | null): string { if (!ascriptionLabel) return 'Self'; const s = ascriptionLabel.trim(); if (s === '自有') return 'Self'; if (s === '外租') return 'Leased'; if (s === '挂靠') return 'Hanging'; return 'Self'; } // Derive vehicle type category from model label // Actual DB values: 4.5吨冷链车, 4.5吨货车, 18吨双飞翼货车, 18吨厢式货车, 49吨牵引车头, 35吨牵引车头, // 重型集装箱半挂车, 重型平板半挂车, 氢能叉车, SJ型蓄电池观光车, 公务用车/小客车, 挂靠油车 function deriveType(modelLabel: string | null, brandLabel: string | null): string { const label = (modelLabel || '').trim(); if (label.includes('4.5吨')) return '4.5T'; if (label.includes('18吨')) return '18T'; if (label.includes('49吨')) return '49T'; if (label.includes('35吨')) return '35T'; if (label.includes('叉车')) return '叉车'; if (label.includes('半挂车')) return '挂车'; return '其他车型'; } // Tag → alias mapping with sort order // tag is generated as: brand-modelLabel-color[+rentCompany if 外租] // Some tags are merged (e.g. 嘉氢 red + 嘉氢 blue/green → one alias) const MODEL_ALIAS_MAP: Record = { // 4.5T 普货 '现代-4.5吨货车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T普货(交投)', order: 101 }, '现代-4.5吨货车-白': { alias: '现代4.5T普货(恒运)', order: 102 }, // 4.5T 冷链 '帕力安牌-4.5吨冷链车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T冷链(交投)', order: 201 }, '帕力安牌-4.5吨冷链车-白色': { alias: '现代4.5T冷链(羚牛)', order: 202 }, '跃进-4.5吨冷链车-白/绿/灰': { alias: '跃进4.5T冷链', order: 203 }, // 18T '飞驰-18吨厢式货车-红': { alias: '飞驰18T(红车)', order: 301 }, '飞驰-18吨厢式货车-白/绿': { alias: '飞驰18T(白车)', order: 302 }, '楚风-18吨厢式货车-白': { alias: '楚风18T厢货', order: 303 }, '苏龙-18吨双飞翼货车-白': { alias: '苏龙18T飞翼', order: 304 }, '苏龙-18吨双飞翼货车-白色': { alias: '苏龙18T飞翼', order: 304 }, // dirty data, merge '苏龙-18吨双飞翼货车-白安吉天地物流科技有限公司': { alias: '苏龙18T飞翼(安吉)', order: 305 }, '帕力安牌-18吨双飞翼货车-白': { alias: '现代18T双飞翼(羚牛)', order: 306 }, // 49T '宇通-49吨牵引车头-白': { alias: '49T宇通', order: 401 }, '飞驰-49吨牵引车头-白/蓝/绿': { alias: '49T飞驰', order: 402 }, '飞驰-49吨牵引车头-白/蓝/绿嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 }, '飞驰-49吨牵引车头-红嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 }, // merge with above '飞驰-49吨牵引车头-红浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-红)', order: 404 }, '飞驰-49吨牵引车头-白/蓝/绿浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-蓝白绿)', order: 405 }, '楚风-49吨牵引车头-蓝/黑海珀特科技(北京)有限公司': { alias: '49T楚风(海珀特)', order: 406 }, // 其他 '红岩-35吨牵引车头-红色': { alias: '35T油车', order: 501 }, '其他-氢能叉车-蓝白绿': { alias: '氢能叉车', order: 502 }, '通华-重型集装箱半挂车-红色浙江锦昌仓储有限公司': { alias: '挂车', order: 503 }, '通华-重型集装箱半挂车-红色嘉兴市鼎义物流有限公司': { alias: '挂车', order: 503 }, '通华-重型集装箱半挂车-红色': { alias: '挂车', order: 503 }, '大通-重型集装箱半挂车-红色': { alias: '挂车', order: 503 }, '明威-重型集装箱半挂车-红色': { alias: '挂车', order: 503 }, '明威-重型集装箱半挂车-红': { alias: '挂车', order: 503 }, '万风-重型平板半挂车-红': { alias: '挂车', order: 503 }, '舒捷-SJ型蓄电池观光车-蓝白': { alias: '观光车', order: 504 }, '东风-挂靠油车-白色': { alias: '公务车/挂靠车', order: 505 }, '腾势-公务用车/小客车-黑': { alias: '公务车/挂靠车', order: 505 }, '腾势-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 }, '其他-公务用车/小客车-蓝色': { alias: '公务车/挂靠车', order: 505 }, '远程牌-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 }, '大通-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 }, }; function deriveModelTag( brandLabel: string | null, modelLabel: string | null, color: string | null, ownershipLabel: string | null, rentCompany: string | null, ): string { const brand = (brandLabel || '').trim(); const model = (modelLabel || '').trim(); const c = (color || '').trim(); const isRented = ownershipLabel?.trim() === '外租'; const company = isRented ? (rentCompany || '').trim() : ''; if (!brand && !model) return '未知车型'; const tag = `${brand}-${model}-${c}${company}`; const mapped = MODEL_ALIAS_MAP[tag]; return mapped ? mapped.alias : tag; } function getModelOrder(model: string): number { // Find the order from alias mapping for (const entry of Object.values(MODEL_ALIAS_MAP)) { if (entry.alias === model) return entry.order; } return 999; } function transformRow(row: VehicleRow): Vehicle { const region = mapRegion(row.省, row.市); return { id: row.id, plateNumber: row.车牌号 || '', vin: row.vin || '', type: deriveType(row.车辆型号Label, row.车辆品牌Label), model: deriveModelTag(row.车辆品牌Label, row.车辆型号Label, row.车辆颜色, row.车辆归属状态Label, row.租赁公司), color: row.车辆颜色 || '', location: region, region, status: mapStatus(row.车辆租赁状态Label), ownership: mapOwnership(row.车辆归属状态Label), rentCompany: row.租赁公司 || '', contractNo: row.合同编码, customerName: row.客户名称, orgName: row.合同归属公司, departmentName: row.合同归属部门, subjectOrg: row.主体, projectName: row.项目名称, customerManager: row.客户经理, brandLabel: row.车辆品牌Label, }; } // Cache for vehicles data (refresh every 5 minutes) let cachedVehicles: Vehicle[] = []; let lastFetchTime = 0; const CACHE_TTL = 60 * 1000; async function getVehicles(): Promise { const now = Date.now(); if (cachedVehicles.length > 0 && now - lastFetchTime < CACHE_TTL) { return cachedVehicles; } const [rows] = await pool.query(MAIN_SQL); cachedVehicles = (rows as VehicleRow[]).map(transformRow); lastFetchTime = now; return cachedVehicles; } function getRegionCounts(vehicles: Vehicle[], regions: readonly string[]): Record { return regions.reduce((acc, reg) => { acc[reg] = vehicles.filter((v) => v.location === reg).length; return acc; }, {} as Record); } function getStats(list: Vehicle[]) { return { total: list.length, inventory: list.filter((v) => v.status === 'Inventory').length, inventoryRegions: getRegionCounts( list.filter((v) => v.status === 'Inventory'), REGIONS, ), pending: 0, operating: list.filter((v) => v.status === 'Operating').length, weeklyDelivered: 0, weeklyReturned: 0, weeklyReplaced: 0, }; } // Week range: last Saturday 00:00 to this Friday 23:59 // MySQL: WEEKDAY() returns 0=Monday..6=Sunday, Saturday=5 // "上周六-本周五": offset from today to last Saturday const WEEK_START_SQL = `DATE_SUB(CURDATE(), INTERVAL (WEEKDAY(CURDATE()) + 2) % 7 DAY)`; const WEEK_END_SQL = `DATE_ADD(${WEEK_START_SQL}, INTERVAL 7 DAY)`; interface WeeklyStats { pendingDelivery: number; weeklyNew: number; weeklyRemoved: number; weeklyDelivered: number; weeklyReturned: number; weeklyReplaced: number; } // 交车单 SQL const DELIVERED_SQL = `SELECT take.id, DATE(take.handover_date) AS handover_date, truck.id AS truck_id, truck.plate_number, dic_contract_type.dic_name AS contract_type, customer.customer_name FROM tab_truck_rent_take take LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id LEFT JOIN tab_contract contract ON task.contract_id = contract.id LEFT JOIN tab_customer customer ON contract.customer_id = customer.id LEFT JOIN tab_dic dic_contract_type ON dic_contract_type.parent_code = 'dic_contract_type' AND dic_contract_type.dic_code = contract.contract_type AND dic_contract_type.is_deleted = 0 WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL AND task.task_type = 1 AND task.task_status = 1 AND take.update_time 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, dic_contract_type.dic_name AS contract_type, customer.customer_name FROM tab_truck_rent_return r LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id LEFT JOIN tab_contract contract ON task.contract_id = contract.id LEFT JOIN tab_customer customer ON contract.customer_id = customer.id LEFT JOIN tab_dic dic_contract_type ON dic_contract_type.parent_code = 'dic_contract_type' AND dic_contract_type.dic_code = contract.contract_type AND dic_contract_type.is_deleted = 0 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, dic_contract_type.dic_name AS contract_type, customer.customer_name FROM tab_truck_rent_take take LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id LEFT JOIN tab_contract contract ON task.contract_id = contract.id LEFT JOIN tab_customer customer ON contract.customer_id = customer.id LEFT JOIN tab_dic dic_contract_type ON dic_contract_type.parent_code = 'dic_contract_type' AND dic_contract_type.dic_code = contract.contract_type AND dic_contract_type.is_deleted = 0 WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL AND task.task_type = 3 AND task.task_status = 1 AND take.update_time IS NOT NULL`; let cachedWeeklyStats: WeeklyStats | null = null; let weeklyStatsLastFetch = 0; async function getWeeklyStats(): Promise { const now = Date.now(); if (cachedWeeklyStats && now - weeklyStatsLastFetch < CACHE_TTL) { return cachedWeeklyStats; } const [[pendingRows], [newRows], [removedRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([ pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE is_deleted=0 AND is_operation=1 AND truck_rent_status=7`), pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE is_deleted=0 AND is_operation=1 AND create_time >= ${WEEK_START_SQL} AND create_time < ${WEEK_END_SQL}`), pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE (is_operation=0 OR is_deleted=1) AND update_time >= ${WEEK_START_SQL} AND update_time < ${WEEK_END_SQL}`), pool.query(`SELECT COUNT(*) AS cnt FROM (${DELIVERED_SQL}) t WHERE t.handover_date >= ${WEEK_START_SQL} AND t.handover_date < ${WEEK_END_SQL}`), pool.query(`SELECT COUNT(*) AS cnt FROM (${RETURNED_SQL}) t WHERE t.handover_date >= ${WEEK_START_SQL} AND t.handover_date < ${WEEK_END_SQL}`), pool.query(`SELECT COUNT(*) AS cnt FROM (${REPLACED_SQL}) t WHERE t.handover_date >= ${WEEK_START_SQL} AND t.handover_date < ${WEEK_END_SQL}`), ]); cachedWeeklyStats = { pendingDelivery: (pendingRows as any[])[0]?.cnt || 0, weeklyNew: (newRows as any[])[0]?.cnt || 0, weeklyRemoved: (removedRows as any[])[0]?.cnt || 0, weeklyDelivered: (deliveredRows as any[])[0]?.cnt || 0, weeklyReturned: (returnedRows as any[])[0]?.cnt || 0, weeklyReplaced: (replacedRows as any[])[0]?.cnt || 0, }; weeklyStatsLastFetch = now; return cachedWeeklyStats; } // GET /api/vehicles/summary app.get('/summary', async (c) => { const [vehicles, weekly] = await Promise.all([getVehicles(), getWeeklyStats()]); const summary: SummaryData = { totalAssets: vehicles.length, operating: { total: vehicles.filter((v) => v.status === 'Operating').length, self: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Self').length, leased: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Leased').length, public: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Public').length, hanging: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Hanging').length, }, inventory: { total: vehicles.filter((v) => v.status === 'Inventory').length, inStock: vehicles.filter((v) => v.status === 'Inventory').length, abnormal: vehicles.filter((v) => v.status === 'Abnormal').length, }, ...weekly, }; return c.json(summary); }); // GET /api/vehicles/by-type app.get('/by-type', async (c) => { const vehicles = await getVehicles(); const typeFilters = [ { name: '4.5T普货', filter: (v: Vehicle) => v.type === '4.5T' && !v.model.includes('冷链') }, { name: '4.5T冷链', filter: (v: Vehicle) => v.type === '4.5T' && v.model.includes('冷链') }, { name: '18T', filter: (v: Vehicle) => v.type === '18T' }, { name: '49T', filter: (v: Vehicle) => v.type === '49T' }, { name: '其他', filter: (v: Vehicle) => !['4.5T', '18T', '49T'].includes(v.type) }, ]; const result: TypeSummary[] = typeFilters.map((t) => { const typeVehicles = vehicles.filter(t.filter); const models = Array.from(new Set(typeVehicles.map((v) => v.model))); const modelSummaries: ModelSummary[] = models.map((model) => { const modelVehicles = typeVehicles.filter((v) => v.model === model); // Use contractNo as batch identifier const batches = Array.from(new Set(modelVehicles.map((v) => v.contractNo || '未知'))).filter(Boolean); return { model, ...getStats(modelVehicles), batches: batches.map((batch) => ({ batch, ...getStats(modelVehicles.filter((v) => (v.contractNo || '未知') === batch)), })), }; }); const typeStats = getStats(typeVehicles); return { type: t.name, totalAssets: typeVehicles.length, totalInventory: typeStats.inventory, totalOperating: typeStats.operating, inventoryRegions: typeStats.inventoryRegions, pending: typeStats.pending, weeklyDelivered: typeStats.weeklyDelivered, weeklyReturned: typeStats.weeklyReturned, weeklyReplaced: typeStats.weeklyReplaced, models: modelSummaries.sort((a, b) => getModelOrder(a.model) - getModelOrder(b.model)), }; }); return c.json(result); }); // GET /api/vehicles/by-batch app.get('/by-batch', async (c) => { const vehicles = await getVehicles(); const batches = Array.from(new Set(vehicles.map((v) => v.contractNo || '未知'))) .filter(Boolean) .sort() .reverse(); const result: BatchGroup[] = batches.map((batch) => { const batchVehicles = vehicles.filter((v) => (v.contractNo || '未知') === batch); const models = Array.from(new Set(batchVehicles.map((v) => v.model))); return { batch, ...getStats(batchVehicles), models: models.map((model) => { const modelVehicles = batchVehicles.filter((v) => v.model === model); return { model, type: modelVehicles[0]?.type || '', ...getStats(modelVehicles), }; }), }; }); return c.json(result); }); // GET /api/vehicles/inventory-analysis app.get('/inventory-analysis', async (c) => { const vehicles = await getVehicles(); const typeFilters = [ { name: '4.5T普货', filter: (v: Vehicle) => v.type === '4.5T' && !v.model.includes('冷链') }, { name: '4.5T冷链', filter: (v: Vehicle) => v.type === '4.5T' && v.model.includes('冷链') }, { name: '18T', filter: (v: Vehicle) => v.type === '18T' }, { name: '49T', filter: (v: Vehicle) => v.type === '49T' }, { name: '其他', filter: (v: Vehicle) => !['4.5T', '18T', '49T'].includes(v.type) }, ]; const result: InventoryTypeSummary[] = typeFilters.map((t) => { const typeVehicles = vehicles.filter(t.filter); const models = Array.from(new Set(typeVehicles.map((v) => v.model))); const modelData = models.map((model) => { const modelVehicles = typeVehicles.filter((v) => v.model === model); const inventoryVehicles = modelVehicles.filter((v) => v.status === 'Inventory'); return { model, totalAssets: modelVehicles.length, totalInventory: inventoryVehicles.length, regions: INVENTORY_REGIONS.reduce( (acc, reg) => { acc[reg] = inventoryVehicles.filter((v) => mapInventoryRegion(v.location) === reg).length; return acc; }, {} as Record, ), }; }); const typeInventory = typeVehicles.filter((v) => v.status === 'Inventory'); return { type: t.name, totalAssets: typeVehicles.length, totalInventory: typeInventory.length, models: modelData, regionSubtotals: INVENTORY_REGIONS.reduce( (acc, reg) => { acc[reg] = typeInventory.filter((v) => mapInventoryRegion(v.location) === reg).length; return acc; }, {} as Record, ), }; }); return c.json(result); }); // GET /api/vehicles/list — flat list with optional filters app.get('/list', async (c) => { const vehicles = await getVehicles(); const { batch, model, location, status, category } = c.req.query(); let filtered = vehicles; if (batch && batch !== 'All') { filtered = filtered.filter((v) => (v.contractNo || '未知') === batch); } if (model && model !== 'All') { filtered = filtered.filter((v) => v.model === model); } if (location && location !== 'All') { // Support both display region names and inventory region names const inventoryRegionMap: Record = { '江浙沪': '嘉兴', '其它': '其他' }; const mappedLocation = inventoryRegionMap[location] || location; filtered = filtered.filter((v) => v.location === mappedLocation); } if (status && status !== 'All') { filtered = filtered.filter((v) => v.status === status); } if (category) { if (category === 'Inventory') { filtered = filtered.filter((v) => v.status === 'Inventory'); } else if (category === 'Operating') { filtered = filtered.filter((v) => v.status === 'Operating'); } } return c.json( filtered.map((v) => ({ id: v.id, plateNumber: v.plateNumber, type: v.type, model: v.model, location: v.location, status: v.status, ownership: v.ownership, contractNo: v.contractNo, customerName: v.customerName, subjectOrg: v.subjectOrg, })), ); }); // GET /api/vehicles/weekly-detail?type=delivered|returned|replaced|pending app.get('/weekly-detail', async (c) => { const type = c.req.query('type'); 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`; } else if (type === 'returned') { sql = `${RETURNED_SQL} AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL} ORDER BY r.return_date DESC`; } 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 FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.truck_rent_status=7`; } else { return c.json([]); } const [rows] = await pool.query(sql); return c.json(rows); }); // GET /api/vehicles/refresh — force cache refresh app.get('/refresh', async (c) => { lastFetchTime = 0; const vehicles = await getVehicles(); return c.json({ message: 'Cache refreshed', count: vehicles.length }); }); export default app;