mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
feat(booking): Implement train schedule selection and update booking data structures
This commit is contained in:
@@ -8,87 +8,98 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { CurrentUser } from '@edr/api-common';
|
import { CurrentUser } from "@edr/api-common";
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
import type { AuthUserPayload } from '../../common/resolve-auth-user-id';
|
import type { AuthUserPayload } from "../../common/resolve-auth-user-id";
|
||||||
import { resolveAuthUserId } from '../../common/resolve-auth-user-id';
|
import { resolveAuthUserId } from "../../common/resolve-auth-user-id";
|
||||||
|
|
||||||
import { TrainSchedulingManage, TrainSchedulingView } from '../../common/booking-guards';
|
import {
|
||||||
import { AssignBookingsDto } from './dto/assign-bookings.dto';
|
TrainSchedulingManage,
|
||||||
import { AssignUnassignedBookingDto } from './dto/assign-unassigned-booking.dto';
|
TrainSchedulingView,
|
||||||
import { CreateContainerTrainScheduleDto } from './dto/create-container-train-schedule.dto';
|
} from "../../common/booking-guards";
|
||||||
import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto';
|
import { AssignBookingsDto } from "./dto/assign-bookings.dto";
|
||||||
import { GetEligibleBulkBookingsDto } from './dto/get-eligible-bulk-bookings.dto';
|
import { AssignUnassignedBookingDto } from "./dto/assign-unassigned-booking.dto";
|
||||||
import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto';
|
import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto";
|
||||||
import { PinWagonsDto } from './dto/pin-wagons.dto';
|
import { GetEligibleBookingsDto } from "./dto/get-eligible-bookings.dto";
|
||||||
import { UpdateContainerItemDto } from './dto/update-container-item.dto';
|
import { GetEligibleBulkBookingsDto } from "./dto/get-eligible-bulk-bookings.dto";
|
||||||
import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto';
|
import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto";
|
||||||
import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto';
|
import { PinWagonsDto } from "./dto/pin-wagons.dto";
|
||||||
import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
|
import { UpdateContainerItemDto } from "./dto/update-container-item.dto";
|
||||||
import { RecordCheckpointDto } from './dto/record-checkpoint.dto';
|
import { PreviewBulkTrainScheduleDto } from "./dto/preview-bulk-train-schedule.dto";
|
||||||
import { AvailableLocomotivesQueryDto } from './dto/available-locomotives-query.dto';
|
import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto";
|
||||||
import { BookableSchedulesQueryDto } from './dto/bookable-schedules-query.dto';
|
import { PreviewTrainScheduleDto } from "./dto/preview-train-schedule.dto";
|
||||||
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
|
import { RecordCheckpointDto } from "./dto/record-checkpoint.dto";
|
||||||
import { TrainSchedulingService } from './train-scheduling.service';
|
import { AvailableLocomotivesQueryDto } from "./dto/available-locomotives-query.dto";
|
||||||
import { BookingBatchService } from './booking-batch.service';
|
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()
|
@ApiBearerAuth()
|
||||||
@Controller('train-scheduling')
|
@Controller("train-scheduling")
|
||||||
export class TrainSchedulingController {
|
export class TrainSchedulingController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trainSchedulingService: TrainSchedulingService,
|
private readonly trainSchedulingService: TrainSchedulingService,
|
||||||
private readonly bookingBatchService: BookingBatchService,
|
private readonly bookingBatchService: BookingBatchService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Get('global-rules')
|
@Get("global-rules")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Get global train scheduling rules (singleton)' })
|
@ApiOperation({ summary: "Get global train scheduling rules (singleton)" })
|
||||||
getGlobalRules() {
|
getGlobalRules() {
|
||||||
return this.trainSchedulingService.getTrainSchedulingGlobalRules();
|
return this.trainSchedulingService.getTrainSchedulingGlobalRules();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('global-rules')
|
@Patch("global-rules")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Update global train scheduling rules (singleton)' })
|
@ApiOperation({ summary: "Update global train scheduling rules (singleton)" })
|
||||||
updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) {
|
updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) {
|
||||||
return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto);
|
return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('eligible-bookings')
|
@Get("eligible-bookings")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'List eligible bookings (container and/or bulk)' })
|
@ApiOperation({ summary: "List eligible bookings (container and/or bulk)" })
|
||||||
getEligibleBookings(@Query() query: GetEligibleBookingsDto) {
|
getEligibleBookings(@Query() query: GetEligibleBookingsDto) {
|
||||||
return this.trainSchedulingService.getEligibleBookings(query);
|
return this.trainSchedulingService.getEligibleBookings(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('batch-board')
|
@Get("batch-board")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Batch monitoring board: schedules with bookings grouped by state' })
|
@ApiOperation({
|
||||||
|
summary: "Batch monitoring board: schedules with bookings grouped by state",
|
||||||
|
})
|
||||||
getBatchBoard() {
|
getBatchBoard() {
|
||||||
return this.bookingBatchService.getBatchBoard();
|
return this.bookingBatchService.getBatchBoard();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('batch-board/:scheduleId')
|
@Get("batch-board/:scheduleId")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Batch board detail for one schedule with EAT 3h windows' })
|
@ApiOperation({
|
||||||
getBatchBoardDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) {
|
summary: "Batch board detail for one schedule with EAT 3h windows",
|
||||||
|
})
|
||||||
|
getBatchBoardDetail(@Param("scheduleId", ParseUUIDPipe) scheduleId: string) {
|
||||||
return this.bookingBatchService.getBatchBoardDetail(scheduleId);
|
return this.bookingBatchService.getBatchBoardDetail(scheduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('available-locomotives')
|
@Get("available-locomotives")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'List AVAILABLE locomotives at the route origin yard',
|
summary: "List AVAILABLE locomotives at the route origin yard",
|
||||||
})
|
})
|
||||||
getAvailableLocomotives(@Query() query: AvailableLocomotivesQueryDto) {
|
getAvailableLocomotives(@Query() query: AvailableLocomotivesQueryDto) {
|
||||||
return this.trainSchedulingService.getAvailableLocomotivesForRoute(query.routeId);
|
return this.trainSchedulingService.getAvailableLocomotivesForRoute(
|
||||||
|
query.routeId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('bookable-schedules')
|
@Get("bookable-schedules")
|
||||||
@TrainSchedulingView()
|
// @TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'OPEN same-route schedules a new booking can target' })
|
@ApiOperation({
|
||||||
|
summary: "OPEN same-route schedules a new booking can target",
|
||||||
|
})
|
||||||
getBookableSchedules(@Query() query: BookableSchedulesQueryDto) {
|
getBookableSchedules(@Query() query: BookableSchedulesQueryDto) {
|
||||||
return this.trainSchedulingService.getBookableSchedules(
|
return this.trainSchedulingService.getBookableSchedules(
|
||||||
query.originYardId,
|
query.originYardId,
|
||||||
@@ -96,282 +107,323 @@ export class TrainSchedulingController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('container/eligible-bookings')
|
@Get("container/eligible-bookings")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'List eligible container bookings' })
|
@ApiOperation({ summary: "List eligible container bookings" })
|
||||||
getEligibleContainerBookings(@Query() query: GetEligibleContainerBookingsDto) {
|
getEligibleContainerBookings(
|
||||||
|
@Query() query: GetEligibleContainerBookingsDto,
|
||||||
|
) {
|
||||||
return this.trainSchedulingService.getEligibleContainerBookings(query);
|
return this.trainSchedulingService.getEligibleContainerBookings(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('bulk/eligible-bookings')
|
@Get("bulk/eligible-bookings")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'List eligible bulk bookings' })
|
@ApiOperation({ summary: "List eligible bulk bookings" })
|
||||||
getEligibleBulkBookings(@Query() query: GetEligibleBulkBookingsDto) {
|
getEligibleBulkBookings(@Query() query: GetEligibleBulkBookingsDto) {
|
||||||
return this.trainSchedulingService.getEligibleBulkBookings(query);
|
return this.trainSchedulingService.getEligibleBulkBookings(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('preview')
|
@Post("preview")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Preview a mixed-capable train schedule' })
|
@ApiOperation({ summary: "Preview a mixed-capable train schedule" })
|
||||||
previewTrainSchedule(@Body() dto: PreviewTrainScheduleDto) {
|
previewTrainSchedule(@Body() dto: PreviewTrainScheduleDto) {
|
||||||
return this.trainSchedulingService.previewTrainSchedule(dto);
|
return this.trainSchedulingService.previewTrainSchedule(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('container/preview')
|
@Post("container/preview")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Preview a container train schedule' })
|
@ApiOperation({ summary: "Preview a container train schedule" })
|
||||||
previewContainerTrainSchedule(@Body() dto: PreviewContainerTrainScheduleDto) {
|
previewContainerTrainSchedule(@Body() dto: PreviewContainerTrainScheduleDto) {
|
||||||
return this.trainSchedulingService.previewContainerTrainSchedule(dto);
|
return this.trainSchedulingService.previewContainerTrainSchedule(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk/preview')
|
@Post("bulk/preview")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Preview a bulk train schedule' })
|
@ApiOperation({ summary: "Preview a bulk train schedule" })
|
||||||
previewBulkTrainSchedule(@Body() dto: PreviewBulkTrainScheduleDto) {
|
previewBulkTrainSchedule(@Body() dto: PreviewBulkTrainScheduleDto) {
|
||||||
return this.trainSchedulingService.previewBulkTrainSchedule(dto);
|
return this.trainSchedulingService.previewBulkTrainSchedule(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('container/schedules')
|
@Post("container/schedules")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Create a container train schedule' })
|
@ApiOperation({ summary: "Create a container train schedule" })
|
||||||
createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
|
createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
|
||||||
return this.trainSchedulingService.createContainerTrainSchedule(dto);
|
return this.trainSchedulingService.createContainerTrainSchedule(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk/schedules')
|
@Post("bulk/schedules")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Create a bulk train schedule' })
|
@ApiOperation({ summary: "Create a bulk train schedule" })
|
||||||
createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
|
createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
|
||||||
return this.trainSchedulingService.createContainerTrainSchedule(dto);
|
return this.trainSchedulingService.createContainerTrainSchedule(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/assign-bookings')
|
@Post("schedules/:id/assign-bookings")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Assign bookings to a train schedule (mixed-capable)' })
|
@ApiOperation({
|
||||||
|
summary: "Assign bookings to a train schedule (mixed-capable)",
|
||||||
|
})
|
||||||
assignBookings(
|
assignBookings(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Body() dto: AssignBookingsDto,
|
@Body() dto: AssignBookingsDto,
|
||||||
) {
|
) {
|
||||||
return this.trainSchedulingService.assignBookingsToSchedule(id, dto);
|
return this.trainSchedulingService.assignBookingsToSchedule(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('container/schedules/:id/assign-bookings')
|
@Post("container/schedules/:id/assign-bookings")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Assign container bookings to a train schedule' })
|
@ApiOperation({ summary: "Assign container bookings to a train schedule" })
|
||||||
assignContainerBookings(
|
assignContainerBookings(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Body() dto: AssignBookingsDto,
|
@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()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Assign bulk bookings to a train schedule' })
|
@ApiOperation({ summary: "Assign bulk bookings to a train schedule" })
|
||||||
assignBulkBookings(
|
assignBulkBookings(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Body() dto: AssignBookingsDto,
|
@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()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Unassign a booking from a train schedule' })
|
@ApiOperation({ summary: "Unassign a booking from a train schedule" })
|
||||||
unassignBooking(
|
unassignBooking(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
||||||
@CurrentUser() user: AuthUserPayload,
|
@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()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Remove an empty wagon slot from a train' })
|
@ApiOperation({ summary: "Remove an empty wagon slot from a train" })
|
||||||
removeWagonSlot(
|
removeWagonSlot(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Param('trainSetWagonId', ParseUUIDPipe) trainSetWagonId: 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()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Update a container number on a wagon slot' })
|
@ApiOperation({ summary: "Update a container number on a wagon slot" })
|
||||||
updateContainerItem(
|
updateContainerItem(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Param('itemId', ParseUUIDPipe) itemId: string,
|
@Param("itemId", ParseUUIDPipe) itemId: string,
|
||||||
@Body() dto: UpdateContainerItemDto,
|
@Body() dto: UpdateContainerItemDto,
|
||||||
) {
|
) {
|
||||||
return this.trainSchedulingService.updateContainerItem(id, itemId, dto);
|
return this.trainSchedulingService.updateContainerItem(id, itemId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('schedules/:id/unassigned-bookings')
|
@Get("schedules/:id/unassigned-bookings")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Get unassigned bookings for a schedule' })
|
@ApiOperation({ summary: "Get unassigned bookings for a schedule" })
|
||||||
getUnassignedBookings(@Param('id', ParseUUIDPipe) id: string) {
|
getUnassignedBookings(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.getUnassignedBookings(id);
|
return this.trainSchedulingService.getUnassignedBookings(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/assign-unassigned-booking')
|
@Post("schedules/:id/assign-unassigned-booking")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Assign one linked unallocated booking to wagons (preserves existing assignments)',
|
summary:
|
||||||
|
"Assign one linked unallocated booking to wagons (preserves existing assignments)",
|
||||||
})
|
})
|
||||||
assignUnassignedBooking(
|
assignUnassignedBooking(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Body() dto: AssignUnassignedBookingDto,
|
@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()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Get removal log for a schedule' })
|
@ApiOperation({ summary: "Get removal log for a schedule" })
|
||||||
getCompositionRemovals(@Param('id', ParseUUIDPipe) id: string) {
|
getCompositionRemovals(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.getCompositionRemovals(id);
|
return this.trainSchedulingService.getCompositionRemovals(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/pin-wagons')
|
@Post("schedules/:id/pin-wagons")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Pin physical wagons to train set slots' })
|
@ApiOperation({ summary: "Pin physical wagons to train set slots" })
|
||||||
pinWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) {
|
pinWagons(@Param("id", ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) {
|
||||||
return this.trainSchedulingService.pinWagons(id, dto);
|
return this.trainSchedulingService.pinWagons(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/finalize')
|
@Post("schedules/:id/finalize")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Finalize a draft train schedule' })
|
@ApiOperation({ summary: "Finalize a draft train schedule" })
|
||||||
finalizeSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.finalizeSchedule(id);
|
return this.trainSchedulingService.finalizeSchedule(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/dispatch')
|
@Post("schedules/:id/dispatch")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Dispatch a scheduled train' })
|
@ApiOperation({ summary: "Dispatch a scheduled train" })
|
||||||
dispatchSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.dispatchSchedule(id);
|
return this.trainSchedulingService.dispatchSchedule(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- batch / booking-window staff actions ----
|
// ---- batch / booking-window staff actions ----
|
||||||
|
|
||||||
@Post('schedules/:id/run-batch')
|
@Post("schedules/:id/run-batch")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Manually run the batch fill for a schedule' })
|
@ApiOperation({ summary: "Manually run the batch fill for a schedule" })
|
||||||
async runBatch(@Param('id', ParseUUIDPipe) id: string) {
|
async runBatch(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
await this.bookingBatchService.fillSchedule(id);
|
await this.bookingBatchService.fillSchedule(id);
|
||||||
return this.bookingBatchService.getBatchBoardDetail(id);
|
return this.bookingBatchService.getBatchBoardDetail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/run-allocation')
|
@Post("schedules/:id/run-allocation")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Run wagon-level allocation for all eligible linked bookings' })
|
@ApiOperation({
|
||||||
async runAllocation(@Param('id', ParseUUIDPipe) id: string) {
|
summary: "Run wagon-level allocation for all eligible linked bookings",
|
||||||
|
})
|
||||||
|
async runAllocation(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.bookingBatchService.runWagonAllocation(id);
|
return this.bookingBatchService.runWagonAllocation(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('schedules/:id/booking-window')
|
@Patch("schedules/:id/booking-window")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Open or close a schedule booking window' })
|
@ApiOperation({ summary: "Open or close a schedule booking window" })
|
||||||
async setBookingWindow(
|
async setBookingWindow(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Body('status') status: 'OPEN' | 'CLOSED',
|
@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);
|
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bookings/:bookingId/mark-paid')
|
@Post("bookings/:bookingId/mark-paid")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Staff: mark a reserved booking paid and allocate it now' })
|
@ApiOperation({
|
||||||
async markBookingPaid(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
summary: "Staff: mark a reserved booking paid and allocate it now",
|
||||||
|
})
|
||||||
|
async markBookingPaid(@Param("bookingId", ParseUUIDPipe) bookingId: string) {
|
||||||
await this.bookingBatchService.markPaid(bookingId);
|
await this.bookingBatchService.markPaid(bookingId);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bookings/:bookingId/expire')
|
@Post("bookings/:bookingId/expire")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Staff: expire a reservation and free its capacity' })
|
@ApiOperation({
|
||||||
async expireBooking(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
summary: "Staff: expire a reservation and free its capacity",
|
||||||
|
})
|
||||||
|
async expireBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) {
|
||||||
await this.bookingBatchService.expireReservation(bookingId);
|
await this.bookingBatchService.expireReservation(bookingId);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bookings/:bookingId/move-schedule')
|
@Post("bookings/:bookingId/move-schedule")
|
||||||
@TrainSchedulingManage()
|
@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(
|
async moveBookingSchedule(
|
||||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
@Param("bookingId", ParseUUIDPipe) bookingId: string,
|
||||||
@Body('trainScheduleId', ParseUUIDPipe) trainScheduleId: string,
|
@Body("trainScheduleId", ParseUUIDPipe) trainScheduleId: string,
|
||||||
) {
|
) {
|
||||||
await this.bookingBatchService.moveToSchedule(bookingId, trainScheduleId);
|
await this.bookingBatchService.moveToSchedule(bookingId, trainScheduleId);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('schedules/:id/checkpoints')
|
@Get("schedules/:id/checkpoints")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Get the tracking corridor + logged checkpoints for a train' })
|
@ApiOperation({
|
||||||
getScheduleCheckpoints(@Param('id', ParseUUIDPipe) id: string) {
|
summary: "Get the tracking corridor + logged checkpoints for a train",
|
||||||
|
})
|
||||||
|
getScheduleCheckpoints(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.getScheduleCheckpoints(id);
|
return this.trainSchedulingService.getScheduleCheckpoints(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/checkpoints')
|
@Post("schedules/:id/checkpoints")
|
||||||
@TrainSchedulingManage()
|
@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(
|
recordCheckpoint(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
@Body() dto: RecordCheckpointDto,
|
@Body() dto: RecordCheckpointDto,
|
||||||
) {
|
) {
|
||||||
return this.trainSchedulingService.recordCheckpoint(id, dto);
|
return this.trainSchedulingService.recordCheckpoint(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('schedules/:id/arrive')
|
@Post("schedules/:id/arrive")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Mark a dispatched train arrived (move assets to destination yard, free assets)' })
|
@ApiOperation({
|
||||||
arriveSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
summary:
|
||||||
|
"Mark a dispatched train arrived (move assets to destination yard, free assets)",
|
||||||
|
})
|
||||||
|
arriveSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.arriveSchedule(id);
|
return this.trainSchedulingService.arriveSchedule(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('container/schedules')
|
@Get("container/schedules")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'List container train schedules' })
|
@ApiOperation({ summary: "List container train schedules" })
|
||||||
getContainerTrainSchedules() {
|
getContainerTrainSchedules() {
|
||||||
return this.trainSchedulingService.getContainerTrainSchedules();
|
return this.trainSchedulingService.getContainerTrainSchedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('bulk/schedules')
|
@Get("bulk/schedules")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'List bulk train schedules' })
|
@ApiOperation({ summary: "List bulk train schedules" })
|
||||||
getBulkTrainSchedules() {
|
getBulkTrainSchedules() {
|
||||||
return this.trainSchedulingService.getContainerTrainSchedules();
|
return this.trainSchedulingService.getContainerTrainSchedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('container/schedules/:id')
|
@Get("container/schedules/:id")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Get container train schedule detail' })
|
@ApiOperation({ summary: "Get container train schedule detail" })
|
||||||
getContainerTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) {
|
getContainerTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('bulk/schedules/:id')
|
@Get("bulk/schedules/:id")
|
||||||
@TrainSchedulingView()
|
@TrainSchedulingView()
|
||||||
@ApiOperation({ summary: 'Get bulk train schedule detail' })
|
@ApiOperation({ summary: "Get bulk train schedule detail" })
|
||||||
getBulkTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) {
|
getBulkTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
return this.trainSchedulingService.getContainerTrainScheduleById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('container/schedules/:id/cancel')
|
@Post("container/schedules/:id/cancel")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Cancel container train schedule' })
|
@ApiOperation({ summary: "Cancel container train schedule" })
|
||||||
cancelTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
cancelTrainSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.cancelTrainSchedule(id);
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk/schedules/:id/cancel')
|
@Post("bulk/schedules/:id/cancel")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Cancel bulk train schedule' })
|
@ApiOperation({ summary: "Cancel bulk train schedule" })
|
||||||
cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
cancelBulkTrainSchedule(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.cancelTrainSchedule(id);
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edr/eslint-config": "workspace:*",
|
"@edr/eslint-config": "workspace:*",
|
||||||
"@edr/tsconfig": "workspace:*",
|
"@edr/tsconfig": "workspace:*",
|
||||||
|
"@hookform/devtools": "^4.4.0",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { DevTool } from "@hookform/devtools";
|
||||||
import {
|
import {
|
||||||
BookingFormInputValues,
|
BookingFormInputValues,
|
||||||
STEPS,
|
STEPS,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
Step2ServiceType,
|
Step2ServiceType,
|
||||||
Step4Route,
|
Step4Route,
|
||||||
Step5CargoDetails,
|
Step5CargoDetails,
|
||||||
|
StepScheduling,
|
||||||
StepDocuments,
|
StepDocuments,
|
||||||
Step8Review,
|
Step8Review,
|
||||||
} from "./new-booking-form/steps";
|
} from "./new-booking-form/steps";
|
||||||
@@ -70,7 +72,11 @@ export default function NewBookingPage() {
|
|||||||
const destinationYard = form.watch("destinationYard");
|
const destinationYard = form.watch("destinationYard");
|
||||||
|
|
||||||
const direction = useMemo(
|
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],
|
[originYard, destinationYard],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,15 +106,11 @@ export default function NewBookingPage() {
|
|||||||
: Number(data.cargoWeight || 0);
|
: Number(data.cargoWeight || 0);
|
||||||
|
|
||||||
// ── Reference data lookups ──────────────────────────────────────────
|
// ── Reference data lookups ──────────────────────────────────────────
|
||||||
const yards = referenceData?.yard ?? [];
|
|
||||||
const services = referenceData?.service ?? [];
|
const services = referenceData?.service ?? [];
|
||||||
const shippingLines = referenceData?.shipping_line ?? [];
|
const shippingLines = referenceData?.shipping_line ?? [];
|
||||||
const cargoTree = referenceData?.cargo_type ?? [];
|
const cargoTree = referenceData?.cargo_type ?? [];
|
||||||
const containerGroups = referenceData?.containers ?? [];
|
const containerGroups = referenceData?.containers ?? [];
|
||||||
|
|
||||||
const findYardId = (name: string): string =>
|
|
||||||
yards.find((y) => y.name === name)?.id ?? "";
|
|
||||||
|
|
||||||
const findServiceTypeId = (): string => {
|
const findServiceTypeId = (): string => {
|
||||||
const code = data.serviceType === "rail" ? "RAIL" : "RAIL_AND_FORWARDING";
|
const code = data.serviceType === "rail" ? "RAIL" : "RAIL_AND_FORWARDING";
|
||||||
return services.find((s) => s.code === code)?.id ?? services[0]?.id ?? "";
|
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 =>
|
const findShippingLineId = (name: string): string | undefined =>
|
||||||
shippingLines.find((l) => l.name === name)?.id;
|
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 => {
|
const findContainerTypeId = (name: string): string => {
|
||||||
for (const group of containerGroups) {
|
for (const group of containerGroups) {
|
||||||
const ct = group.types.find((t) => t.name === name);
|
const ct = group.types.find((t) => t.name === name);
|
||||||
@@ -139,21 +134,18 @@ export default function NewBookingPage() {
|
|||||||
?.children?.find((c) => c.name === data.bulkCommoditytype)
|
?.children?.find((c) => c.name === data.bulkCommoditytype)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const cargoTypeId =
|
const cargoTypeId = selectedChild?.id;
|
||||||
data.cargoType === "container"
|
|
||||||
? findContainerCargoTypeId()
|
|
||||||
: (selectedChild?.id ?? "");
|
|
||||||
|
|
||||||
const cargoFreeText =
|
const cargoFreeText =
|
||||||
data.cargoType === "container"
|
data.cargoType === "container"
|
||||||
? undefined
|
? undefined
|
||||||
: selectedChild?.show_free_text_box
|
: selectedChild?.show_free_text_box
|
||||||
? data.bulkCommoditytype
|
? data.cargoFreeText
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// ── Build API payload ───────────────────────────────────────────────
|
// ── Build API payload ───────────────────────────────────────────────
|
||||||
const apiPayload: CreateBookingPayload = {
|
const apiPayload: CreateBookingPayload = {
|
||||||
scheduledDate: new Date().toISOString().slice(0, 10),
|
scheduledDate: new Date().toISOString(),
|
||||||
contractType:
|
contractType:
|
||||||
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
||||||
serviceTypeId: findServiceTypeId(),
|
serviceTypeId: findServiceTypeId(),
|
||||||
@@ -161,13 +153,13 @@ export default function NewBookingPage() {
|
|||||||
data.equipmentReturn === "with_return"
|
data.equipmentReturn === "with_return"
|
||||||
? "WITH_RETURN"
|
? "WITH_RETURN"
|
||||||
: "WITHOUT_RETURN",
|
: "WITHOUT_RETURN",
|
||||||
originYardId: findYardId(data.originYard),
|
originYardId: data.originYard,
|
||||||
destinationYardId: findYardId(data.destinationYard),
|
destinationYardId: data.destinationYard,
|
||||||
tradeDirection: direction!,
|
tradeDirection: direction!,
|
||||||
cargoTypeId: data.cargoType === "container" ? undefined : cargoTypeId,
|
cargoTypeId,
|
||||||
|
trainScheduleId: data.trainScheduleId,
|
||||||
cargoTotalWeightVgm: totalWeight,
|
cargoTotalWeightVgm: totalWeight,
|
||||||
isHazardous: data.isHazardous,
|
isHazardous: data.isHazardous,
|
||||||
paymentCurrency: "USD",
|
|
||||||
allowConsolidation: data.consolidationEnabled,
|
allowConsolidation: data.consolidationEnabled,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
freightType:
|
freightType:
|
||||||
@@ -243,67 +235,59 @@ export default function NewBookingPage() {
|
|||||||
Back to Bookings
|
Back to Bookings
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="new-booking-form"
|
id="new-booking-form"
|
||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{/* Step indicator */}
|
<Box flex={1} p="24px">
|
||||||
<Box>
|
<Box mb="lg">
|
||||||
<Box className="mx-auto max-w-5xl" style={{ paddingInline: "16px" }}>
|
|
||||||
<StepIndicator step={step} />
|
<StepIndicator step={step} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Step content */}
|
{createMutation.isError && (
|
||||||
<Box flex={1}>
|
<Alert
|
||||||
<Box className="mx-auto max-w-5xl" style={{ padding: "32px 24px" }}>
|
color="red"
|
||||||
{createMutation.isError && (
|
icon={<AlertCircle size={16} />}
|
||||||
<Alert
|
radius="md"
|
||||||
color="red"
|
mb="lg"
|
||||||
icon={<AlertCircle size={16} />}
|
>
|
||||||
radius="md"
|
<Text size="sm" fw={600}>
|
||||||
mb="lg"
|
Failed to save draft
|
||||||
>
|
</Text>
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" mt={4} c="red.7">
|
||||||
Failed to save draft
|
{createMutation.error instanceof Error
|
||||||
</Text>
|
? createMutation.error.message
|
||||||
<Text size="sm" mt={4} c="red.7">
|
: "An unexpected error occurred. Please try again."}
|
||||||
{createMutation.error instanceof Error
|
</Text>
|
||||||
? createMutation.error.message
|
</Alert>
|
||||||
: "An unexpected error occurred. Please try again."}
|
)}
|
||||||
</Text>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 1 && <Step1ContractType form={form} />}
|
{step === 1 && <Step1ContractType form={form} />}
|
||||||
{step === 2 && <Step2ServiceType form={form} />}
|
{step === 2 && <Step2ServiceType form={form} />}
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<Step4Route
|
<Step4Route
|
||||||
form={form}
|
form={form}
|
||||||
referenceData={referenceData}
|
referenceData={referenceData}
|
||||||
isLoading={refDataLoading}
|
isLoading={refDataLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<Step5CargoDetails
|
<Step5CargoDetails
|
||||||
form={form}
|
form={form}
|
||||||
direction={direction}
|
direction={direction!}
|
||||||
referenceData={referenceData}
|
referenceData={referenceData}
|
||||||
isLoading={refDataLoading}
|
isLoading={refDataLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 5 && <StepDocuments form={form} />}
|
{step === 5 && (
|
||||||
{step === 6 && (
|
<StepScheduling form={form} referenceData={referenceData} />
|
||||||
<Step8Review
|
)}
|
||||||
form={form}
|
{step === 6 && <StepDocuments form={form} />}
|
||||||
setStep={setStep}
|
{step === 7 && (
|
||||||
direction={direction}
|
<Step8Review form={form} setStep={setStep} direction={direction!} />
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Navigation footer */}
|
{/* Navigation footer */}
|
||||||
@@ -359,6 +343,7 @@ export default function NewBookingPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
|
{/* <DevTool control={form.control} /> */}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,10 +101,12 @@ export const bookingFormSchema = z
|
|||||||
destinationYard: z.string().min(1, "Select a destination yard."),
|
destinationYard: z.string().min(1, "Select a destination yard."),
|
||||||
shippingLine: z.string(),
|
shippingLine: z.string(),
|
||||||
scheduledDate: z.string().min(1, "Select a shipment date."),
|
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."),
|
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
|
||||||
cargoWeight: z.string(),
|
cargoWeight: z.string(),
|
||||||
freightType: z.string(), // parent group
|
freightType: z.string(), // parent group
|
||||||
bulkCommoditytype: z.string(),
|
bulkCommoditytype: z.string(),
|
||||||
|
cargoFreeText: z.string(),
|
||||||
isHazardous: z.boolean(),
|
isHazardous: z.boolean(),
|
||||||
isRefrigerated: z.boolean(),
|
isRefrigerated: z.boolean(),
|
||||||
containers: z.array(
|
containers: z.array(
|
||||||
@@ -229,8 +231,10 @@ export const initialBookingFormValues: DeepPartial<BookingFormValues> = {
|
|||||||
destinationYard: "",
|
destinationYard: "",
|
||||||
shippingLine: "",
|
shippingLine: "",
|
||||||
scheduledDate: "",
|
scheduledDate: "",
|
||||||
|
trainScheduleId: "",
|
||||||
cargoWeight: "",
|
cargoWeight: "",
|
||||||
bulkCommoditytype: "",
|
bulkCommoditytype: "",
|
||||||
|
cargoFreeText: "",
|
||||||
isHazardous: false,
|
isHazardous: false,
|
||||||
isRefrigerated: false,
|
isRefrigerated: false,
|
||||||
containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
|
containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
|
||||||
@@ -264,7 +268,7 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
|||||||
"containers",
|
"containers",
|
||||||
"consolidationEnabled",
|
"consolidationEnabled",
|
||||||
],
|
],
|
||||||
5: ["scheduledDate"],
|
5: ["scheduledDate", "trainScheduleId"],
|
||||||
6: ["documents"],
|
6: ["documents"],
|
||||||
7: ["notes", "termsAccepted"],
|
7: ["notes", "termsAccepted"],
|
||||||
};
|
};
|
||||||
@@ -280,50 +284,32 @@ export interface WagonConfig {
|
|||||||
type: "20ft" | "40ft";
|
type: "20ft" | "40ft";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WagonCalcResult {
|
|
||||||
totalWagons: number;
|
|
||||||
hasOddUnit: boolean;
|
|
||||||
sharedWagons: number;
|
|
||||||
wagonLayout: WagonConfig[];
|
|
||||||
ft40Wagons: number;
|
|
||||||
ft20Wagons: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRouteDirection(
|
export function getRouteDirection(
|
||||||
origin: Freight.BookingReferenceYard | null | undefined,
|
origin: Freight.BookingReferenceYard | null | undefined,
|
||||||
dest: Freight.BookingReferenceYard | null | undefined,
|
dest: Freight.BookingReferenceYard | null | undefined,
|
||||||
): Freight.ScheduleTradeDirection | null {
|
): Freight.ScheduleTradeDirection | null {
|
||||||
if (!origin || !dest) return null;
|
if (!origin || !dest) return null;
|
||||||
if(origin.country === 'ethiopia' && dest.country === 'ethiopia') {
|
if (origin.country === "ethiopia" && dest.country === "ethiopia") {
|
||||||
return 'DOMESTIC';
|
return "DOMESTIC";
|
||||||
}
|
|
||||||
if(origin.country === 'ethiopia' && dest.country === 'djibouti') {
|
|
||||||
return 'IMPORT';
|
|
||||||
}
|
}
|
||||||
if(origin.country === 'djibouti' && dest.country === 'ethiopia') {
|
if (origin.country === "ethiopia" && dest.country === "djibouti") {
|
||||||
return 'EXPORT';
|
return "IMPORT";
|
||||||
|
}
|
||||||
|
if (origin.country === "djibouti" && dest.country === "ethiopia") {
|
||||||
|
return "EXPORT";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcWagons(containers: ContainerConfig[]): WagonCalcResult {
|
export function calcWagons(containers: ContainerConfig[]) {
|
||||||
const Ft40Wagons = containers
|
|
||||||
.filter((c) => c.type === "40ft")
|
|
||||||
.reduce((sum, c) => sum + Number(c.qty), 0);
|
|
||||||
const Ft20Wagons = containers
|
const Ft20Wagons = containers
|
||||||
.filter((c) => c.type === "20ft")
|
.filter((c) => c.type === "20ft")
|
||||||
.reduce((sum, c) => sum + Number(c.qty), 0);
|
.reduce((sum, c) => sum + Number(c.qty), 0);
|
||||||
const wagonLayout: WagonConfig[] = [];
|
|
||||||
let hasOddUnit = Ft20Wagons % 2 === 1;
|
let hasOddUnit = Ft20Wagons % 2 === 1;
|
||||||
let sharedWagons = Math.floor(Ft20Wagons / 2);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalWagons: sharedWagons + Ft40Wagons,
|
|
||||||
hasOddUnit,
|
hasOddUnit,
|
||||||
sharedWagons,
|
|
||||||
ft40Wagons: Ft40Wagons,
|
|
||||||
ft20Wagons: Ft20Wagons,
|
ft20Wagons: Ft20Wagons,
|
||||||
wagonLayout,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import { Controller, type UseFormReturn } from "react-hook-form";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BOOKING_DOCS_SETTING,
|
BOOKING_DOCS_SETTING,
|
||||||
|
BookingFormInputValues,
|
||||||
type BookingDocuments,
|
type BookingDocuments,
|
||||||
type BookingFormValues,
|
type BookingFormValues,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
import { StepHeader } from "./shared";
|
import { StepHeader } from "./shared";
|
||||||
|
|
||||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
type BookingForm = UseFormReturn<
|
||||||
|
BookingFormInputValues,
|
||||||
|
any,
|
||||||
|
BookingFormValues
|
||||||
|
>;
|
||||||
|
|
||||||
function countAttached(documents: BookingDocuments): number {
|
function countAttached(documents: BookingDocuments): number {
|
||||||
return BOOKING_DOCS_SETTING.fields.filter((f) => {
|
return BOOKING_DOCS_SETTING.fields.filter((f) => {
|
||||||
@@ -57,7 +62,11 @@ export function StepDocuments({ form }: { form: BookingForm }) {
|
|||||||
color: attached === total ? "#0A6F4D" : "#2E5B96",
|
color: attached === total ? "#0A6F4D" : "#2E5B96",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{attached === total ? <CheckCircle2 size={16} /> : `${attached}/${total}`}
|
{attached === total ? (
|
||||||
|
<CheckCircle2 size={16} />
|
||||||
|
) : (
|
||||||
|
`${attached}/${total}`
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{attached === 0
|
{attached === 0
|
||||||
|
|||||||
@@ -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<BookingFormInputValues, any, BookingFormValues>;
|
||||||
|
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<string, Freight.BookableScheduleItem>();
|
||||||
|
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 (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={{ base: 12, md: 7.5 }}>
|
||||||
|
<Card>
|
||||||
|
<Box>
|
||||||
|
<Title order={3} fw={700}>
|
||||||
|
Shipment Date
|
||||||
|
</Title>
|
||||||
|
<Text c="edr-muted" size="sm" mt={4}>
|
||||||
|
Pick a shipment date for your booking from the available
|
||||||
|
schedule.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text fw={700} size="xl">
|
||||||
|
{format(currentDate, "MMMM yyyy")}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
p={6}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => setCurrentDate((d) => addMonths(d, -1))}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={18} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
p={6}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => setCurrentDate((d) => addMonths(d, 1))}
|
||||||
|
>
|
||||||
|
<ChevronRight size={18} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gap: "4px",
|
||||||
|
gridTemplateColumns: "repeat(7, 1fr)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"].map((d) => (
|
||||||
|
<Text
|
||||||
|
key={d}
|
||||||
|
ta="center"
|
||||||
|
size="xs"
|
||||||
|
fw={700}
|
||||||
|
c="edr-muted"
|
||||||
|
mb={8}
|
||||||
|
>
|
||||||
|
{d}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
{days.map((d, i) => {
|
||||||
|
const canSelect =
|
||||||
|
d.isCurrentMonth && d.hasSchedule && !d.isFull;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={i}
|
||||||
|
variant="unstyled"
|
||||||
|
disabled={!canSelect}
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue("scheduledDate", d.dateString, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
form.setValue("trainScheduleId", d.scheduleId, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: "80px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "start",
|
||||||
|
justifyContent: "start",
|
||||||
|
gap: "4px",
|
||||||
|
borderRadius: theme.radius.md,
|
||||||
|
cursor: canSelect ? "pointer" : "default",
|
||||||
|
backgroundColor: d.isSelected
|
||||||
|
? theme.colors["edr-green"][5]
|
||||||
|
: d.isFull
|
||||||
|
? theme.colors["edr-red-soft"][0]
|
||||||
|
: d.hasSchedule
|
||||||
|
? theme.colors.gray[0]
|
||||||
|
: "transparent",
|
||||||
|
border:
|
||||||
|
d.isToday && !d.isSelected
|
||||||
|
? `2px solid ${theme.colors["edr-green"][5]}`
|
||||||
|
: "none",
|
||||||
|
opacity: d.isCurrentMonth ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={700}
|
||||||
|
c={d.isSelected ? "white" : "edr-text.0"}
|
||||||
|
>
|
||||||
|
{d.day}
|
||||||
|
</Text>
|
||||||
|
{d.isCurrentMonth && !d.isSelected && (
|
||||||
|
<>
|
||||||
|
{d.isFull && (
|
||||||
|
<Text
|
||||||
|
size="9px"
|
||||||
|
fw={800}
|
||||||
|
c="edr-red.0"
|
||||||
|
style={{ letterSpacing: "0.05em" }}
|
||||||
|
>
|
||||||
|
FULL
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{d.hasSchedule && !d.isFull && (
|
||||||
|
<Text
|
||||||
|
size="9px"
|
||||||
|
fw={800}
|
||||||
|
c="edr-green.6"
|
||||||
|
style={{ letterSpacing: "0.05em" }}
|
||||||
|
>
|
||||||
|
{d.remainingWagons} WGN
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="xl" mt="xs">
|
||||||
|
{legendItems.map((item) => (
|
||||||
|
<Group key={item.label} gap={8}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: item.color,
|
||||||
|
border:
|
||||||
|
item.label === "No Service"
|
||||||
|
? `2px dashed ${theme.colors.gray[3]}`
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="edr-muted" fw={600}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={{ base: 12, md: 4.5 }}>
|
||||||
|
<Card>
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Title order={4} fw={800} style={{ letterSpacing: "-0.02em" }}>
|
||||||
|
Booking Summary
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
<SummaryRow label="Origin Yard" value={originName} />
|
||||||
|
<SummaryRow label="Destination Yard" value={destinationName} />
|
||||||
|
<SummaryRow
|
||||||
|
label="Cargo Type"
|
||||||
|
value={
|
||||||
|
cargoType
|
||||||
|
? cargoType === "container"
|
||||||
|
? "Container Freight"
|
||||||
|
: "Bulk Freight"
|
||||||
|
: "Not selected"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider my="sm" color="gray.2" />
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="edr-muted"
|
||||||
|
fw={600}
|
||||||
|
style={{ letterSpacing: "0.05em" }}
|
||||||
|
>
|
||||||
|
Shipment Date
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fw={800}
|
||||||
|
size="lg"
|
||||||
|
c={selectedDate ? "edr-green.6" : "edr-muted"}
|
||||||
|
>
|
||||||
|
{selectedDate || "Not selected"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Box
|
||||||
|
p={8}
|
||||||
|
style={{
|
||||||
|
borderRadius: theme.radius.md,
|
||||||
|
backgroundColor: selectedDate
|
||||||
|
? theme.colors["edr-green"][0]
|
||||||
|
: theme.colors.gray[1],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CalendarIcon
|
||||||
|
size={20}
|
||||||
|
color={
|
||||||
|
selectedDate
|
||||||
|
? theme.colors["edr-green"][6]
|
||||||
|
: theme.colors["edr-muted"][0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
borderRadius: theme.radius.md,
|
||||||
|
border: `1px dashed ${theme.colors.gray[4]}`,
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="xs" align="flex-start" wrap="nowrap">
|
||||||
|
<Info
|
||||||
|
size={16}
|
||||||
|
color={theme.colors["edr-muted"][0]}
|
||||||
|
style={{ flexShrink: 0, marginTop: 2 }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="edr-muted"
|
||||||
|
fw={500}
|
||||||
|
style={{ lineHeight: 1.5 }}
|
||||||
|
>
|
||||||
|
Final confirmation of your selected date will be provided
|
||||||
|
after review of your booking details.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="edr-muted"
|
||||||
|
fw={600}
|
||||||
|
textTransform="uppercase"
|
||||||
|
style={{ letterSpacing: "0.05em" }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={700} c="edr-text.0">
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
|
||||||
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react";
|
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 type { Freight } from "@edr/types";
|
||||||
import {
|
import {
|
||||||
BookingFormInputValues,
|
BookingFormInputValues,
|
||||||
@@ -17,7 +24,11 @@ import {
|
|||||||
StepLabel,
|
StepLabel,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
type BookingForm = UseFormReturn<BookingFormInputValues, any, BookingFormValues>;
|
type BookingForm = UseFormReturn<
|
||||||
|
BookingFormInputValues,
|
||||||
|
any,
|
||||||
|
BookingFormValues
|
||||||
|
>;
|
||||||
|
|
||||||
export function Step5CargoDetails({
|
export function Step5CargoDetails({
|
||||||
form,
|
form,
|
||||||
@@ -32,6 +43,7 @@ export function Step5CargoDetails({
|
|||||||
}) {
|
}) {
|
||||||
const cargoType = form.watch("cargoType");
|
const cargoType = form.watch("cargoType");
|
||||||
const freightType = form.watch("freightType");
|
const freightType = form.watch("freightType");
|
||||||
|
const bulkCommoditytype = form.watch("bulkCommoditytype");
|
||||||
const containers = form.watch("containers");
|
const containers = form.watch("containers");
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
@@ -39,16 +51,28 @@ export function Step5CargoDetails({
|
|||||||
name: "containers",
|
name: "containers",
|
||||||
});
|
});
|
||||||
|
|
||||||
const containerTypeOptions = useMemo(() => {
|
const containerTypeOptionsBySize = useMemo(() => {
|
||||||
if (!referenceData?.containers) return [];
|
if (!referenceData?.containers) return new Map<string, string[]>();
|
||||||
return referenceData.containers.flatMap((group) =>
|
return new Map(
|
||||||
group.types.map((t) => t.name),
|
referenceData.containers.map((g) => [g.size, g.types.map((t) => t.name)]),
|
||||||
);
|
);
|
||||||
}, [referenceData]);
|
}, [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(() => {
|
const freightTypeGroups = useMemo(() => {
|
||||||
if (!referenceData?.cargo_type) return [];
|
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]);
|
}, [referenceData]);
|
||||||
|
|
||||||
const commodityOptions = useMemo(() => {
|
const commodityOptions = useMemo(() => {
|
||||||
@@ -219,6 +243,22 @@ export function Step5CargoDetails({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedCommodity?.show_free_text_box && (
|
||||||
|
<Controller
|
||||||
|
name="cargoFreeText"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
{...field}
|
||||||
|
label="Describe cargo *"
|
||||||
|
placeholder="e.g. Charcoal, Wheat, etc."
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -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"
|
className="flex flex-col gap-3 rounded-xl border border-gray-200 bg-gray-50/50 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Text size="xs" fw={600} c="dimmed" tt="uppercase" className="tracking-wide">
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={600}
|
||||||
|
c="dimmed"
|
||||||
|
tt="uppercase"
|
||||||
|
className="tracking-wide"
|
||||||
|
>
|
||||||
Container {index + 1}
|
Container {index + 1}
|
||||||
</Text>
|
</Text>
|
||||||
{fields.length > 1 && (
|
{fields.length > 1 && (
|
||||||
@@ -300,7 +346,9 @@ export function Step5CargoDetails({
|
|||||||
<Package className="h-4 w-4 text-emerald-600" />
|
<Package className="h-4 w-4 text-emerald-600" />
|
||||||
<p className="font-semibold">{ct.label}</p>
|
<p className="font-semibold">{ct.label}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500">{ct.limit}</p>
|
<p className="text-xs text-gray-500">
|
||||||
|
{ct.limit}
|
||||||
|
</p>
|
||||||
</OptionCard>
|
</OptionCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -324,7 +372,10 @@ export function Step5CargoDetails({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
qtyField.onChange(
|
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"
|
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({
|
|||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
value={qtyField.value ?? 1}
|
value={qtyField.value ?? 1}
|
||||||
onChange={(e) => qtyField.onChange(e.target.value)}
|
onChange={(e) =>
|
||||||
|
qtyField.onChange(e.target.value)
|
||||||
|
}
|
||||||
onBlur={qtyField.onBlur}
|
onBlur={qtyField.onBlur}
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -388,7 +441,11 @@ export function Step5CargoDetails({
|
|||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
label="Container Type *"
|
label="Container Type *"
|
||||||
placeholder="Select type..."
|
placeholder="Select type..."
|
||||||
data={containerTypeOptions}
|
data={
|
||||||
|
containerTypeOptionsBySize.get(
|
||||||
|
containers[index]?.type ?? "20ft",
|
||||||
|
) ?? []
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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<BookingFormValues>;
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<StepHeader
|
|
||||||
title="Wagon Allocation"
|
|
||||||
description="System-calculated wagon requirements based on your container profile."
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!wagons ? (
|
|
||||||
<AlertBox tone="info">
|
|
||||||
Complete the container configuration in the previous step to see wagon
|
|
||||||
allocation.
|
|
||||||
</AlertBox>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
|
||||||
<div className="rounded-xl bg-primary/5 p-4 text-center">
|
|
||||||
<p className="text-3xl font-bold text-primary">
|
|
||||||
{wagons.totalWagons}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
Wagons Required
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-muted p-4 text-center">
|
|
||||||
<p className="text-3xl font-bold">{totalContainers}</p>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
{containerSummary || "Containers"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-muted p-4 text-center">
|
|
||||||
<p className="text-3xl font-bold">{wagons.sharedWagons}</p>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">Shared Slots</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<StepLabel>Wagon Layout</StepLabel>
|
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
|
||||||
{new Array(wagons.ft40Wagons).fill(0).map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`flex h-12 min-w-[100px] items-center justify-center rounded-lg border-2 px-3 text-xs font-semibold
|
|
||||||
border-primary/30 bg-primary/5 text-primary `}
|
|
||||||
>
|
|
||||||
1 × 40ft
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{new Array(wagons.sharedWagons).fill(0).map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`flex h-12 min-w-[100px] items-center justify-center rounded-lg border-2 px-3 text-xs font-semibold
|
|
||||||
border-amber-300 bg-amber-50 text-amber-700
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
2 × 20ft
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{wagons.hasOddUnit && (
|
|
||||||
<div
|
|
||||||
className={`flex h-12 min-w-[100px] items-center justify-center rounded-lg border-2 px-3 text-xs font-semibold border-destructive! bg-destructive/10 text-destructive `}
|
|
||||||
>
|
|
||||||
1 × 20ft
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{wagons.hasOddUnit && (
|
|
||||||
<>
|
|
||||||
<AlertBox tone="warning">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold">Unpaired 20ft Container</p>
|
|
||||||
<p className="mt-1 text-xs">
|
|
||||||
One 20ft container occupies only half a wagon. The wagon
|
|
||||||
will depart once a co-loader is found to fill the
|
|
||||||
remaining slot, which <strong>may delay departure</strong>{" "}
|
|
||||||
beyond the standard lead time.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AlertBox>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,5 +2,6 @@ export { Step1ContractType } from "./step1-contract-type";
|
|||||||
export { Step2ServiceType } from "./step2-service-type";
|
export { Step2ServiceType } from "./step2-service-type";
|
||||||
export { Step4Route } from "./step4-route";
|
export { Step4Route } from "./step4-route";
|
||||||
export { Step5CargoDetails } from "./step5-cargo-details";
|
export { Step5CargoDetails } from "./step5-cargo-details";
|
||||||
|
export { StepScheduling } from "./step-scheduling";
|
||||||
export { StepDocuments } from "./step-documents";
|
export { StepDocuments } from "./step-documents";
|
||||||
export { Step8Review } from "./step8-review";
|
export { Step8Review } from "./step8-review";
|
||||||
|
|||||||
@@ -463,9 +463,9 @@ export interface CreateBookingContainerDto {
|
|||||||
|
|
||||||
export interface CreateBookingDto {
|
export interface CreateBookingDto {
|
||||||
reference?: string;
|
reference?: string;
|
||||||
customerId?: string;
|
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
trainId?: string;
|
trainId?: string;
|
||||||
|
trainScheduleId?: string;
|
||||||
scheduledDate: string;
|
scheduledDate: string;
|
||||||
contractType: "NEW" | "RENEWAL";
|
contractType: "NEW" | "RENEWAL";
|
||||||
previousContractId?: string;
|
previousContractId?: string;
|
||||||
|
|||||||
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
@@ -206,7 +206,7 @@ importers:
|
|||||||
version: 5.101.0(react@19.2.6)
|
version: 5.101.0(react@19.2.6)
|
||||||
'@tria-plc/iamui-common':
|
'@tria-plc/iamui-common':
|
||||||
specifier: 1.1.2
|
specifier: 1.1.2
|
||||||
version: 1.1.2(9eda5000a9f7ac3b614e21a2a3a3f1e2)
|
version: 1.1.2(631ddfe3435b77e5a0893e986e0c71da)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.7
|
specifier: ^1.7.7
|
||||||
version: 1.17.0
|
version: 1.17.0
|
||||||
@@ -365,6 +365,9 @@ importers:
|
|||||||
'@edr/tsconfig':
|
'@edr/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../packages/config/tsconfig
|
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':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.3.0
|
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))
|
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:
|
peerDependencies:
|
||||||
hono: ^4
|
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':
|
'@hookform/resolvers@3.10.0':
|
||||||
resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==}
|
resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9613,6 +9622,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
|
resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
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:
|
load-esm@1.0.3:
|
||||||
resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==}
|
resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==}
|
||||||
engines: {node: '>=13.2.0'}
|
engines: {node: '>=13.2.0'}
|
||||||
@@ -11382,6 +11396,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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:
|
react-smooth@4.0.4:
|
||||||
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
|
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -13008,6 +13027,12 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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:
|
use-isomorphic-layout-effect@1.2.1:
|
||||||
resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
|
resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -14693,6 +14718,22 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.23
|
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))':
|
'@hookform/resolvers@3.10.0(react-hook-form@7.77.0(react@18.3.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
react-hook-form: 7.77.0(react@18.3.1)
|
react-hook-form: 7.77.0(react@18.3.1)
|
||||||
@@ -15569,29 +15610,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@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':
|
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -18783,146 +18801,6 @@ snapshots:
|
|||||||
- webpack-command
|
- webpack-command
|
||||||
- worker-loader
|
- 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':
|
'@ts-morph/common@0.27.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
@@ -25223,6 +25101,10 @@ snapshots:
|
|||||||
rfdc: 1.4.1
|
rfdc: 1.4.1
|
||||||
wrap-ansi: 9.0.2
|
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-esm@1.0.3: {}
|
||||||
|
|
||||||
load-json-file@1.1.0:
|
load-json-file@1.1.0:
|
||||||
@@ -27387,6 +27269,10 @@ snapshots:
|
|||||||
'@types/prop-types': 15.7.15
|
'@types/prop-types': 15.7.15
|
||||||
'@types/react': 18.3.31
|
'@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):
|
react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-equals: 5.4.0
|
fast-equals: 5.4.0
|
||||||
@@ -29433,11 +29319,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.31
|
'@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:
|
dependencies:
|
||||||
react: 18.3.1
|
'@babel/runtime': 7.29.7
|
||||||
optionalDependencies:
|
dequal: 2.0.3
|
||||||
'@types/react': 18.3.31
|
react: 19.2.6
|
||||||
|
|
||||||
use-isomorphic-layout-effect@1.2.1(@types/react@18.3.31)(react@19.2.6):
|
use-isomorphic-layout-effect@1.2.1(@types/react@18.3.31)(react@19.2.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user