feat(assets): refine compact inventory drilldowns and exact subtotal scopes

This commit is contained in:
shishengliang
2026-09-23 17:52:33 +08:00
parent d20bc71efe
commit b5a48b6417
15 changed files with 534 additions and 111 deletions
+14 -2
View File
@@ -354,8 +354,16 @@ app.get('/customer-stats', async (c) => {
return c.json(result);
});
// GET /api/vehicles/list — flat list with optional filters
app.get('/list', async (c) => {
// POST uses a JSON scope to avoid URL length limits for inventory subtotal drilldown.
app.on(['GET', 'POST'], '/list', async (c) => {
let inventoryKeys: Set<string> | undefined;
if (c.req.method === 'POST') {
const body = await c.req.json().catch(() => null);
if (!body || !Array.isArray(body.inventoryKeys) || !body.inventoryKeys.every((key: unknown) => typeof key === 'string')) {
return c.json({ error: '库存明细范围无效' }, 400);
}
inventoryKeys = new Set(body.inventoryKeys);
}
const vehicles = await getVehiclesForUser(c);
const { brand, batch, model, location, status, category, vehicleType, manager, customer, isColdChain, isTrailer, department, attendance } = c.req.query();
@@ -363,6 +371,10 @@ app.get('/list', async (c) => {
? vehicles.filter(v => isOperatingVehicle(v) || isInventoryPending(v))
: vehicles;
if (inventoryKeys) {
filtered = filtered.filter(v => isInventoryVehicle(v) && inventoryKeys.has(inventoryKey(inventoryDimensions(v))));
}
// attendance filter: active = day_mileage > 0, idle = day_mileage = 0 (only for Operating vehicles)
if (attendance === 'active' || attendance === 'idle') {
const realtimeRows = await vehicleRepository.getAttendanceMileageRows();
@@ -78,3 +78,31 @@ test('inventory composition and color variants reconcile with permission-filtere
assert.ok(list.every((vehicle: {inventoryKey:string}) => vehicle.inventoryKey === JSON.stringify([row.region,row.city,row.brand,row.type,row.model])));
assert.equal(list.filter((vehicle: {color:string}) => vehicle.color === '红').length,1);
});
test('inventory subtotal scopes intersect actual stock, company and exact city/brand/model records', async (t) => {
const make = (id: string, overrides: Partial<Vehicle> = {}): Vehicle => ({
id, plateNumber: `TEST-${id}`, vin: id, type: '18T', model: '飞驰18T白车', color: '白',
location: '嘉兴', region: '嘉兴', province: '浙江省', city: '嘉兴市',
status: 'Inventory', operationStatus: '3', ownership: 'Self', rentCompany: '',
contractNo: null, customerName: null, orgName: null, departmentName: null,
subjectOrg: '公司甲', projectName: null, customerManager: null, managerId: null,
brandLabel: '飞驰', ...overrides,
});
t.mock.method(vehicleRepository, 'getVehicles', async () => [
make('1'), make('2', { model: '飞驰18T红车', status: 'Pending' }),
make('3', { city: '金华市' }), make('4', { brandLabel: '其他品牌' }),
make('5', { operationStatus: '1', status: 'Operating' }), make('6', { subjectOrg: '公司乙' }),
]);
const stats = await (await app.request('/inventory-stats?subject=公司甲')).json();
const keys = stats.filter((row: { city: string; brand: string }) => row.city === '嘉兴市' && row.brand === '飞驰')
.map((row: { region: string; city: string; brand: string; type: string; model: string }) => JSON.stringify([row.region, row.city, row.brand, row.type, row.model]));
const request = (inventoryKeys: unknown) => app.request('/list?source=asset&subject=公司甲&category=Inventory', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ inventoryKeys }),
});
const response = await request(keys);
assert.equal(response.status, 200);
assert.deepEqual((await response.json()).map((row: { id: string }) => row.id), ['1', '2']);
assert.deepEqual(await (await request([])).json(), []);
assert.equal((await request(null)).status, 400);
assert.equal((await request([123])).status, 400);
});
+3
View File
@@ -116,6 +116,9 @@ test('keeps summary and list meanings of other vehicle types distinct', () => {
assert.equal(summaryOther?.filter(trailer), true);
assert.equal(VEHICLE_TYPE_FILTERS['其他'](trailer), false);
assert.equal(VEHICLE_TYPE_FILTERS['其他'](forklift), true);
assert.equal(VEHICLE_TYPE_FILTERS['库存其他'](trailer), true);
assert.equal(VEHICLE_TYPE_FILTERS['库存其他'](forklift), true);
assert.equal(VEHICLE_TYPE_FILTERS['库存其他'](vehicle({ type: '18T', model: '苏龙18T飞翼' })), false);
});
test('keeps location filtering semantics for the ambiguous other region', () => {
+1
View File
@@ -272,6 +272,7 @@ export const VEHICLE_TYPE_FILTERS: Record<string, (vehicle: Vehicle) => boolean>
'49T': (vehicle) => vehicle.type === '49T',
'挂车': (vehicle) => vehicle.type === '挂车' || vehicle.model.includes('挂车'),
'其他': (vehicle) => classifyVehicleType(vehicle) === 'other',
'库存其他': (vehicle) => ['trailer', 'other'].includes(classifyVehicleType(vehicle)),
};
export function filterByLocation(vehicles: Vehicle[], location: string, source?: string): Vehicle[] {
@@ -27,7 +27,9 @@ function vehicle(id: string): Vehicle {
test('keeps all vehicle GET endpoints in their established order', () => {
const source = readFileSync(new URL('../vehicles.ts', import.meta.url), 'utf8');
const paths = Array.from(source.matchAll(/app\.get\('([^']+)'/g), (match) => match[1]);
const paths = Array.from(
source.matchAll(/app\.get\('([^']+)'|app\.on\(\[([^\]]+)\],\s*'([^']+)'/g),
).flatMap((match) => match[1] ? [match[1]] : /'GET'/.test(match[2]) ? [match[3]] : []);
assert.deepEqual(paths, [
'/summary',