feat: improve asset inventory statistics and responsive UX (v1.1.16)
This commit is contained in:
@@ -13,6 +13,10 @@ import type { AuthUser } from '../auth/types.js';
|
||||
import type { Context } from 'hono';
|
||||
import {
|
||||
INVENTORY_REGIONS,
|
||||
isOperatingVehicle,
|
||||
isInventoryVehicle,
|
||||
isInventoryAbnormal,
|
||||
isInventoryPending,
|
||||
SUMMARY_TYPE_FILTERS,
|
||||
VEHICLE_TYPE_FILTERS,
|
||||
classifyVehicleType,
|
||||
@@ -63,18 +67,18 @@ app.get('/summary', async (c) => {
|
||||
const summary: SummaryData = {
|
||||
totalAssets: vehicles.length,
|
||||
operating: {
|
||||
total: vehicles.filter((v) => v.status === 'Operating' && (v.operationStatus === '1' || v.operationStatus === '2')).length,
|
||||
self: vehicles.filter((v) => v.status === 'Operating' && v.operationStatus === '2').length,
|
||||
leased: vehicles.filter((v) => v.status === 'Operating' && v.operationStatus === '1').length,
|
||||
public: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Public').length,
|
||||
total: vehicles.filter((v) => isOperatingVehicle(v)).length,
|
||||
self: vehicles.filter((v) => ['2', '自营'].includes(v.operationStatus || '')).length,
|
||||
leased: vehicles.filter((v) => ['1', '租赁'].includes(v.operationStatus || '')).length,
|
||||
public: vehicles.filter((v) => isOperatingVehicle(v) && v.ownership === 'Public').length,
|
||||
hanging: 0,
|
||||
},
|
||||
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,
|
||||
total: vehicles.filter((v) => isInventoryVehicle(v)).length,
|
||||
inStock: vehicles.filter((v) => isInventoryVehicle(v) && v.status === 'Inventory').length,
|
||||
abnormal: vehicles.filter((v) => isInventoryAbnormal(v)).length,
|
||||
},
|
||||
pendingDelivery: vehicles.filter((v) => v.status === 'Pending').length,
|
||||
pendingDelivery: vehicles.filter((v) => isInventoryPending(v)).length,
|
||||
weeklyNew: 0,
|
||||
weeklyRemoved: 0,
|
||||
weeklyDelivered: [...weeklyIds.delivered].filter(id => vehicleIds.has(id)).length,
|
||||
@@ -164,7 +168,7 @@ app.get('/inventory-analysis', async (c) => {
|
||||
|
||||
const modelData = models.map((model) => {
|
||||
const modelVehicles = typeVehicles.filter((v) => v.model === model);
|
||||
const inventoryVehicles = modelVehicles.filter((v) => v.status === 'Inventory');
|
||||
const inventoryVehicles = modelVehicles.filter((v) => isInventoryVehicle(v));
|
||||
|
||||
return {
|
||||
model,
|
||||
@@ -180,7 +184,7 @@ app.get('/inventory-analysis', async (c) => {
|
||||
};
|
||||
});
|
||||
|
||||
const typeInventory = typeVehicles.filter((v) => v.status === 'Inventory');
|
||||
const typeInventory = typeVehicles.filter((v) => isInventoryVehicle(v));
|
||||
|
||||
return {
|
||||
type: t.name,
|
||||
@@ -203,7 +207,7 @@ app.get('/inventory-analysis', async (c) => {
|
||||
// GET /api/vehicles/dept-stats — department & manager breakdown with mileage/attendance
|
||||
app.get('/dept-stats', async (c) => {
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const withManager = vehicles.filter((v) => v.status === 'Operating');
|
||||
const withManager = vehicles.filter((v) => isOperatingVehicle(v));
|
||||
|
||||
// Query realtime day_mileage from tab_truck_remote_sync_realtime_info
|
||||
const realtimeRows = await vehicleRepository.getRealtimeMileageRows();
|
||||
@@ -252,7 +256,7 @@ app.get('/dept-stats', async (c) => {
|
||||
app.get('/region-stats', async (c) => {
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const { customer, city: filterCity, region: filterRegion } = c.req.query();
|
||||
let operating = vehicles.filter((v) => v.status === 'Operating' || v.status === 'Pending');
|
||||
let operating = vehicles.filter((v) => isOperatingVehicle(v) || isInventoryPending(v));
|
||||
if (customer) operating = operating.filter((v) => v.customerName === customer);
|
||||
if (filterCity) operating = operating.filter((v) => resolveCity(v.city, v.province) === filterCity);
|
||||
if (filterRegion) operating = operating.filter((v) => mapMacroRegion(v.province, v.city) === filterRegion);
|
||||
@@ -272,10 +276,10 @@ app.get('/region-stats', async (c) => {
|
||||
const make = (label: string, tv: Vehicle[]) => ({
|
||||
type: label,
|
||||
total: tv.length,
|
||||
operating: tv.filter((v) => v.status === 'Operating').length,
|
||||
inventory: tv.filter((v) => v.status === 'Inventory').length,
|
||||
abnormal: tv.filter((v) => v.status === 'Abnormal').length,
|
||||
pending: tv.filter((v) => v.status === 'Pending').length,
|
||||
operating: tv.filter((v) => isOperatingVehicle(v)).length,
|
||||
inventory: tv.filter((v) => isInventoryVehicle(v)).length,
|
||||
abnormal: tv.filter((v) => isInventoryAbnormal(v)).length,
|
||||
pending: tv.filter((v) => isInventoryPending(v)).length,
|
||||
customers: Array.from(new Set(tv.map((v) => v.customerName).filter(Boolean))) as string[],
|
||||
});
|
||||
const known = KNOWN.map((type) => make(type, vList.filter((v) => v.type === type)));
|
||||
@@ -295,8 +299,8 @@ app.get('/region-stats', async (c) => {
|
||||
.map(([city, cv]) => ({
|
||||
city,
|
||||
totalAssets: cv.length,
|
||||
operatingCount: cv.filter((v) => v.status === 'Operating').length,
|
||||
pendingCount: cv.filter((v) => v.status === 'Pending').length,
|
||||
operatingCount: cv.filter((v) => isOperatingVehicle(v)).length,
|
||||
pendingCount: cv.filter((v) => isInventoryPending(v)).length,
|
||||
customers: Array.from(new Set(cv.map((v) => v.customerName).filter(Boolean))) as string[],
|
||||
typeBreakdown: getTypeBreakdown(cv),
|
||||
}))
|
||||
@@ -324,8 +328,8 @@ app.get('/region-stats', async (c) => {
|
||||
return {
|
||||
region,
|
||||
totalAssets: allVehicles.length,
|
||||
operatingCount: allVehicles.filter((v) => v.status === 'Operating').length,
|
||||
pendingCount: allVehicles.filter((v) => v.status === 'Pending').length,
|
||||
operatingCount: allVehicles.filter((v) => isOperatingVehicle(v)).length,
|
||||
pendingCount: allVehicles.filter((v) => isInventoryPending(v)).length,
|
||||
customers,
|
||||
typeBreakdown: getTypeBreakdown(allVehicles),
|
||||
cities,
|
||||
@@ -338,7 +342,7 @@ app.get('/region-stats', async (c) => {
|
||||
// GET /api/vehicles/customer-stats — per-customer breakdown for operating vehicles
|
||||
app.get('/customer-stats', async (c) => {
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const operating = vehicles.filter((v) => v.status === 'Operating');
|
||||
const operating = vehicles.filter((v) => isOperatingVehicle(v));
|
||||
|
||||
const custMap = new Map<string, Vehicle[]>();
|
||||
for (const v of operating) {
|
||||
@@ -377,7 +381,7 @@ app.get('/list', async (c) => {
|
||||
const realtimeRows = await vehicleRepository.getAttendanceMileageRows();
|
||||
const todayMap = new Map<string, number>();
|
||||
for (const row of realtimeRows as any[]) todayMap.set((row.plate_number || '').trim(), Number(row.day_mileage) || 0);
|
||||
filtered = filtered.filter((v) => v.status === 'Operating');
|
||||
filtered = filtered.filter((v) => isOperatingVehicle(v));
|
||||
if (attendance === 'active') {
|
||||
filtered = filtered.filter((v) => (todayMap.get(v.plateNumber) || 0) > 0);
|
||||
} else {
|
||||
@@ -407,13 +411,13 @@ app.get('/list', async (c) => {
|
||||
}
|
||||
if (category) {
|
||||
if (category === 'Inventory') {
|
||||
filtered = filtered.filter((v) => v.status === 'Inventory');
|
||||
filtered = filtered.filter((v) => isInventoryVehicle(v));
|
||||
} else if (category === 'Abnormal') {
|
||||
filtered = filtered.filter((v) => v.status === 'Abnormal');
|
||||
filtered = filtered.filter((v) => isInventoryAbnormal(v));
|
||||
} else if (category === 'Operating') {
|
||||
filtered = filtered.filter((v) => v.status === 'Operating');
|
||||
filtered = filtered.filter((v) => isOperatingVehicle(v));
|
||||
} else if (category === 'Pending') {
|
||||
filtered = filtered.filter((v) => v.status === 'Pending');
|
||||
filtered = filtered.filter((v) => isInventoryPending(v));
|
||||
}
|
||||
}
|
||||
if (manager) {
|
||||
@@ -453,6 +457,8 @@ app.get('/list', async (c) => {
|
||||
rentCompany: v.rentCompany,
|
||||
contractNo: v.contractNo,
|
||||
customerName: v.customerName,
|
||||
abnormalMove: v.abnormalMove ?? null,
|
||||
pendingDelivery: v.pendingDelivery ?? null,
|
||||
subjectOrg: v.subjectOrg,
|
||||
departmentName: v.departmentName,
|
||||
customerManager: v.customerManager,
|
||||
@@ -465,7 +471,7 @@ app.get('/list', async (c) => {
|
||||
// GET /api/vehicles/inventory-stats — 库存统计,不设数据权限,对所有人开放
|
||||
app.get('/inventory-stats', async (c) => {
|
||||
const vehicles = applySubjectFilter(c, await vehicleRepository.getVehicles());
|
||||
const inventory = vehicles.filter((v) => v.status === 'Inventory');
|
||||
const inventory = vehicles.filter((v) => isInventoryVehicle(v));
|
||||
|
||||
const TYPE_NAME_MAP: Record<string, string> = {
|
||||
t4_5: '4.5T普货',
|
||||
@@ -476,7 +482,7 @@ app.get('/inventory-stats', async (c) => {
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
const groups = new Map<string, number>();
|
||||
const groups = new Map<string, { quantity: number; abnormal: number; pending: number }>();
|
||||
for (const v of inventory) {
|
||||
const typeCategory = classifyVehicleType(v);
|
||||
const typeName = TYPE_NAME_MAP[typeCategory];
|
||||
@@ -485,13 +491,17 @@ app.get('/inventory-stats', async (c) => {
|
||||
const brand = v.brandLabel || '未知';
|
||||
const model = v.model;
|
||||
const key = `${region}|${city}|${brand}|${typeName}|${model}`;
|
||||
groups.set(key, (groups.get(key) || 0) + 1);
|
||||
const counts = groups.get(key) || { quantity: 0, abnormal: 0, pending: 0 };
|
||||
counts.quantity += 1;
|
||||
if (isInventoryAbnormal(v)) counts.abnormal += 1;
|
||||
if (isInventoryPending(v)) counts.pending += 1;
|
||||
groups.set(key, counts);
|
||||
}
|
||||
|
||||
const result = Array.from(groups.entries())
|
||||
.map(([key, quantity]) => {
|
||||
.map(([key, counts]) => {
|
||||
const [region, city, brand, type, model] = key.split('|');
|
||||
return { region, city, brand, type, model, batch: model, quantity };
|
||||
return { region, city, brand, type, model, batch: model, ...counts };
|
||||
})
|
||||
.sort((a, b) => b.quantity - a.quantity);
|
||||
|
||||
@@ -590,8 +600,8 @@ app.get('/subjects', async (c) => {
|
||||
if (!map.has(name)) map.set(name, { total: 0, inventory: 0, operating: 0 });
|
||||
const s = map.get(name)!;
|
||||
s.total += 1;
|
||||
if (v.status === 'Inventory') s.inventory += 1;
|
||||
if (v.status === 'Operating') s.operating += 1;
|
||||
if (isInventoryVehicle(v)) s.inventory += 1;
|
||||
if (isOperatingVehicle(v)) s.operating += 1;
|
||||
}
|
||||
|
||||
const result = Array.from(map.entries())
|
||||
@@ -622,7 +632,7 @@ app.get('/debug', async (c) => {
|
||||
// GET /api/vehicles/region-chart — aggregated chart data with top N + "其他"
|
||||
app.get('/region-chart', async (c) => {
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const operating = vehicles.filter((v) => v.status === 'Operating');
|
||||
const operating = vehicles.filter((v) => isOperatingVehicle(v));
|
||||
const groupBy = c.req.query('groupBy') || 'region'; // 'region' | 'province'
|
||||
const source = c.req.query('source') || 'realtime'; // 'realtime' | 'vehicle'
|
||||
const top = Number(c.req.query('top')) || 8;
|
||||
|
||||
Reference in New Issue
Block a user