diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts index 3c191c713..3ef7693ab 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts @@ -8,87 +8,98 @@ import { Patch, Post, Query, -} from '@nestjs/common'; -import { CurrentUser } from '@edr/api-common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import type { AuthUserPayload } from '../../common/resolve-auth-user-id'; -import { resolveAuthUserId } from '../../common/resolve-auth-user-id'; +} from "@nestjs/common"; +import { CurrentUser } from "@edr/api-common"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; +import type { AuthUserPayload } from "../../common/resolve-auth-user-id"; +import { resolveAuthUserId } from "../../common/resolve-auth-user-id"; -import { TrainSchedulingManage, TrainSchedulingView } from '../../common/booking-guards'; -import { AssignBookingsDto } from './dto/assign-bookings.dto'; -import { AssignUnassignedBookingDto } from './dto/assign-unassigned-booking.dto'; -import { CreateContainerTrainScheduleDto } from './dto/create-container-train-schedule.dto'; -import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto'; -import { GetEligibleBulkBookingsDto } from './dto/get-eligible-bulk-bookings.dto'; -import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto'; -import { PinWagonsDto } from './dto/pin-wagons.dto'; -import { UpdateContainerItemDto } from './dto/update-container-item.dto'; -import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto'; -import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto'; -import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto'; -import { RecordCheckpointDto } from './dto/record-checkpoint.dto'; -import { AvailableLocomotivesQueryDto } from './dto/available-locomotives-query.dto'; -import { BookableSchedulesQueryDto } from './dto/bookable-schedules-query.dto'; -import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto'; -import { TrainSchedulingService } from './train-scheduling.service'; -import { BookingBatchService } from './booking-batch.service'; +import { + TrainSchedulingManage, + TrainSchedulingView, +} from "../../common/booking-guards"; +import { AssignBookingsDto } from "./dto/assign-bookings.dto"; +import { AssignUnassignedBookingDto } from "./dto/assign-unassigned-booking.dto"; +import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto"; +import { GetEligibleBookingsDto } from "./dto/get-eligible-bookings.dto"; +import { GetEligibleBulkBookingsDto } from "./dto/get-eligible-bulk-bookings.dto"; +import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto"; +import { PinWagonsDto } from "./dto/pin-wagons.dto"; +import { UpdateContainerItemDto } from "./dto/update-container-item.dto"; +import { PreviewBulkTrainScheduleDto } from "./dto/preview-bulk-train-schedule.dto"; +import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto"; +import { PreviewTrainScheduleDto } from "./dto/preview-train-schedule.dto"; +import { RecordCheckpointDto } from "./dto/record-checkpoint.dto"; +import { AvailableLocomotivesQueryDto } from "./dto/available-locomotives-query.dto"; +import { BookableSchedulesQueryDto } from "./dto/bookable-schedules-query.dto"; +import { UpdateTrainSchedulingGlobalRulesDto } from "./dto/update-train-scheduling-global-rules.dto"; +import { TrainSchedulingService } from "./train-scheduling.service"; +import { BookingBatchService } from "./booking-batch.service"; -@ApiTags('train-scheduling') +@ApiTags("train-scheduling") @ApiBearerAuth() -@Controller('train-scheduling') +@Controller("train-scheduling") export class TrainSchedulingController { constructor( private readonly trainSchedulingService: TrainSchedulingService, private readonly bookingBatchService: BookingBatchService, - ) {} + ) { } - @Get('global-rules') + @Get("global-rules") @TrainSchedulingView() - @ApiOperation({ summary: 'Get global train scheduling rules (singleton)' }) + @ApiOperation({ summary: "Get global train scheduling rules (singleton)" }) getGlobalRules() { return this.trainSchedulingService.getTrainSchedulingGlobalRules(); } - @Patch('global-rules') + @Patch("global-rules") @TrainSchedulingManage() - @ApiOperation({ summary: 'Update global train scheduling rules (singleton)' }) + @ApiOperation({ summary: "Update global train scheduling rules (singleton)" }) updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) { return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto); } - @Get('eligible-bookings') + @Get("eligible-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'List eligible bookings (container and/or bulk)' }) + @ApiOperation({ summary: "List eligible bookings (container and/or bulk)" }) getEligibleBookings(@Query() query: GetEligibleBookingsDto) { return this.trainSchedulingService.getEligibleBookings(query); } - @Get('batch-board') + @Get("batch-board") @TrainSchedulingView() - @ApiOperation({ summary: 'Batch monitoring board: schedules with bookings grouped by state' }) + @ApiOperation({ + summary: "Batch monitoring board: schedules with bookings grouped by state", + }) getBatchBoard() { return this.bookingBatchService.getBatchBoard(); } - @Get('batch-board/:scheduleId') + @Get("batch-board/:scheduleId") @TrainSchedulingView() - @ApiOperation({ summary: 'Batch board detail for one schedule with EAT 3h windows' }) - getBatchBoardDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) { + @ApiOperation({ + summary: "Batch board detail for one schedule with EAT 3h windows", + }) + getBatchBoardDetail(@Param("scheduleId", ParseUUIDPipe) scheduleId: string) { return this.bookingBatchService.getBatchBoardDetail(scheduleId); } - @Get('available-locomotives') + @Get("available-locomotives") @TrainSchedulingView() @ApiOperation({ - summary: 'List AVAILABLE locomotives at the route origin yard', + summary: "List AVAILABLE locomotives at the route origin yard", }) getAvailableLocomotives(@Query() query: AvailableLocomotivesQueryDto) { - return this.trainSchedulingService.getAvailableLocomotivesForRoute(query.routeId); + return this.trainSchedulingService.getAvailableLocomotivesForRoute( + query.routeId, + ); } - @Get('bookable-schedules') - @TrainSchedulingView() - @ApiOperation({ summary: 'OPEN same-route schedules a new booking can target' }) + @Get("bookable-schedules") + // @TrainSchedulingView() + @ApiOperation({ + summary: "OPEN same-route schedules a new booking can target", + }) getBookableSchedules(@Query() query: BookableSchedulesQueryDto) { return this.trainSchedulingService.getBookableSchedules( query.originYardId, @@ -96,282 +107,323 @@ export class TrainSchedulingController { ); } - @Get('container/eligible-bookings') + @Get("container/eligible-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'List eligible container bookings' }) - getEligibleContainerBookings(@Query() query: GetEligibleContainerBookingsDto) { + @ApiOperation({ summary: "List eligible container bookings" }) + getEligibleContainerBookings( + @Query() query: GetEligibleContainerBookingsDto, + ) { return this.trainSchedulingService.getEligibleContainerBookings(query); } - @Get('bulk/eligible-bookings') + @Get("bulk/eligible-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'List eligible bulk bookings' }) + @ApiOperation({ summary: "List eligible bulk bookings" }) getEligibleBulkBookings(@Query() query: GetEligibleBulkBookingsDto) { return this.trainSchedulingService.getEligibleBulkBookings(query); } - @Post('preview') + @Post("preview") @TrainSchedulingView() - @ApiOperation({ summary: 'Preview a mixed-capable train schedule' }) + @ApiOperation({ summary: "Preview a mixed-capable train schedule" }) previewTrainSchedule(@Body() dto: PreviewTrainScheduleDto) { return this.trainSchedulingService.previewTrainSchedule(dto); } - @Post('container/preview') + @Post("container/preview") @TrainSchedulingView() - @ApiOperation({ summary: 'Preview a container train schedule' }) + @ApiOperation({ summary: "Preview a container train schedule" }) previewContainerTrainSchedule(@Body() dto: PreviewContainerTrainScheduleDto) { return this.trainSchedulingService.previewContainerTrainSchedule(dto); } - @Post('bulk/preview') + @Post("bulk/preview") @TrainSchedulingView() - @ApiOperation({ summary: 'Preview a bulk train schedule' }) + @ApiOperation({ summary: "Preview a bulk train schedule" }) previewBulkTrainSchedule(@Body() dto: PreviewBulkTrainScheduleDto) { return this.trainSchedulingService.previewBulkTrainSchedule(dto); } - @Post('container/schedules') + @Post("container/schedules") @TrainSchedulingManage() - @ApiOperation({ summary: 'Create a container train schedule' }) + @ApiOperation({ summary: "Create a container train schedule" }) createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } - @Post('bulk/schedules') + @Post("bulk/schedules") @TrainSchedulingManage() - @ApiOperation({ summary: 'Create a bulk train schedule' }) + @ApiOperation({ summary: "Create a bulk train schedule" }) createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } - @Post('schedules/:id/assign-bookings') + @Post("schedules/:id/assign-bookings") @TrainSchedulingManage() - @ApiOperation({ summary: 'Assign bookings to a train schedule (mixed-capable)' }) + @ApiOperation({ + summary: "Assign bookings to a train schedule (mixed-capable)", + }) assignBookings( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule(id, dto); } - @Post('container/schedules/:id/assign-bookings') + @Post("container/schedules/:id/assign-bookings") @TrainSchedulingManage() - @ApiOperation({ summary: 'Assign container bookings to a train schedule' }) + @ApiOperation({ summary: "Assign container bookings to a train schedule" }) assignContainerBookings( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { - return this.trainSchedulingService.assignBookingsToSchedule(id, dto, 'CONTAINER'); + return this.trainSchedulingService.assignBookingsToSchedule( + id, + dto, + "CONTAINER", + ); } - @Post('bulk/schedules/:id/assign-bookings') + @Post("bulk/schedules/:id/assign-bookings") @TrainSchedulingManage() - @ApiOperation({ summary: 'Assign bulk bookings to a train schedule' }) + @ApiOperation({ summary: "Assign bulk bookings to a train schedule" }) assignBulkBookings( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { - return this.trainSchedulingService.assignBookingsToSchedule(id, dto, 'BULK'); + return this.trainSchedulingService.assignBookingsToSchedule( + id, + dto, + "BULK", + ); } - @Delete('schedules/:id/bookings/:bookingId') + @Delete("schedules/:id/bookings/:bookingId") @TrainSchedulingManage() - @ApiOperation({ summary: 'Unassign a booking from a train schedule' }) + @ApiOperation({ summary: "Unassign a booking from a train schedule" }) unassignBooking( - @Param('id', ParseUUIDPipe) id: string, - @Param('bookingId', ParseUUIDPipe) bookingId: string, + @Param("id", ParseUUIDPipe) id: string, + @Param("bookingId", ParseUUIDPipe) bookingId: string, @CurrentUser() user: AuthUserPayload, ) { - return this.trainSchedulingService.unassignBooking(id, bookingId, resolveAuthUserId(user)); + return this.trainSchedulingService.unassignBooking( + id, + bookingId, + resolveAuthUserId(user), + ); } - @Delete('schedules/:id/wagons/:trainSetWagonId') + @Delete("schedules/:id/wagons/:trainSetWagonId") @TrainSchedulingManage() - @ApiOperation({ summary: 'Remove an empty wagon slot from a train' }) + @ApiOperation({ summary: "Remove an empty wagon slot from a train" }) removeWagonSlot( - @Param('id', ParseUUIDPipe) id: string, - @Param('trainSetWagonId', ParseUUIDPipe) trainSetWagonId: string, + @Param("id", ParseUUIDPipe) id: string, + @Param("trainSetWagonId", ParseUUIDPipe) trainSetWagonId: string, ) { - return this.trainSchedulingService.removeTrainSetWagonSlot(id, trainSetWagonId); + return this.trainSchedulingService.removeTrainSetWagonSlot( + id, + trainSetWagonId, + ); } - @Patch('schedules/:id/container-items/:itemId') + @Patch("schedules/:id/container-items/:itemId") @TrainSchedulingManage() - @ApiOperation({ summary: 'Update a container number on a wagon slot' }) + @ApiOperation({ summary: "Update a container number on a wagon slot" }) updateContainerItem( - @Param('id', ParseUUIDPipe) id: string, - @Param('itemId', ParseUUIDPipe) itemId: string, + @Param("id", ParseUUIDPipe) id: string, + @Param("itemId", ParseUUIDPipe) itemId: string, @Body() dto: UpdateContainerItemDto, ) { return this.trainSchedulingService.updateContainerItem(id, itemId, dto); } - @Get('schedules/:id/unassigned-bookings') + @Get("schedules/:id/unassigned-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'Get unassigned bookings for a schedule' }) - getUnassignedBookings(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get unassigned bookings for a schedule" }) + getUnassignedBookings(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getUnassignedBookings(id); } - @Post('schedules/:id/assign-unassigned-booking') + @Post("schedules/:id/assign-unassigned-booking") @TrainSchedulingManage() @ApiOperation({ - summary: 'Assign one linked unallocated booking to wagons (preserves existing assignments)', + summary: + "Assign one linked unallocated booking to wagons (preserves existing assignments)", }) assignUnassignedBooking( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignUnassignedBookingDto, ) { - return this.trainSchedulingService.assignUnassignedBookingToWagons(id, dto.bookingId); + return this.trainSchedulingService.assignUnassignedBookingToWagons( + id, + dto.bookingId, + ); } - @Get('schedules/:id/composition-removals') + @Get("schedules/:id/composition-removals") @TrainSchedulingView() - @ApiOperation({ summary: 'Get removal log for a schedule' }) - getCompositionRemovals(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get removal log for a schedule" }) + getCompositionRemovals(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getCompositionRemovals(id); } - @Post('schedules/:id/pin-wagons') + @Post("schedules/:id/pin-wagons") @TrainSchedulingManage() - @ApiOperation({ summary: 'Pin physical wagons to train set slots' }) - pinWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) { + @ApiOperation({ summary: "Pin physical wagons to train set slots" }) + pinWagons(@Param("id", ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) { return this.trainSchedulingService.pinWagons(id, dto); } - @Post('schedules/:id/finalize') + @Post("schedules/:id/finalize") @TrainSchedulingManage() - @ApiOperation({ summary: 'Finalize a draft train schedule' }) - finalizeSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Finalize a draft train schedule" }) + finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.finalizeSchedule(id); } - @Post('schedules/:id/dispatch') + @Post("schedules/:id/dispatch") @TrainSchedulingManage() - @ApiOperation({ summary: 'Dispatch a scheduled train' }) - dispatchSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Dispatch a scheduled train" }) + dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.dispatchSchedule(id); } // ---- batch / booking-window staff actions ---- - @Post('schedules/:id/run-batch') + @Post("schedules/:id/run-batch") @TrainSchedulingManage() - @ApiOperation({ summary: 'Manually run the batch fill for a schedule' }) - async runBatch(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Manually run the batch fill for a schedule" }) + async runBatch(@Param("id", ParseUUIDPipe) id: string) { await this.bookingBatchService.fillSchedule(id); return this.bookingBatchService.getBatchBoardDetail(id); } - @Post('schedules/:id/run-allocation') + @Post("schedules/:id/run-allocation") @TrainSchedulingManage() - @ApiOperation({ summary: 'Run wagon-level allocation for all eligible linked bookings' }) - async runAllocation(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ + summary: "Run wagon-level allocation for all eligible linked bookings", + }) + async runAllocation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingBatchService.runWagonAllocation(id); } - @Patch('schedules/:id/booking-window') + @Patch("schedules/:id/booking-window") @TrainSchedulingManage() - @ApiOperation({ summary: 'Open or close a schedule booking window' }) + @ApiOperation({ summary: "Open or close a schedule booking window" }) async setBookingWindow( - @Param('id', ParseUUIDPipe) id: string, - @Body('status') status: 'OPEN' | 'CLOSED', + @Param("id", ParseUUIDPipe) id: string, + @Body("status") status: "OPEN" | "CLOSED", ) { - await this.trainSchedulingService.setBookingWindow(id, status === 'CLOSED' ? 'CLOSED' : 'OPEN'); + await this.trainSchedulingService.setBookingWindow( + id, + status === "CLOSED" ? "CLOSED" : "OPEN", + ); return this.trainSchedulingService.getContainerTrainScheduleById(id); } - @Post('bookings/:bookingId/mark-paid') + @Post("bookings/:bookingId/mark-paid") @TrainSchedulingManage() - @ApiOperation({ summary: 'Staff: mark a reserved booking paid and allocate it now' }) - async markBookingPaid(@Param('bookingId', ParseUUIDPipe) bookingId: string) { + @ApiOperation({ + summary: "Staff: mark a reserved booking paid and allocate it now", + }) + async markBookingPaid(@Param("bookingId", ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.markPaid(bookingId); return { ok: true }; } - @Post('bookings/:bookingId/expire') + @Post("bookings/:bookingId/expire") @TrainSchedulingManage() - @ApiOperation({ summary: 'Staff: expire a reservation and free its capacity' }) - async expireBooking(@Param('bookingId', ParseUUIDPipe) bookingId: string) { + @ApiOperation({ + summary: "Staff: expire a reservation and free its capacity", + }) + async expireBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.expireReservation(bookingId); return { ok: true }; } - @Post('bookings/:bookingId/move-schedule') + @Post("bookings/:bookingId/move-schedule") @TrainSchedulingManage() - @ApiOperation({ summary: 'Re-point a booking to another OPEN same-route schedule' }) + @ApiOperation({ + summary: "Re-point a booking to another OPEN same-route schedule", + }) async moveBookingSchedule( - @Param('bookingId', ParseUUIDPipe) bookingId: string, - @Body('trainScheduleId', ParseUUIDPipe) trainScheduleId: string, + @Param("bookingId", ParseUUIDPipe) bookingId: string, + @Body("trainScheduleId", ParseUUIDPipe) trainScheduleId: string, ) { await this.bookingBatchService.moveToSchedule(bookingId, trainScheduleId); return { ok: true }; } - @Get('schedules/:id/checkpoints') + @Get("schedules/:id/checkpoints") @TrainSchedulingView() - @ApiOperation({ summary: 'Get the tracking corridor + logged checkpoints for a train' }) - getScheduleCheckpoints(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ + summary: "Get the tracking corridor + logged checkpoints for a train", + }) + getScheduleCheckpoints(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getScheduleCheckpoints(id); } - @Post('schedules/:id/checkpoints') + @Post("schedules/:id/checkpoints") @TrainSchedulingManage() - @ApiOperation({ summary: 'Log the train passing a station (final station triggers arrival)' }) + @ApiOperation({ + summary: "Log the train passing a station (final station triggers arrival)", + }) recordCheckpoint( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: RecordCheckpointDto, ) { return this.trainSchedulingService.recordCheckpoint(id, dto); } - @Post('schedules/:id/arrive') + @Post("schedules/:id/arrive") @TrainSchedulingManage() - @ApiOperation({ summary: 'Mark a dispatched train arrived (move assets to destination yard, free assets)' }) - arriveSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ + summary: + "Mark a dispatched train arrived (move assets to destination yard, free assets)", + }) + arriveSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.arriveSchedule(id); } - @Get('container/schedules') + @Get("container/schedules") @TrainSchedulingView() - @ApiOperation({ summary: 'List container train schedules' }) + @ApiOperation({ summary: "List container train schedules" }) getContainerTrainSchedules() { return this.trainSchedulingService.getContainerTrainSchedules(); } - @Get('bulk/schedules') + @Get("bulk/schedules") @TrainSchedulingView() - @ApiOperation({ summary: 'List bulk train schedules' }) + @ApiOperation({ summary: "List bulk train schedules" }) getBulkTrainSchedules() { return this.trainSchedulingService.getContainerTrainSchedules(); } - @Get('container/schedules/:id') + @Get("container/schedules/:id") @TrainSchedulingView() - @ApiOperation({ summary: 'Get container train schedule detail' }) - getContainerTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get container train schedule detail" }) + getContainerTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } - @Get('bulk/schedules/:id') + @Get("bulk/schedules/:id") @TrainSchedulingView() - @ApiOperation({ summary: 'Get bulk train schedule detail' }) - getBulkTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get bulk train schedule detail" }) + getBulkTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } - @Post('container/schedules/:id/cancel') + @Post("container/schedules/:id/cancel") @TrainSchedulingManage() - @ApiOperation({ summary: 'Cancel container train schedule' }) - cancelTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Cancel container train schedule" }) + cancelTrainSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } - @Post('bulk/schedules/:id/cancel') + @Post("bulk/schedules/:id/cancel") @TrainSchedulingManage() - @ApiOperation({ summary: 'Cancel bulk train schedule' }) - cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Cancel bulk train schedule" }) + cancelBulkTrainSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } } diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index ae79d7562..f5fddd009 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -38,6 +38,7 @@ "devDependencies": { "@edr/eslint-config": "workspace:*", "@edr/tsconfig": "workspace:*", + "@hookform/devtools": "^4.4.0", "@tailwindcss/vite": "^4.3.0", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index 2ae53d576..7420f90d3 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -7,6 +7,7 @@ import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; +import { DevTool } from "@hookform/devtools"; import { BookingFormInputValues, STEPS, @@ -22,6 +23,7 @@ import { Step2ServiceType, Step4Route, Step5CargoDetails, + StepScheduling, StepDocuments, Step8Review, } from "./new-booking-form/steps"; @@ -70,7 +72,11 @@ export default function NewBookingPage() { const destinationYard = form.watch("destinationYard"); const direction = useMemo( - () => getRouteDirection(referenceData?.yard.find((y) => y.id === originYard), referenceData?.yard.find((y) => y.id === destinationYard)), + () => + getRouteDirection( + referenceData?.yard.find((y) => y.id === originYard), + referenceData?.yard.find((y) => y.id === destinationYard), + ), [originYard, destinationYard], ); @@ -100,15 +106,11 @@ export default function NewBookingPage() { : Number(data.cargoWeight || 0); // ── Reference data lookups ────────────────────────────────────────── - const yards = referenceData?.yard ?? []; const services = referenceData?.service ?? []; const shippingLines = referenceData?.shipping_line ?? []; const cargoTree = referenceData?.cargo_type ?? []; const containerGroups = referenceData?.containers ?? []; - const findYardId = (name: string): string => - yards.find((y) => y.name === name)?.id ?? ""; - const findServiceTypeId = (): string => { const code = data.serviceType === "rail" ? "RAIL" : "RAIL_AND_FORWARDING"; return services.find((s) => s.code === code)?.id ?? services[0]?.id ?? ""; @@ -117,13 +119,6 @@ export default function NewBookingPage() { const findShippingLineId = (name: string): string | undefined => shippingLines.find((l) => l.name === name)?.id; - const findContainerCargoTypeId = (): string => { - const group = cargoTree.find( - (g) => g.code === "CONTAINER" || /container/i.test(g.name), - ); - return group?.id ?? ""; - }; - const findContainerTypeId = (name: string): string => { for (const group of containerGroups) { const ct = group.types.find((t) => t.name === name); @@ -139,21 +134,18 @@ export default function NewBookingPage() { ?.children?.find((c) => c.name === data.bulkCommoditytype) : undefined; - const cargoTypeId = - data.cargoType === "container" - ? findContainerCargoTypeId() - : (selectedChild?.id ?? ""); + const cargoTypeId = selectedChild?.id; const cargoFreeText = data.cargoType === "container" ? undefined : selectedChild?.show_free_text_box - ? data.bulkCommoditytype + ? data.cargoFreeText : undefined; // ── Build API payload ─────────────────────────────────────────────── const apiPayload: CreateBookingPayload = { - scheduledDate: new Date().toISOString().slice(0, 10), + scheduledDate: new Date().toISOString(), contractType: data.contractType.toUpperCase() as CreateBookingPayload["contractType"], serviceTypeId: findServiceTypeId(), @@ -161,13 +153,13 @@ export default function NewBookingPage() { data.equipmentReturn === "with_return" ? "WITH_RETURN" : "WITHOUT_RETURN", - originYardId: findYardId(data.originYard), - destinationYardId: findYardId(data.destinationYard), + originYardId: data.originYard, + destinationYardId: data.destinationYard, tradeDirection: direction!, - cargoTypeId: data.cargoType === "container" ? undefined : cargoTypeId, + cargoTypeId, + trainScheduleId: data.trainScheduleId, cargoTotalWeightVgm: totalWeight, isHazardous: data.isHazardous, - paymentCurrency: "USD", allowConsolidation: data.consolidationEnabled, // @ts-ignore freightType: @@ -243,67 +235,59 @@ export default function NewBookingPage() { Back to Bookings -
- {/* Step indicator */} - - + + - - {/* Step content */} - - - {createMutation.isError && ( - } - radius="md" - mb="lg" - > - - Failed to save draft - - - {createMutation.error instanceof Error - ? createMutation.error.message - : "An unexpected error occurred. Please try again."} - - - )} + {createMutation.isError && ( + } + radius="md" + mb="lg" + > + + Failed to save draft + + + {createMutation.error instanceof Error + ? createMutation.error.message + : "An unexpected error occurred. Please try again."} + + + )} - {step === 1 && } - {step === 2 && } - {step === 3 && ( - - )} - {step === 4 && ( - - )} - {step === 5 && } - {step === 6 && ( - - )} - + {step === 1 && } + {step === 2 && } + {step === 3 && ( + + )} + {step === 4 && ( + + )} + {step === 5 && ( + + )} + {step === 6 && } + {step === 7 && ( + + )} {/* Navigation footer */} @@ -359,6 +343,7 @@ export default function NewBookingPage() { + {/* */}
); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts index ea8611d9e..1e89b7be4 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts @@ -101,10 +101,12 @@ export const bookingFormSchema = z destinationYard: z.string().min(1, "Select a destination yard."), shippingLine: z.string(), scheduledDate: z.string().min(1, "Select a shipment date."), + trainScheduleId: z.string().min(1, "Select a shipment date."), cargoType: z.enum(["container", "bulk"], "Select a cargo type."), cargoWeight: z.string(), freightType: z.string(), // parent group bulkCommoditytype: z.string(), + cargoFreeText: z.string(), isHazardous: z.boolean(), isRefrigerated: z.boolean(), containers: z.array( @@ -229,8 +231,10 @@ export const initialBookingFormValues: DeepPartial = { destinationYard: "", shippingLine: "", scheduledDate: "", + trainScheduleId: "", cargoWeight: "", bulkCommoditytype: "", + cargoFreeText: "", isHazardous: false, isRefrigerated: false, containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }], @@ -264,7 +268,7 @@ export const stepFields: Record>> = { "containers", "consolidationEnabled", ], - 5: ["scheduledDate"], + 5: ["scheduledDate", "trainScheduleId"], 6: ["documents"], 7: ["notes", "termsAccepted"], }; @@ -280,50 +284,32 @@ export interface WagonConfig { type: "20ft" | "40ft"; } -export interface WagonCalcResult { - totalWagons: number; - hasOddUnit: boolean; - sharedWagons: number; - wagonLayout: WagonConfig[]; - ft40Wagons: number; - ft20Wagons: number; -} - export function getRouteDirection( origin: Freight.BookingReferenceYard | null | undefined, dest: Freight.BookingReferenceYard | null | undefined, -): Freight.ScheduleTradeDirection | null { +): Freight.ScheduleTradeDirection | null { if (!origin || !dest) return null; -if(origin.country === 'ethiopia' && dest.country === 'ethiopia') { - return 'DOMESTIC'; -} - if(origin.country === 'ethiopia' && dest.country === 'djibouti') { - return 'IMPORT'; + if (origin.country === "ethiopia" && dest.country === "ethiopia") { + return "DOMESTIC"; } - if(origin.country === 'djibouti' && dest.country === 'ethiopia') { - return 'EXPORT'; + if (origin.country === "ethiopia" && dest.country === "djibouti") { + return "IMPORT"; + } + if (origin.country === "djibouti" && dest.country === "ethiopia") { + return "EXPORT"; } return null; } -export function calcWagons(containers: ContainerConfig[]): WagonCalcResult { - const Ft40Wagons = containers - .filter((c) => c.type === "40ft") - .reduce((sum, c) => sum + Number(c.qty), 0); +export function calcWagons(containers: ContainerConfig[]) { const Ft20Wagons = containers .filter((c) => c.type === "20ft") .reduce((sum, c) => sum + Number(c.qty), 0); - const wagonLayout: WagonConfig[] = []; let hasOddUnit = Ft20Wagons % 2 === 1; - let sharedWagons = Math.floor(Ft20Wagons / 2); return { - totalWagons: sharedWagons + Ft40Wagons, hasOddUnit, - sharedWagons, - ft40Wagons: Ft40Wagons, ft20Wagons: Ft20Wagons, - wagonLayout, }; } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx index cd7db7b85..f600d10d7 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx @@ -5,12 +5,17 @@ import { Controller, type UseFormReturn } from "react-hook-form"; import { BOOKING_DOCS_SETTING, + BookingFormInputValues, type BookingDocuments, type BookingFormValues, } from "./schema"; import { StepHeader } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; function countAttached(documents: BookingDocuments): number { return BOOKING_DOCS_SETTING.fields.filter((f) => { @@ -57,7 +62,11 @@ export function StepDocuments({ form }: { form: BookingForm }) { color: attached === total ? "#0A6F4D" : "#2E5B96", }} > - {attached === total ? : `${attached}/${total}`} + {attached === total ? ( + + ) : ( + `${attached}/${total}` + )} {attached === 0 diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx new file mode 100644 index 000000000..c403835c5 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx @@ -0,0 +1,389 @@ +import { + Box, + Button, + Card, + Grid, + Group, + Stack, + Text, + Title, + useMantineTheme, + Divider, +} from "@mantine/core"; +import { UseFormReturn } from "react-hook-form"; +import { BookingFormInputValues, BookingFormValues } from "./schema"; +import { + ChevronLeft, + ChevronRight, + Info, + Calendar as CalendarIcon, +} from "lucide-react"; +import type { Freight } from "@edr/types"; +import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; +import { + format, + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + eachDayOfInterval, + isToday, + isSameMonth, + addMonths, +} from "date-fns"; + +interface StepSchedulingProps { + form: UseFormReturn; + referenceData?: Freight.BookingReferenceData; +} + +export function StepScheduling({ form, referenceData }: StepSchedulingProps) { + const theme = useMantineTheme(); + const [currentDate, setCurrentDate] = useState(new Date()); + const selectedDate = form.watch("scheduledDate"); + const originYardId = form.watch("originYard"); + const destinationYardId = form.watch("destinationYard"); + const cargoType = form.watch("cargoType"); + + const originName = useMemo( + () => + referenceData?.yard.find((y) => y.id === originYardId)?.name ?? + "Not selected", + [referenceData, originYardId], + ); + + const destinationName = useMemo( + () => + referenceData?.yard.find((y) => y.id === destinationYardId)?.name ?? + "Not selected", + [referenceData, destinationYardId], + ); + + const { data: bookableSchedules } = useQuery( + api.bookings.getBookableSchedules.queryOptions({ + input: { originYardId, destinationYardId }, + enabled: !!originYardId && !!destinationYardId, + }), + ); + + const scheduleMap = useMemo(() => { + const map = new Map(); + if (bookableSchedules) { + for (const s of bookableSchedules) { + if (!map.has(s.scheduleDate)) { + map.set(s.scheduleDate, s); + } + } + } + return map; + }, [bookableSchedules]); + + const days = useMemo(() => { + const monthStart = startOfMonth(currentDate); + const monthEnd = endOfMonth(currentDate); + const calStart = startOfWeek(monthStart, { weekStartsOn: 1 }); + const calEnd = endOfWeek(monthEnd, { weekStartsOn: 1 }); + + return eachDayOfInterval({ start: calStart, end: calEnd }).map((date) => { + const dateString = format(date, "yyyy-MM-dd"); + const schedule = scheduleMap.get(dateString); + + return { + day: date.getDate(), + dateString, + isToday: isToday(date), + isSelected: selectedDate === dateString, + isCurrentMonth: isSameMonth(date, currentDate), + isFull: schedule ? schedule.remainingWagons <= 0 : false, + hasSchedule: !!schedule, + remainingWagons: schedule?.remainingWagons ?? 0, + scheduleId: schedule?.id ?? "", + }; + }); + }, [currentDate, scheduleMap, selectedDate]); + + const legendItems = [ + { label: "Available", color: theme.colors.gray[1] }, + { label: "Full", color: theme.colors["edr-red-soft"][0] }, + { label: "No Service", color: "transparent" }, + { label: "Selected", color: theme.colors["edr-green"][5] }, + ]; + + return ( + + + + + + + Shipment Date + + + Pick a shipment date for your booking from the available + schedule. + + + + + + + + {format(currentDate, "MMMM yyyy")} + + + + + + + + + + {["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"].map((d) => ( + + {d} + + ))} + {days.map((d, i) => { + const canSelect = + d.isCurrentMonth && d.hasSchedule && !d.isFull; + return ( + + ); + })} + + + + {legendItems.map((item) => ( + + + + {item.label} + + + ))} + + + + + + + + + + Booking Summary + + + + + + + + + + + + + Shipment Date + + + {selectedDate || "Not selected"} + + + + + + + + + + + + + Final confirmation of your selected date will be provided + after review of your booking details. + + + + + + + + + ); +} + +function SummaryRow({ label, value }: { label: string; value: string }) { + return ( + + + {label} + + + {value} + + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx index 7a7a0d888..574a51a01 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx @@ -1,7 +1,14 @@ import { useMemo } from "react"; import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form"; import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react"; -import { ActionIcon, Button, Skeleton, Stack, Text, TextInput } from "@mantine/core"; +import { + ActionIcon, + Button, + Skeleton, + Stack, + Text, + TextInput, +} from "@mantine/core"; import type { Freight } from "@edr/types"; import { BookingFormInputValues, @@ -17,7 +24,11 @@ import { StepLabel, } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; export function Step5CargoDetails({ form, @@ -32,6 +43,7 @@ export function Step5CargoDetails({ }) { const cargoType = form.watch("cargoType"); const freightType = form.watch("freightType"); + const bulkCommoditytype = form.watch("bulkCommoditytype"); const containers = form.watch("containers"); const { fields, append, remove } = useFieldArray({ @@ -39,16 +51,28 @@ export function Step5CargoDetails({ name: "containers", }); - const containerTypeOptions = useMemo(() => { - if (!referenceData?.containers) return []; - return referenceData.containers.flatMap((group) => - group.types.map((t) => t.name), + const containerTypeOptionsBySize = useMemo(() => { + if (!referenceData?.containers) return new Map(); + return new Map( + referenceData.containers.map((g) => [g.size, g.types.map((t) => t.name)]), ); }, [referenceData]); + const selectedCommodity = useMemo(() => { + if (!referenceData?.cargo_type || !freightType || !bulkCommoditytype) return null; + const group = referenceData.cargo_type.find( + (g) => g.code.toLowerCase() === freightType, + ); + return group?.children?.find( + (c) => c.name === bulkCommoditytype, + ) ?? null; + }, [referenceData, freightType, bulkCommoditytype]); + const freightTypeGroups = useMemo(() => { if (!referenceData?.cargo_type) return []; - return referenceData.cargo_type.filter((g) => g.code !== "CONTAINER"); + return referenceData.cargo_type.filter( + (g) => g.code !== "CONTAINER" && !/container/i.test(g.name), + ); }, [referenceData]); const commodityOptions = useMemo(() => { @@ -219,6 +243,22 @@ export function Step5CargoDetails({ )} /> )} + + {selectedCommodity?.show_free_text_box && ( + ( + + )} + /> + )} )} @@ -253,7 +293,13 @@ export function Step5CargoDetails({ className="flex flex-col gap-3 rounded-xl border border-gray-200 bg-gray-50/50 p-4" >
- + Container {index + 1} {fields.length > 1 && ( @@ -300,7 +346,9 @@ export function Step5CargoDetails({

{ct.label}

-

{ct.limit}

+

+ {ct.limit} +

))} @@ -324,7 +372,10 @@ export function Step5CargoDetails({ type="button" onClick={() => qtyField.onChange( - Math.max(1, Number(qtyField.value ?? 1) - 1).toString(), + Math.max( + 1, + Number(qtyField.value ?? 1) - 1, + ).toString(), ) } className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium transition hover:bg-gray-50" @@ -333,7 +384,9 @@ export function Step5CargoDetails({ qtyField.onChange(e.target.value)} + onChange={(e) => + qtyField.onChange(e.target.value) + } onBlur={qtyField.onBlur} type="number" min={1} @@ -388,7 +441,11 @@ export function Step5CargoDetails({ error={fieldState.error} label="Container Type *" placeholder="Select type..." - data={containerTypeOptions} + data={ + containerTypeOptionsBySize.get( + containers[index]?.type ?? "20ft", + ) ?? [] + } /> )} /> diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx deleted file mode 100644 index c58b34241..000000000 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { type UseFormReturn } from "react-hook-form"; -import { type BookingFormValues, type WagonCalcResult } from "./schema"; -import { AlertBox, StepHeader, StepLabel } from "./shared"; - -type BookingForm = UseFormReturn; - -export function Step6WagonAllocation({ - form, - wagons, -}: { - form: BookingForm; - wagons: WagonCalcResult | null; -}) { - const containers = form.watch("containers") ?? []; - const totalContainers = containers.reduce( - (sum, c) => sum + Number(c.qty || 0), - 0, - ); - const containerSummary = containers - .filter((c) => +c.qty > 0) - .map((c) => `${c.qty} × ${c.type}`) - .join(", "); - return ( -
- - - {!wagons ? ( - - Complete the container configuration in the previous step to see wagon - allocation. - - ) : ( - <> -
-
-

- {wagons.totalWagons} -

-

- Wagons Required -

-
-
-

{totalContainers}

-

- {containerSummary || "Containers"} -

-
-
-

{wagons.sharedWagons}

-

Shared Slots

-
-
- -
- Wagon Layout -
- {new Array(wagons.ft40Wagons).fill(0).map((_, index) => ( -
- 1 × 40ft -
- ))} - {new Array(wagons.sharedWagons).fill(0).map((_, index) => ( -
- 2 × 20ft -
- ))} - {wagons.hasOddUnit && ( -
- 1 × 20ft -
- )} -
-
- - {wagons.hasOddUnit && ( - <> - -
-
-

Unpaired 20ft Container

-

- One 20ft container occupies only half a wagon. The wagon - will depart once a co-loader is found to fill the - remaining slot, which may delay departure{" "} - beyond the standard lead time. -

-
-
-
- - )} - - )} -
- ); -} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/steps.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/steps.tsx index 9f1445f91..2532da237 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/steps.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/steps.tsx @@ -2,5 +2,6 @@ export { Step1ContractType } from "./step1-contract-type"; export { Step2ServiceType } from "./step2-service-type"; export { Step4Route } from "./step4-route"; export { Step5CargoDetails } from "./step5-cargo-details"; +export { StepScheduling } from "./step-scheduling"; export { StepDocuments } from "./step-documents"; export { Step8Review } from "./step8-review"; diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index 2ba2c9535..833a5d437 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -463,9 +463,9 @@ export interface CreateBookingContainerDto { export interface CreateBookingDto { reference?: string; - customerId?: string; companyId?: string; trainId?: string; + trainScheduleId?: string; scheduledDate: string; contractType: "NEW" | "RENEWAL"; previousContractId?: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f918ecee4..c336eb8bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,7 +206,7 @@ importers: version: 5.101.0(react@19.2.6) '@tria-plc/iamui-common': specifier: 1.1.2 - version: 1.1.2(9eda5000a9f7ac3b614e21a2a3a3f1e2) + version: 1.1.2(631ddfe3435b77e5a0893e986e0c71da) axios: specifier: ^1.7.7 version: 1.17.0 @@ -365,6 +365,9 @@ importers: '@edr/tsconfig': specifier: workspace:* version: link:../../../packages/config/tsconfig + '@hookform/devtools': + specifier: ^4.4.0 + version: 4.4.0(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tailwindcss/vite': specifier: ^4.3.0 version: 4.3.0(vite@5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)) @@ -2090,6 +2093,12 @@ packages: peerDependencies: hono: ^4 + '@hookform/devtools@4.4.0': + resolution: {integrity: sha512-Mtlic+uigoYBPXlfvPBfiYYUZuyMrD3pTjDpVIhL6eCZTvQkHsKBSKeZCvXWUZr8fqrkzDg27N+ZuazLKq6Vmg==} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: ^16.8.0 || ^17 || ^18 || ^19 + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -9613,6 +9622,11 @@ packages: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} + little-state-machine@4.8.1: + resolution: {integrity: sha512-liPHqaWMQ7rzZryQUDnbZ1Gclnnai3dIyaJ0nAgwZRXMzqbYrydrlCI0NDojRUbE5VYh5vu6hygEUZiH77nQkQ==} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + load-esm@1.0.3: resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} engines: {node: '>=13.2.0'} @@ -11382,6 +11396,11 @@ packages: '@types/react': optional: true + react-simple-animate@3.5.3: + resolution: {integrity: sha512-Ob+SmB5J1tXDEZyOe2Hf950K4M8VaWBBmQ3cS2BUnTORqHjhK0iKG8fB+bo47ZL15t8d3g/Y0roiqH05UBjG7A==} + peerDependencies: + react-dom: ^16.8.0 || ^17 || ^18 || ^19 + react-smooth@4.0.4: resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} peerDependencies: @@ -13008,6 +13027,12 @@ packages: '@types/react': optional: true + use-deep-compare-effect@1.8.1: + resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13' + use-isomorphic-layout-effect@1.2.1: resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} peerDependencies: @@ -14693,6 +14718,22 @@ snapshots: dependencies: hono: 4.12.23 + '@hookform/devtools@4.4.0(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6) + '@types/lodash': 4.17.24 + little-state-machine: 4.8.1(react@19.2.6) + lodash: 4.18.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-simple-animate: 3.5.3(react-dom@19.2.6(react@19.2.6)) + use-deep-compare-effect: 1.8.1(react@19.2.6) + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - supports-color + '@hookform/resolvers@3.10.0(react-hook-form@7.77.0(react@18.3.1))': dependencies: react-hook-form: 7.77.0(react@18.3.1) @@ -15569,29 +15610,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-date-pickers@6.20.2(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6))(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mui/system@5.18.0(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(date-fns@4.4.0)(dayjs@1.11.21)(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@babel/runtime': 7.29.7 - '@mui/base': 5.0.0-beta.70(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@mui/material': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@mui/system': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6) - '@mui/utils': 5.17.1(@types/react@18.3.31)(react@19.2.6) - '@types/react-transition-group': 4.4.12(@types/react@18.3.31) - clsx: 2.1.1 - prop-types: 15.8.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-transition-group: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - optionalDependencies: - '@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6) - date-fns: 4.4.0 - dayjs: 1.11.21 - luxon: 3.7.2 - moment: 2.30.1 - transitivePeerDependencies: - - '@types/react' - '@napi-rs/canvas-android-arm64@0.1.100': optional: true @@ -18783,146 +18801,6 @@ snapshots: - webpack-command - worker-loader - '@tria-plc/iamui-common@1.1.2(9eda5000a9f7ac3b614e21a2a3a3f1e2)': - dependencies: - '@chakra-ui/react': 3.35.0(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6) - '@hookform/resolvers': 5.4.0(react-hook-form@7.77.0(react@19.2.6)) - '@lottiefiles/react-lottie-player': 3.6.0(react@19.2.6) - '@mantine/core': 8.3.18(@mantine/hooks@8.3.18(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@mantine/dates': 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@8.3.18(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@mantine/hooks': 8.3.18(react@19.2.6) - '@onlyoffice/document-editor-react': 2.2.0(@onlyoffice/doceditor-types@9.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-accordion': 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-avatar': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-checkbox': 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-collapsible': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-context-menu': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-hover-card': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-label': 2.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-popover': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-progress': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-radio-group': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-select': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-separator': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slider': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.5(@types/react@18.3.31)(react@19.2.6) - '@radix-ui/react-switch': 1.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-tabs': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toast': 1.2.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toggle-group': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-tooltip': 1.2.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-pdf-viewer/core': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-pdf-viewer/default-layout': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-pdf/renderer': 4.5.1(react@19.2.6) - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1))(react@19.2.6) - '@tabler/icons-react': 3.44.0(react@19.2.6) - '@tailwindcss/vite': 4.3.0(vite@5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)) - '@tanstack/react-query': 5.101.0(react@19.2.6) - '@tanstack/react-query-devtools': 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6) - '@tanstack/react-table': 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tinymce/tinymce-react': 6.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tinymce@7.9.3) - '@tiptap/extension-color': 3.26.0(@tiptap/extension-text-style@3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0))) - '@tiptap/extension-highlight': 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0)) - '@tiptap/extension-image': 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0)) - '@tiptap/extension-link': 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0))(@tiptap/pm@3.26.0) - '@tiptap/extension-text-align': 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0)) - '@tiptap/extension-text-style': 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0)) - '@tiptap/extension-underline': 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0)) - '@tiptap/react': 3.26.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.26.0(@tiptap/pm@3.26.0))(@tiptap/pm@3.26.0)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tiptap/starter-kit': 3.26.0 - '@types/node': 24.13.1 - '@types/tinymce': 4.6.9 - axios: 1.17.0 - class-variance-authority: 0.7.1 - clsx: 2.1.1 - cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - date-fns: 4.4.0 - dayjs: 1.11.21 - ethiopian-calendar-date-converter: 2.1.6 - ethiopian-calendar-new: 1.1.0 - file-type: 18.7.0 - force: 0.0.3 - framer-motion: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - html2canvas: 1.4.1 - i18next: 25.10.10(typescript@5.9.3) - i18next-browser-languagedetector: 8.2.1 - jquery: 3.7.1 - js-cookie: 3.0.8 - jspdf: 3.0.4 - loadash: 1.0.0 - lodash: 4.18.1 - lucide-react: 0.513.0(react@19.2.6) - mui-ethiopian-datepicker: 0.3.2(7d86988bc4fd0020aebaf3d4b373b1a0) - next-themes: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - path: 0.12.7 - qs: 6.15.2 - react: 19.2.6 - react-cookie: 8.1.2(@types/react@18.3.31)(react@19.2.6) - react-datepicker: 8.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-day-picker: 9.14.0(react@19.2.6) - react-dom: 19.2.6(react@19.2.6) - react-dropzone: 14.4.1(react@19.2.6) - react-hook-form: 7.77.0(react@19.2.6) - react-i18next: 15.7.4(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react-icons: 5.6.0(react@19.2.6) - react-image-crop: 11.0.10(react@19.2.6) - react-intersection-observer: 9.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-is: 19.2.7 - react-joyride: 2.9.3(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-pdf: 10.4.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-pdf-html: 2.1.5(@react-pdf/renderer@4.5.1(react@19.2.6))(react@19.2.6) - react-pdf-viewer: 0.1.0 - react-redux: 9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1) - react-resizable-panels: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-router-dom: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-signature-canvas: 1.1.0-alpha.2(@types/prop-types@15.7.15)(@types/react@18.3.31)(prop-types@15.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - recharts: 3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1) - socket.io-client: 4.8.3 - sonner: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - tailwind-merge: 3.6.0 - tailwind-scrollbar-hide: 4.0.0(tailwindcss@4.3.0) - tailwindcss: 4.3.0 - tailwindcss-animate: 1.0.7(tailwindcss@4.3.0) - tesseract.js: 7.0.0 - tinymce: 7.9.3 - url: 0.11.4 - vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - xlsx: 0.18.5 - zod: 3.25.76 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@floating-ui/dom' - - '@mui/icons-material' - - '@mui/material' - - '@mui/x-date-pickers' - - '@onlyoffice/doceditor-types' - - '@tiptap/core' - - '@tiptap/pm' - - '@types/prop-types' - - '@types/react' - - '@types/react-dom' - - bufferutil - - debug - - encoding - - pdfjs-dist - - prop-types - - react-native - - redux - - supports-color - - typescript - - utf-8-validate - - vite - - webpack-cli - - webpack-command - - worker-loader - '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -25223,6 +25101,10 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + little-state-machine@4.8.1(react@19.2.6): + dependencies: + react: 19.2.6 + load-esm@1.0.3: {} load-json-file@1.1.0: @@ -27387,6 +27269,10 @@ snapshots: '@types/prop-types': 15.7.15 '@types/react': 18.3.31 + react-simple-animate@3.5.3(react-dom@19.2.6(react@19.2.6)): + dependencies: + react-dom: 19.2.6(react@19.2.6) + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: fast-equals: 5.4.0 @@ -29433,11 +29319,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.31 - use-isomorphic-layout-effect@1.2.1(@types/react@18.3.31)(react@18.3.1): + use-deep-compare-effect@1.8.1(react@19.2.6): dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.31 + '@babel/runtime': 7.29.7 + dequal: 2.0.3 + react: 19.2.6 use-isomorphic-layout-effect@1.2.1(@types/react@18.3.31)(react@19.2.6): dependencies: