feat(warehouse): add warehouse management module (batch 1)

Backend (edr-freight-api):
- Warehouse, WarehouseYard, WarehouseZone, WarehouseInventory entities
- CRUD for warehouses/yards/zones with scoped code uniqueness
- Inventory receive with location-hierarchy + capacity validation and
  transactional capacity-counter updates
- inspect / ready-for-loading status transitions
- inventory inquiry (booking/customer/container/cargo-type joins)
- migration creating freight.warehouse* tables

Frontend (freight backoffice):
- types, service, react-query hooks, URL constants
- reusable components: badges, filters, table/card views, inventory and
  inquiry tables, create/receive modals
- pages: Warehouse list, detail (overview/yards/zones/inventory tabs),
  inventory, inventory inquiry
- Warehouse Information card + Receive At Warehouse on booking detail
- routes + sidebar nav; app-wide ErrorBoundary

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-06-11 15:54:56 +00:00
parent 7facbeda22
commit c86118cd8d
54 changed files with 4257 additions and 1 deletions

View File

@@ -0,0 +1,53 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, OneToMany } from 'typeorm';
import { WarehouseYard } from './warehouse-yard.entity';
export const WAREHOUSE_TYPES = ['OPEN_WAREHOUSE', 'CLOSED_WAREHOUSE'] as const;
export type WarehouseType = (typeof WAREHOUSE_TYPES)[number];
export const WAREHOUSE_STATUSES = ['ACTIVE', 'INACTIVE'] as const;
export type WarehouseStatus = (typeof WAREHOUSE_STATUSES)[number];
@Entity({ schema: 'freight', name: 'warehouses' })
@Index(['code'], { unique: true })
@Index(['type'])
@Index(['status'])
@Index(['stationId'])
export class Warehouse extends BaseEntity {
@Column({ name: 'name', type: 'varchar', length: 160 })
name!: string;
@Column({ name: 'code', type: 'varchar', length: 40, unique: true })
code!: string;
@Column({ name: 'type', type: 'varchar', length: 32 })
type!: WarehouseType;
@Column({ name: 'station_id', type: 'uuid', nullable: true })
stationId?: string | null;
@Column({ name: 'location_name', type: 'varchar', length: 200, nullable: true })
locationName?: string | null;
@Column({ name: 'capacity_weight', type: 'numeric', precision: 14, scale: 3, nullable: true })
capacityWeight?: number | null;
@Column({ name: 'capacity_containers', type: 'int', nullable: true })
capacityContainers?: number | null;
@Column({ name: 'current_weight', type: 'numeric', precision: 14, scale: 3, default: 0 })
currentWeight!: number;
@Column({ name: 'current_containers', type: 'int', default: 0 })
currentContainers!: number;
@Column({ name: 'status', type: 'varchar', length: 16, default: 'ACTIVE' })
status!: WarehouseStatus;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive!: boolean;
@OneToMany(() => WarehouseYard, (yard) => yard.warehouse)
yards?: WarehouseYard[];
}