feat(assets): refine compact inventory drilldowns and exact subtotal scopes
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user