Compare commits
6 Commits
9a7382101b
...
f051e3f5aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f051e3f5aa | ||
|
|
17f2222e52 | ||
|
|
4cac404f49 | ||
|
|
b4fdbd14a7 | ||
|
|
01a64431dc | ||
|
|
73d5afde5c |
1325
src/App.tsx
1325
src/App.tsx
File diff suppressed because it is too large
Load Diff
25
src/api.ts
25
src/api.ts
@@ -2,6 +2,9 @@ import type {
|
||||
SummaryData,
|
||||
TypeSummary,
|
||||
VehicleListItem,
|
||||
DeptGroup,
|
||||
RegionGroup,
|
||||
CustomerStats,
|
||||
} from './types';
|
||||
|
||||
const BASE = '/api/vehicles';
|
||||
@@ -26,6 +29,11 @@ export async function fetchVehicleList(params: {
|
||||
location?: string;
|
||||
status?: string;
|
||||
category?: string;
|
||||
vehicleType?: string;
|
||||
manager?: string;
|
||||
customer?: string;
|
||||
isColdChain?: string;
|
||||
isTrailer?: string;
|
||||
}): Promise<VehicleListItem[]> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.batch) query.set('batch', params.batch);
|
||||
@@ -33,6 +41,11 @@ export async function fetchVehicleList(params: {
|
||||
if (params.location) query.set('location', params.location);
|
||||
if (params.status) query.set('status', params.status);
|
||||
if (params.category) query.set('category', params.category);
|
||||
if (params.vehicleType) query.set('vehicleType', params.vehicleType);
|
||||
if (params.manager) query.set('manager', params.manager);
|
||||
if (params.customer) query.set('customer', params.customer);
|
||||
if (params.isColdChain) query.set('isColdChain', params.isColdChain);
|
||||
if (params.isTrailer) query.set('isTrailer', params.isTrailer);
|
||||
return fetchJson<VehicleListItem[]>(`${BASE}/list?${query.toString()}`);
|
||||
}
|
||||
|
||||
@@ -44,6 +57,18 @@ export interface WeeklyDetailItem {
|
||||
customer_name: string | null;
|
||||
}
|
||||
|
||||
export async function fetchDeptStats(): Promise<DeptGroup[]> {
|
||||
return fetchJson<DeptGroup[]>(`${BASE}/dept-stats`);
|
||||
}
|
||||
|
||||
export async function fetchRegionStats(): Promise<RegionGroup[]> {
|
||||
return fetchJson<RegionGroup[]>(`${BASE}/region-stats`);
|
||||
}
|
||||
|
||||
export async function fetchCustomerStats(): Promise<CustomerStats[]> {
|
||||
return fetchJson<CustomerStats[]>(`${BASE}/customer-stats`);
|
||||
}
|
||||
|
||||
export async function fetchWeeklyDetail(type: string): Promise<WeeklyDetailItem[]> {
|
||||
return fetchJson<WeeklyDetailItem[]>(`${BASE}/weekly-detail?type=${type}`);
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
45
src/types.ts
45
src/types.ts
@@ -109,3 +109,48 @@ export interface VehicleListItem {
|
||||
customerName: string | null;
|
||||
subjectOrg: string | null;
|
||||
}
|
||||
|
||||
export interface ManagerStats {
|
||||
manager: string;
|
||||
department: string;
|
||||
t4_5: number;
|
||||
t4_5c: number;
|
||||
t18: number;
|
||||
t49: number;
|
||||
trailer: number;
|
||||
other: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface DeptGroup {
|
||||
department: string;
|
||||
totalAssets: number;
|
||||
operatingCount: number;
|
||||
idleCount: number;
|
||||
managers: ManagerStats[];
|
||||
}
|
||||
|
||||
export interface RegionGroup {
|
||||
region: string;
|
||||
totalAssets: number;
|
||||
operatingCount: number;
|
||||
inventoryCount: number;
|
||||
customers: string[];
|
||||
typeBreakdown: { type: string; total: number; operating: number; inventory: number; customers: string[] }[];
|
||||
}
|
||||
|
||||
export interface CustomerStats {
|
||||
customer: string;
|
||||
manager: string;
|
||||
brand: string;
|
||||
department: string;
|
||||
region: string;
|
||||
city: string;
|
||||
t4_5: number;
|
||||
t4_5c: number;
|
||||
t18: number;
|
||||
t49: number;
|
||||
trailer: number;
|
||||
other: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user