From f528b7573708bf3fd5bb9c24bc791eb71dbe03b2 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Wed, 17 Jun 2026 12:44:57 +0000 Subject: [PATCH] Warehouse --- ...50000000002-AddProofOfDeliveryToCargoes.ts | 19 ++ .../src/modules/cargoes/cargoes.service.ts | 4 +- .../modules/cargoes/dto/deliver-cargo.dto.ts | 12 +- .../cargoes/entities/cargoes.entity.ts | 10 + .../warehouses/dto/create-warehouse.dto.ts | 5 + .../warehouses/warehouse-inventory.service.ts | 2 +- .../modules/warehouses/warehouses.service.ts | 5 +- .../src/seed/batch1-4-test-data.seeder.ts | 6 +- .../components/cargoes/DeliverCargoDialog.tsx | 96 +++++++ .../warehouses/CreateWarehouseModal.tsx | 18 ++ .../warehouses/WarehouseCardView.tsx | 17 +- .../warehouses/WarehouseDashboardCharts.tsx | 237 ++++++++++++++++++ .../warehouses/WarehouseInventoryTable.tsx | 2 + .../components/warehouses/WarehouseTable.tsx | 20 ++ .../src/components/warehouses/index.ts | 1 + .../backoffice/src/constants/URLS.ts | 5 + .../backoffice/src/hooks/useCargoes.ts | 5 +- .../backoffice/src/hooks/useFacilities.ts | 16 ++ .../backoffice/src/hooks/useStations.ts | 16 ++ .../src/pages/fleet/FleetCrudPages.tsx | 16 ++ .../src/pages/warehouses/LoadingQueuePage.tsx | 202 +++++++++++++-- .../warehouses/WarehouseDashboardPage.tsx | 66 +++-- .../warehouses/WarehouseInventoryPage.tsx | 7 +- .../backoffice/src/services/cargoService.ts | 12 +- .../src/services/facility.service.ts | 9 + .../backoffice/src/types/warehouse.ts | 33 +++ 26 files changed, 790 insertions(+), 51 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts create mode 100644 apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseDashboardCharts.tsx create mode 100644 apps/edr-freight-web/backoffice/src/hooks/useFacilities.ts create mode 100644 apps/edr-freight-web/backoffice/src/hooks/useStations.ts create mode 100644 apps/edr-freight-web/backoffice/src/services/facility.service.ts diff --git a/apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts b/apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts new file mode 100644 index 000000000..5df498712 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +/** + * Proof of Delivery (customer pickup) capture on cargoes: + * receiver name, delivered/picked-up timestamp, and delivery remarks. + */ +export class AddProofOfDeliveryToCargoes1750000000002 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns('freight.cargoes', [ + new TableColumn({ name: 'receiver_name', type: 'varchar', isNullable: true }), + new TableColumn({ name: 'delivered_at', type: 'timestamp', isNullable: true }), + new TableColumn({ name: 'delivery_remarks', type: 'text', isNullable: true }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumns('freight.cargoes', ['receiver_name', 'delivered_at', 'delivery_remarks']); + } +} diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts index f5940d6a6..c6dac4cb2 100644 --- a/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.service.ts @@ -157,7 +157,9 @@ export class CargoesService { } cargo.status = 'DELIVERED'; - if (dto?.deliveryRemarks) cargo.description = dto.deliveryRemarks; + cargo.deliveredAt = dto?.pickupDate ? new Date(dto.pickupDate) : new Date(); + if (dto?.receiverName) cargo.receiverName = dto.receiverName; + if (dto?.deliveryRemarks) cargo.deliveryRemarks = dto.deliveryRemarks; const remaining = await this.cargoRepo.count({ where: { containerId: cargo.containerId, status: 'LOADED' }, diff --git a/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts b/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts index 020e4d630..de402a33e 100644 --- a/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts +++ b/apps/edr-freight-api/src/modules/cargoes/dto/deliver-cargo.dto.ts @@ -1,6 +1,16 @@ -import { IsOptional, IsString } from 'class-validator'; +import { IsDateString, IsOptional, IsString } from 'class-validator'; export class DeliverCargoDto { + /** Name of the person who received / picked up the cargo (Proof of Delivery). */ + @IsOptional() + @IsString() + receiverName?: string; + + /** When the cargo was picked up / delivered. Defaults to now. */ + @IsOptional() + @IsDateString() + pickupDate?: string; + @IsOptional() @IsString() deliveryRemarks?: string; diff --git a/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts b/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts index 7c2f752e5..051548528 100644 --- a/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts +++ b/apps/edr-freight-api/src/modules/cargoes/entities/cargoes.entity.ts @@ -38,6 +38,16 @@ export class Cargo extends BaseEntity { @Column({ name: 'unloaded_at', type: 'timestamp', nullable: true }) unloadedAt!: Date | null; + // Proof of Delivery (customer pickup) capture. + @Column({ name: 'receiver_name', type: 'varchar', nullable: true }) + receiverName!: string | null; + + @Column({ name: 'delivered_at', type: 'timestamp', nullable: true }) + deliveredAt!: Date | null; + + @Column({ name: 'delivery_remarks', type: 'text', nullable: true }) + deliveryRemarks!: string | null; + // Relationship to Container @ManyToOne(() => Container, (container) => container.cargoes, { onDelete: 'RESTRICT' }) @JoinColumn({ name: 'container_id' }) diff --git a/apps/edr-freight-api/src/modules/warehouses/dto/create-warehouse.dto.ts b/apps/edr-freight-api/src/modules/warehouses/dto/create-warehouse.dto.ts index 3760033b1..788c798bf 100644 --- a/apps/edr-freight-api/src/modules/warehouses/dto/create-warehouse.dto.ts +++ b/apps/edr-freight-api/src/modules/warehouses/dto/create-warehouse.dto.ts @@ -23,6 +23,11 @@ export class CreateWarehouseDto { @IsUUID() stationId?: string; + @ApiPropertyOptional({ format: 'uuid', description: 'Parent facility / port this warehouse belongs to.' }) + @IsOptional() + @IsUUID() + facilityId?: string; + @ApiPropertyOptional() @IsOptional() @IsString() diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index f50089c1f..7fa735642 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -86,7 +86,7 @@ export class WarehouseInventoryService { return this.inventoryRepository.findAll({ where, - relations: { warehouse: true, yard: true, zone: true }, + relations: { warehouse: { facility: true }, yard: true, zone: true, booking: true }, order: { createdAt: 'DESC' }, }); } diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouses.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouses.service.ts index b4904f39d..3cbcc6833 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouses.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouses.service.ts @@ -29,13 +29,14 @@ export class WarehousesService { return this.warehousesRepository.findAll({ where: whereClauses, + relations: { facility: true }, order: { code: 'ASC' }, }); } async findById(id: string): Promise { const warehouse = await this.warehousesRepository.findById(id, { - relations: { yards: { zones: true } }, + relations: { facility: true, yards: { zones: true } }, }); if (!warehouse) { @@ -53,6 +54,7 @@ export class WarehousesService { code: dto.code.trim(), type: dto.type, stationId: dto.stationId ?? null, + facilityId: dto.facilityId ?? null, locationName: dto.locationName?.trim() ?? null, capacityWeight: dto.capacityWeight ?? null, capacityContainers: dto.capacityContainers ?? null, @@ -80,6 +82,7 @@ export class WarehousesService { code: dto.code?.trim() ?? existing.code, type: dto.type ?? existing.type, stationId: dto.stationId ?? existing.stationId, + facilityId: dto.facilityId ?? existing.facilityId, locationName: dto.locationName?.trim() ?? existing.locationName, capacityWeight: dto.capacityWeight ?? existing.capacityWeight, capacityContainers: dto.capacityContainers ?? existing.capacityContainers, diff --git a/apps/edr-freight-api/src/seed/batch1-4-test-data.seeder.ts b/apps/edr-freight-api/src/seed/batch1-4-test-data.seeder.ts index 5405ebc7d..cb4038097 100644 --- a/apps/edr-freight-api/src/seed/batch1-4-test-data.seeder.ts +++ b/apps/edr-freight-api/src/seed/batch1-4-test-data.seeder.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource } from 'typeorm'; -import { Booking } from '../modules/bookings/entities/booking.entity'; import { Facility } from '../modules/facilities/entities/facility.entity'; import { Warehouse } from '../modules/warehouses/entities/warehouse.entity'; import { WarehouseInventory } from '../modules/warehouses/entities/warehouse-inventory.entity'; @@ -148,12 +147,11 @@ export class Batch14TestDataSeeder { warehouseId: warehouse.id, yardId: zone.yardId, zoneId: zone.id, - code: `TEST_INV_${status}_001`, status: status as any, quantity: 100 + i * 10, weight: 500 + i * 50, volume: 100 + i * 10, - arrivedAt: new Date(now.getTime() - i * 3600000), // Staggered arrival times + arrivedAt: new Date(now.getTime() - i * 3600000), storedAt: status !== 'RECEIVED' ? new Date(now.getTime() - (i - 1) * 3600000) : null, reservedAt: ['RESERVED', 'READY_FOR_LOADING', 'LOADED', 'DISPATCHED'].includes(status) ? new Date() : null, readyForLoadingAt: ['READY_FOR_LOADING', 'LOADED', 'DISPATCHED'].includes(status) ? new Date() : null, diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx new file mode 100644 index 000000000..caa554a10 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react'; +import { PackageCheck } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { useDeliverCargo } from '@/hooks/useCargoes'; +import { useToast } from '@/hooks/use-toast'; + +/** + * Customer Pickup + Proof of Delivery capture for a LOADED cargo. + * Records receiver name, pickup date and remarks, then marks the cargo DELIVERED. + */ +export function DeliverCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuccess?: () => void }) { + const [open, setOpen] = useState(false); + const [receiverName, setReceiverName] = useState(''); + const [pickupDate, setPickupDate] = useState(''); + const [deliveryRemarks, setDeliveryRemarks] = useState(''); + const deliver = useDeliverCargo(); + const { toast } = useToast(); + + const handleDeliver = async () => { + if (!receiverName.trim()) { + toast({ title: 'Receiver name is required', variant: 'destructive' }); + return; + } + try { + await deliver.mutateAsync({ + id: cargoId, + payload: { + receiverName: receiverName.trim(), + pickupDate: pickupDate ? new Date(pickupDate).toISOString() : undefined, + deliveryRemarks: deliveryRemarks.trim() || undefined, + }, + }); + toast({ title: 'Delivered', description: 'Proof of delivery recorded; cargo marked delivered.' }); + setOpen(false); + setReceiverName(''); + setPickupDate(''); + setDeliveryRemarks(''); + onSuccess?.(); + } catch (error) { + const message = + (error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? + 'Could not record delivery.'; + toast({ title: 'Delivery failed', description: String(message), variant: 'destructive' }); + } + }; + + return ( + + + + + + + Customer Pickup & Proof of Delivery + +
+
+ + setReceiverName(e.target.value)} + /> +
+
+ + setPickupDate(e.target.value)} + /> +
+
+ +