feat(warehouses): delete yards/zones and inspect zone contents

Yard and zone soft-delete, refused with 409 while a yard still has
zones or a zone still holds inventory. warehouse_zones:delete was
missing from the catalog — the role presets spread every zone key, so
its absence crashes FreightPositionsSeeder at boot; a migration seeds
it everywhere.

Clicking a zone opens its contents as a datatable. Container identity
comes from booking_container_units for booked cargo and from
containers for backlog registrations; bulk cargo keeps its row with no
container number rather than disappearing from the zone.
This commit is contained in:
Hagernesh
2026-08-28 14:54:17 +00:00
parent 8ef50f9aff
commit d4373dfbe4
14 changed files with 533 additions and 27 deletions

View File

@@ -0,0 +1,55 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Seed `edr_freight_app:warehouse_zones:delete` — the zone counterpart of the
* warehouse and yard delete permissions, which already exist.
*
* `ROLE_PERMISSION_PRESETS` spreads `Object.values(FREIGHT_PERMS.warehouseZones)`
* into the warehouse positions, so the moment the key is added to the registry
* `FreightPositionsSeeder.loadPermissionIds` resolves it against `iam.permissions`
* at boot — and throws `missing_permissions:<key>` if the row is absent. The
* catalog is otherwise written by `EdrOrgSeeder`, which skips itself unless
* `SEED_EDR_ORG` is set, so a migration is the only path that runs everywhere.
*
* Idempotent on `key`; keeps the registry's fixed uuid so every environment
* lands on the same id. Skips silently when the freight application row is
* absent, since there is nothing to attach to.
*/
export class WarehouseZoneDeletePermission3810000000000 implements MigrationInterface {
private static readonly KEY = 'edr_freight_app:warehouse_zones:delete';
private static readonly ID = 'f1c00001-0001-4000-8000-000000000004';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`INSERT INTO iam.permissions (id, key, name, application_id)
SELECT $2::uuid,
$1::varchar,
'{"am": "Delete warehouse zone", "en": "Delete warehouse zone"}'::jsonb,
a.id
FROM iam.application a
WHERE a.key = 'edr_freight_app'
AND NOT EXISTS (SELECT 1 FROM iam.permissions p WHERE p.key = $1::varchar)`,
[WarehouseZoneDeletePermission3810000000000.KEY, WarehouseZoneDeletePermission3810000000000.ID],
);
}
/**
* Grants go first, or the delete trips the position/role permission foreign
* keys — a half-removed permission is worse than one left in place.
*/
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM iam.position_permissions
WHERE permission_id IN (SELECT id FROM iam.permissions WHERE key = $1)`,
[WarehouseZoneDeletePermission3810000000000.KEY],
);
await queryRunner.query(
`DELETE FROM iam.role_permissions
WHERE permission_id IN (SELECT id FROM iam.permissions WHERE key = $1)`,
[WarehouseZoneDeletePermission3810000000000.KEY],
);
await queryRunner.query(`DELETE FROM iam.permissions WHERE key = $1`, [
WarehouseZoneDeletePermission3810000000000.KEY,
]);
}
}