mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
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:
@@ -0,0 +1,81 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
|
||||
import { Warehouse } from './warehouse.entity';
|
||||
import { WarehouseYard } from './warehouse-yard.entity';
|
||||
import { WarehouseZone } from './warehouse-zone.entity';
|
||||
|
||||
export const WAREHOUSE_INVENTORY_STATUSES = [
|
||||
'ARRIVED_AT_WAREHOUSE',
|
||||
'UNDER_INSPECTION',
|
||||
'READY_FOR_LOADING',
|
||||
] as const;
|
||||
export type WarehouseInventoryStatus = (typeof WAREHOUSE_INVENTORY_STATUSES)[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'warehouse_inventory' })
|
||||
@Index(['warehouseId'])
|
||||
@Index(['yardId'])
|
||||
@Index(['zoneId'])
|
||||
@Index(['bookingId'])
|
||||
@Index(['cargoId'])
|
||||
@Index(['containerId'])
|
||||
@Index(['goodsId'])
|
||||
@Index(['status'])
|
||||
export class WarehouseInventory extends BaseEntity {
|
||||
@Column({ name: 'warehouse_id', type: 'uuid' })
|
||||
warehouseId!: string;
|
||||
|
||||
@ManyToOne(() => Warehouse)
|
||||
@JoinColumn({ name: 'warehouse_id' })
|
||||
warehouse?: Warehouse;
|
||||
|
||||
@Column({ name: 'yard_id', type: 'uuid' })
|
||||
yardId!: string;
|
||||
|
||||
@ManyToOne(() => WarehouseYard)
|
||||
@JoinColumn({ name: 'yard_id' })
|
||||
yard?: WarehouseYard;
|
||||
|
||||
@Column({ name: 'zone_id', type: 'uuid' })
|
||||
zoneId!: string;
|
||||
|
||||
@ManyToOne(() => WarehouseZone)
|
||||
@JoinColumn({ name: 'zone_id' })
|
||||
zone?: WarehouseZone;
|
||||
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@Column({ name: 'cargo_id', type: 'uuid', nullable: true })
|
||||
cargoId?: string | null;
|
||||
|
||||
@Column({ name: 'container_id', type: 'uuid', nullable: true })
|
||||
containerId?: string | null;
|
||||
|
||||
@Column({ name: 'goods_id', type: 'uuid', nullable: true })
|
||||
goodsId?: string | null;
|
||||
|
||||
@Column({ name: 'quantity', type: 'numeric', precision: 12, scale: 3, default: 0 })
|
||||
quantity!: number;
|
||||
|
||||
@Column({ name: 'weight', type: 'numeric', precision: 14, scale: 3, default: 0 })
|
||||
weight!: number;
|
||||
|
||||
@Column({ name: 'volume', type: 'numeric', precision: 12, scale: 3, nullable: true })
|
||||
volume?: number | null;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', length: 32, default: 'ARRIVED_AT_WAREHOUSE' })
|
||||
status!: WarehouseInventoryStatus;
|
||||
|
||||
@Column({ name: 'arrived_at', type: 'timestamptz', nullable: true })
|
||||
arrivedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'inspected_at', type: 'timestamptz', nullable: true })
|
||||
inspectedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'ready_for_loading_at', type: 'timestamptz', nullable: true })
|
||||
readyForLoadingAt?: Date | null;
|
||||
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
|
||||
import { Warehouse } from './warehouse.entity';
|
||||
import { WarehouseZone } from './warehouse-zone.entity';
|
||||
|
||||
export const WAREHOUSE_YARD_TYPES = [
|
||||
'CONTAINER_YARD',
|
||||
'BULK_YARD',
|
||||
'GENERAL_CARGO_YARD',
|
||||
'HAZARDOUS_YARD',
|
||||
'COLD_STORAGE_YARD',
|
||||
] as const;
|
||||
export type WarehouseYardType = (typeof WAREHOUSE_YARD_TYPES)[number];
|
||||
|
||||
export const WAREHOUSE_YARD_STATUSES = ['ACTIVE', 'INACTIVE'] as const;
|
||||
export type WarehouseYardStatus = (typeof WAREHOUSE_YARD_STATUSES)[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'warehouse_yards' })
|
||||
@Index(['warehouseId'])
|
||||
@Index(['type'])
|
||||
@Index(['status'])
|
||||
export class WarehouseYard extends BaseEntity {
|
||||
@Column({ name: 'warehouse_id', type: 'uuid' })
|
||||
warehouseId!: string;
|
||||
|
||||
@ManyToOne(() => Warehouse, (warehouse) => warehouse.yards, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'warehouse_id' })
|
||||
warehouse?: Warehouse;
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 160 })
|
||||
name!: string;
|
||||
|
||||
@Column({ name: 'code', type: 'varchar', length: 40 })
|
||||
code!: string;
|
||||
|
||||
@Column({ name: 'type', type: 'varchar', length: 32 })
|
||||
type!: WarehouseYardType;
|
||||
|
||||
@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!: WarehouseYardStatus;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@OneToMany(() => WarehouseZone, (zone) => zone.yard)
|
||||
zones?: WarehouseZone[];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
|
||||
import { WarehouseYard } from './warehouse-yard.entity';
|
||||
|
||||
export const WAREHOUSE_ZONE_TYPES = [
|
||||
'CONTAINER_ZONE',
|
||||
'BULK_ZONE',
|
||||
'GENERAL_CARGO_ZONE',
|
||||
'HAZARDOUS_ZONE',
|
||||
'COLD_STORAGE_ZONE',
|
||||
] as const;
|
||||
export type WarehouseZoneType = (typeof WAREHOUSE_ZONE_TYPES)[number];
|
||||
|
||||
export const WAREHOUSE_ZONE_STATUSES = ['ACTIVE', 'INACTIVE'] as const;
|
||||
export type WarehouseZoneStatus = (typeof WAREHOUSE_ZONE_STATUSES)[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'warehouse_zones' })
|
||||
@Index(['yardId'])
|
||||
@Index(['type'])
|
||||
@Index(['status'])
|
||||
export class WarehouseZone extends BaseEntity {
|
||||
@Column({ name: 'yard_id', type: 'uuid' })
|
||||
yardId!: string;
|
||||
|
||||
@ManyToOne(() => WarehouseYard, (yard) => yard.zones, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'yard_id' })
|
||||
yard?: WarehouseYard;
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 160 })
|
||||
name!: string;
|
||||
|
||||
@Column({ name: 'code', type: 'varchar', length: 40 })
|
||||
code!: string;
|
||||
|
||||
@Column({ name: 'type', type: 'varchar', length: 32 })
|
||||
type!: WarehouseZoneType;
|
||||
|
||||
@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!: WarehouseZoneStatus;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
Reference in New Issue
Block a user