feat: add dept/region/customer stats APIs and extend /list filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,40 @@ function mapInventoryRegion(region: string): string {
|
||||
return '其它';
|
||||
}
|
||||
|
||||
// Macro-region mapping: province/city -> 华东/华南/华北/华中/西南/西北/其他
|
||||
function mapMacroRegion(province: string | null, city: string | null): string {
|
||||
const prov = (province || '').trim();
|
||||
const c = (city || '').trim();
|
||||
const loc = prov + c;
|
||||
if (/上海|江苏|浙江|安徽|福建|江西|山东|南京|杭州|合肥|济南|青岛|苏州|宁波|厦门|嘉兴|无锡/.test(loc)) return '华东';
|
||||
if (/广东|广西|海南|广州|深圳|佛山|东莞|珠海|惠州|中山|南宁/.test(loc)) return '华南';
|
||||
if (/北京|天津|河北|山西|内蒙古|石家庄|太原|呼和浩特/.test(loc)) return '华北';
|
||||
if (/河南|湖北|湖南|郑州|武汉|长沙/.test(loc)) return '华中';
|
||||
if (/重庆|四川|贵州|云南|西藏|成都|昆明|贵阳/.test(loc)) return '西南';
|
||||
if (/陕西|甘肃|青海|宁夏|新疆|西安|兰州|乌鲁木齐/.test(loc)) return '西北';
|
||||
return '其他';
|
||||
}
|
||||
|
||||
type VehicleTypeCounts = { t4_5: number; t4_5c: number; t18: number; t49: number; trailer: number; other: number; total: number };
|
||||
|
||||
function classifyVehicleType(v: Vehicle): keyof Omit<VehicleTypeCounts, 'total'> {
|
||||
if (v.type === '4.5T' && !v.model.includes('冷链')) return 't4_5';
|
||||
if (v.type === '4.5T' && v.model.includes('冷链')) return 't4_5c';
|
||||
if (v.type === '18T') return 't18';
|
||||
if (v.type === '49T') return 't49';
|
||||
if (v.type === '挂车' || v.model.includes('挂车')) return 'trailer';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function countByType(vehicles: Vehicle[]): VehicleTypeCounts {
|
||||
const counts: VehicleTypeCounts = { t4_5: 0, t4_5c: 0, t18: 0, t49: 0, trailer: 0, other: 0, total: 0 };
|
||||
for (const v of vehicles) {
|
||||
counts[classifyVehicleType(v)]++;
|
||||
counts.total++;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
// Map rental status to frontend status
|
||||
// Actual DB values: 在库(0), 自营(1), 租赁(2), 待交车(7), 挂靠(8), 异动(12)
|
||||
function mapStatus(rentStatus: string | null): 'Operating' | 'Inventory' | 'Pending' | 'Abnormal' {
|
||||
@@ -605,12 +639,121 @@ app.get('/inventory-analysis', async (c) => {
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// GET /api/vehicles/dept-stats — department & manager breakdown
|
||||
app.get('/dept-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const withManager = vehicles.filter((v) => v.customerManager);
|
||||
|
||||
const deptMap = new Map<string, Map<string, Vehicle[]>>();
|
||||
for (const v of withManager) {
|
||||
const dept = v.departmentName || '未分配';
|
||||
const mgr = v.customerManager!;
|
||||
if (!deptMap.has(dept)) deptMap.set(dept, new Map());
|
||||
const mgrMap = deptMap.get(dept)!;
|
||||
if (!mgrMap.has(mgr)) mgrMap.set(mgr, []);
|
||||
mgrMap.get(mgr)!.push(v);
|
||||
}
|
||||
|
||||
const result = Array.from(deptMap.entries()).map(([department, mgrMap]) => {
|
||||
const allDeptVehicles = Array.from(mgrMap.values()).flat();
|
||||
const managers = Array.from(mgrMap.entries())
|
||||
.map(([manager, mvs]) => ({ manager, department, ...countByType(mvs) }))
|
||||
.sort((a, b) => b.total - a.total);
|
||||
return {
|
||||
department,
|
||||
totalAssets: allDeptVehicles.length,
|
||||
operatingCount: allDeptVehicles.filter((v) => v.status === 'Operating').length,
|
||||
idleCount: allDeptVehicles.filter((v) => v.status !== 'Operating').length,
|
||||
managers,
|
||||
};
|
||||
}).sort((a, b) => b.totalAssets - a.totalAssets);
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// GET /api/vehicles/region-stats — macro-region breakdown for operating vehicles
|
||||
app.get('/region-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const operating = vehicles.filter((v) => v.status === 'Operating');
|
||||
|
||||
const regionMap = new Map<string, Vehicle[]>();
|
||||
for (const v of operating) {
|
||||
const region = mapMacroRegion(v.province, v.city);
|
||||
if (!regionMap.has(region)) regionMap.set(region, []);
|
||||
regionMap.get(region)!.push(v);
|
||||
}
|
||||
|
||||
const regionOrder = ['华东', '华南', '华北', '华中', '西南', '西北', '其他'];
|
||||
const result = regionOrder
|
||||
.filter((r) => regionMap.has(r))
|
||||
.map((region) => {
|
||||
const rv = regionMap.get(region)!;
|
||||
const customers = Array.from(new Set(rv.map((v) => v.customerName).filter(Boolean))) as string[];
|
||||
const typeBreakdown = ['4.5T', '18T', '49T'].map((type) => {
|
||||
const typeVehicles = rv.filter((v) => v.type === type);
|
||||
return {
|
||||
type,
|
||||
total: typeVehicles.length,
|
||||
operating: typeVehicles.filter((v) => v.status === 'Operating').length,
|
||||
inventory: typeVehicles.filter((v) => v.status === 'Inventory').length,
|
||||
customers: Array.from(new Set(typeVehicles.map((v) => v.customerName).filter(Boolean))) as string[],
|
||||
};
|
||||
}).filter((t) => t.total > 0);
|
||||
|
||||
return { region, totalAssets: rv.length, operatingCount: rv.filter((v) => v.status === 'Operating').length, inventoryCount: rv.filter((v) => v.status === 'Inventory').length, customers, typeBreakdown };
|
||||
});
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// GET /api/vehicles/customer-stats — per-customer breakdown for operating vehicles
|
||||
app.get('/customer-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const operating = vehicles.filter((v) => v.status === 'Operating' && v.customerName);
|
||||
|
||||
const custMap = new Map<string, Vehicle[]>();
|
||||
for (const v of operating) {
|
||||
const cust = v.customerName!;
|
||||
if (!custMap.has(cust)) custMap.set(cust, []);
|
||||
custMap.get(cust)!.push(v);
|
||||
}
|
||||
|
||||
const result = Array.from(custMap.entries())
|
||||
.map(([customer, cvs]) => {
|
||||
const first = cvs[0];
|
||||
return {
|
||||
customer,
|
||||
manager: first.customerManager || '',
|
||||
brand: first.brandLabel || '',
|
||||
department: first.departmentName || '',
|
||||
region: mapMacroRegion(first.province, first.city),
|
||||
city: first.city || '',
|
||||
...countByType(cvs),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.total - a.total);
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// Vehicle type filter map (same logic as /by-type)
|
||||
const VEHICLE_TYPE_FILTERS: Record<string, (v: Vehicle) => boolean> = {
|
||||
'4.5T普货': (v) => v.type === '4.5T' && !v.model.includes('冷链'),
|
||||
'4.5T冷链': (v) => v.type === '4.5T' && v.model.includes('冷链'),
|
||||
'18T': (v) => v.type === '18T',
|
||||
'49T': (v) => v.type === '49T',
|
||||
'其他': (v) => !['4.5T', '18T', '49T'].includes(v.type),
|
||||
};
|
||||
|
||||
// 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();
|
||||
const { batch, model, location, status, category, vehicleType, manager, customer, isColdChain, isTrailer } = c.req.query();
|
||||
|
||||
let filtered = vehicles;
|
||||
if (vehicleType && VEHICLE_TYPE_FILTERS[vehicleType]) {
|
||||
filtered = filtered.filter(VEHICLE_TYPE_FILTERS[vehicleType]);
|
||||
}
|
||||
if (batch && batch !== 'All') {
|
||||
filtered = filtered.filter((v) => (v.contractNo || '未知') === batch);
|
||||
}
|
||||
@@ -633,6 +776,20 @@ app.get('/list', async (c) => {
|
||||
filtered = filtered.filter((v) => v.status === 'Operating');
|
||||
}
|
||||
}
|
||||
if (manager) {
|
||||
filtered = filtered.filter((v) => v.customerManager === manager);
|
||||
}
|
||||
if (customer) {
|
||||
filtered = filtered.filter((v) => v.customerName === customer);
|
||||
}
|
||||
if (isColdChain !== undefined) {
|
||||
const wantCold = isColdChain === 'true';
|
||||
filtered = filtered.filter((v) => wantCold ? v.model.includes('冷链') : !v.model.includes('冷链'));
|
||||
}
|
||||
if (isTrailer !== undefined) {
|
||||
const wantTrailer = isTrailer === 'true';
|
||||
filtered = filtered.filter((v) => wantTrailer ? (v.type === '挂车' || v.model.includes('挂车')) : !(v.type === '挂车' || v.model.includes('挂车')));
|
||||
}
|
||||
|
||||
return c.json(
|
||||
filtered.map((v) => ({
|
||||
|
||||
Reference in New Issue
Block a user