From 4e22c4372ffc7be873534b2c0909d7d10f545e5d Mon Sep 17 00:00:00 2001 From: Yonas Tewabe Date: Thu, 25 Jun 2026 11:16:58 +0300 Subject: [PATCH 1/6] Update Dockerfile.web --- infrastructure/docker/Dockerfile.web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/docker/Dockerfile.web b/infrastructure/docker/Dockerfile.web index 51da18f2e..835116f9a 100644 --- a/infrastructure/docker/Dockerfile.web +++ b/infrastructure/docker/Dockerfile.web @@ -4,7 +4,7 @@ ARG TURBO_FILTER=@edr/freight-portal ARG APP_PATH=apps/edr-freight-web/portal ARG VITE_API_URL=https://edrfreightapi.triaplc.com/api ARG VITE_BASE_API_URL=https://edrfreightapi.triaplc.com -ARG VITE_API_URL=VITE_USER_MANAGEMENT_BASE=/_um +ARG VITE_USER_MANAGEMENT_BASE=/_um ARG NEXT_PUBLIC_API_URL=http://localhost:4000 FROM node:24.15.0-alpine AS base From 1ff304611a1a67e005f5d1e30cace64dba820c55 Mon Sep 17 00:00:00 2001 From: Yonas Tewabe Date: Thu, 25 Jun 2026 11:17:03 +0300 Subject: [PATCH 2/6] Update docker-compose.yaml --- docker-compose.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 09eb8728f..b37833d89 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,12 +4,10 @@ # # Build: DOCKER_BUILDKIT=1 docker compose build # Run: docker compose up -d - services: # Message broker for payment event delivery (payment-api -> passenger/freight). # Management UI: http://localhost:15672 (login edr / edr_secret). vhost: payment. # In deployed envs this is a shared/managed RabbitMQ; only PAYMENT_RABBITMQ_URL changes. - freight-api: build: context: . @@ -41,8 +39,8 @@ services: args: TURBO_FILTER: "@edr/freight-portal" APP_PATH: apps/edr-freight-web/portal - # Browser-reachable URL; override for production deployments - VITE_API_URL: ${FREIGHT_VITE_API_URL:-https://edrfreightapi.triaplc.com/api} + env_file: + - apps/edr-freight-web/portal/.env secrets: - npmrc ports: @@ -55,7 +53,8 @@ services: args: TURBO_FILTER: "@edr/freight-backoffice" APP_PATH: apps/edr-freight-web/backoffice - VITE_API_URL: ${FREIGHT_VITE_API_URL:-https://edrfreightapi.triaplc.com/api} + env_file: + - apps/edr-freight-web/backoffice/.env secrets: - npmrc ports: @@ -68,7 +67,8 @@ services: args: APP_PACKAGE: "@edr/passenger-portal" APP_PATH: apps/edr-passenger-web/portal - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} + env_file: + - apps/edr-passenger-web/portal/.env secrets: - npmrc ports: @@ -83,7 +83,8 @@ services: args: APP_PACKAGE: "@edr/passenger-backoffice" APP_PATH: apps/edr-passenger-web/backoffice - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} + env_file: + - apps/edr-passenger-web/backoffice/.env secrets: - npmrc ports: From 1c841f549f57201dadc6ffa156ffb4a08752c55c Mon Sep 17 00:00:00 2001 From: Yonas Tewabe Date: Thu, 25 Jun 2026 11:22:56 +0300 Subject: [PATCH 3/6] Update docker-compose.yaml --- docker-compose.yaml | 112 ++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index b37833d89..d8badcd1d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,65 +32,65 @@ services: extra_hosts: - "paymentcallback.triaplc.com:10.18.7.179" - freight-portal: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.web - args: - TURBO_FILTER: "@edr/freight-portal" - APP_PATH: apps/edr-freight-web/portal - env_file: - - apps/edr-freight-web/portal/.env - secrets: - - npmrc - ports: - - "${FREIGHT_PORTAL_PORT:-5173}:80" +freight-portal: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/freight-portal" + APP_PATH: apps/edr-freight-web/portal + VITE_API_URL: ${VITE_API_URL:-https://edrfreightapi.triaplc.com/api} + VITE_BASE_API_URL: ${VITE_BASE_API_URL:-https://edrfreightapi.triaplc.com} + VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-/_um} + secrets: + - npmrc + ports: + - "${FREIGHT_PORTAL_PORT:-5173}:80" - freight-backoffice: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.web - args: - TURBO_FILTER: "@edr/freight-backoffice" - APP_PATH: apps/edr-freight-web/backoffice - env_file: - - apps/edr-freight-web/backoffice/.env - secrets: - - npmrc - ports: - - "${FREIGHT_BACKOFFICE_PORT:-5183}:80" +freight-backoffice: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/freight-backoffice" + APP_PATH: apps/edr-freight-web/backoffice + VITE_API_URL: ${VITE_API_URL:-https://edrfreightapi.triaplc.com/api} + VITE_BASE_API_URL: ${VITE_BASE_API_URL:-https://edrfreightapi.triaplc.com} + VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-/_um} + secrets: + - npmrc + ports: + - "${FREIGHT_BACKOFFICE_PORT:-5183}:80" - passenger-portal: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.passenger-web - args: - APP_PACKAGE: "@edr/passenger-portal" - APP_PATH: apps/edr-passenger-web/portal - env_file: - - apps/edr-passenger-web/portal/.env - secrets: - - npmrc - ports: - - "${PASSENGER_PORTAL_PORT:-5174}:${PASSENGER_PORTAL_PORT:-5174}" - env_file: - - apps/edr-passenger-web/portal/.env +passenger-portal: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.passenger-web + args: + APP_PACKAGE: "@edr/passenger-portal" + APP_PATH: apps/edr-passenger-web/portal + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} + secrets: + - npmrc + ports: + - "${PASSENGER_PORTAL_PORT:-5174}:${PASSENGER_PORTAL_PORT:-5174}" + env_file: + - apps/edr-passenger-web/portal/.env - passenger-backoffice: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.passenger-web - args: - APP_PACKAGE: "@edr/passenger-backoffice" - APP_PATH: apps/edr-passenger-web/backoffice - env_file: - - apps/edr-passenger-web/backoffice/.env - secrets: - - npmrc - ports: - - "${PASSENGER_BACKOFFICE_PORT:-5184}:${PASSENGER_BACKOFFICE_PORT:-5184}" - env_file: - - apps/edr-passenger-web/backoffice/.env +passenger-backoffice: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.passenger-web + args: + APP_PACKAGE: "@edr/passenger-backoffice" + APP_PATH: apps/edr-passenger-web/backoffice + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} + secrets: + - npmrc + ports: + - "${PASSENGER_BACKOFFICE_PORT:-5184}:${PASSENGER_BACKOFFICE_PORT:-5184}" + env_file: + - apps/edr-passenger-web/backoffice/.env payment-api: build: From c1aa7ed8d772333646324bb9e8fada08afebbbe5 Mon Sep 17 00:00:00 2001 From: Yonas Tewabe Date: Thu, 25 Jun 2026 11:25:57 +0300 Subject: [PATCH 4/6] Update docker-compose.yaml --- docker-compose.yaml | 118 ++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index d8badcd1d..a045125bb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,65 +32,65 @@ services: extra_hosts: - "paymentcallback.triaplc.com:10.18.7.179" -freight-portal: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.web - args: - TURBO_FILTER: "@edr/freight-portal" - APP_PATH: apps/edr-freight-web/portal - VITE_API_URL: ${VITE_API_URL:-https://edrfreightapi.triaplc.com/api} - VITE_BASE_API_URL: ${VITE_BASE_API_URL:-https://edrfreightapi.triaplc.com} - VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-/_um} - secrets: - - npmrc - ports: - - "${FREIGHT_PORTAL_PORT:-5173}:80" - -freight-backoffice: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.web - args: - TURBO_FILTER: "@edr/freight-backoffice" - APP_PATH: apps/edr-freight-web/backoffice - VITE_API_URL: ${VITE_API_URL:-https://edrfreightapi.triaplc.com/api} - VITE_BASE_API_URL: ${VITE_BASE_API_URL:-https://edrfreightapi.triaplc.com} - VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-/_um} - secrets: - - npmrc - ports: - - "${FREIGHT_BACKOFFICE_PORT:-5183}:80" - -passenger-portal: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.passenger-web - args: - APP_PACKAGE: "@edr/passenger-portal" - APP_PATH: apps/edr-passenger-web/portal - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} - secrets: - - npmrc - ports: - - "${PASSENGER_PORTAL_PORT:-5174}:${PASSENGER_PORTAL_PORT:-5174}" - env_file: - - apps/edr-passenger-web/portal/.env - -passenger-backoffice: - build: - context: . - dockerfile: infrastructure/docker/Dockerfile.passenger-web - args: - APP_PACKAGE: "@edr/passenger-backoffice" - APP_PATH: apps/edr-passenger-web/backoffice - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} - secrets: - - npmrc - ports: - - "${PASSENGER_BACKOFFICE_PORT:-5184}:${PASSENGER_BACKOFFICE_PORT:-5184}" - env_file: - - apps/edr-passenger-web/backoffice/.env + freight-portal: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/freight-portal" + APP_PATH: apps/edr-freight-web/portal + VITE_API_URL: ${VITE_API_URL:-https://edrfreightapi.triaplc.com/api} + VITE_BASE_API_URL: ${VITE_BASE_API_URL:-https://edrfreightapi.triaplc.com} + VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-/_um} + secrets: + - npmrc + ports: + - "${FREIGHT_PORTAL_PORT:-5173}:80" + + freight-backoffice: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.web + args: + TURBO_FILTER: "@edr/freight-backoffice" + APP_PATH: apps/edr-freight-web/backoffice + VITE_API_URL: ${VITE_API_URL:-https://edrfreightapi.triaplc.com/api} + VITE_BASE_API_URL: ${VITE_BASE_API_URL:-https://edrfreightapi.triaplc.com} + VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-/_um} + secrets: + - npmrc + ports: + - "${FREIGHT_BACKOFFICE_PORT:-5183}:80" + + passenger-portal: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.passenger-web + args: + APP_PACKAGE: "@edr/passenger-portal" + APP_PATH: apps/edr-passenger-web/portal + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} + secrets: + - npmrc + ports: + - "${PASSENGER_PORTAL_PORT:-5174}:${PASSENGER_PORTAL_PORT:-5174}" + env_file: + - apps/edr-passenger-web/portal/.env + + passenger-backoffice: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.passenger-web + args: + APP_PACKAGE: "@edr/passenger-backoffice" + APP_PATH: apps/edr-passenger-web/backoffice + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:4000} + secrets: + - npmrc + ports: + - "${PASSENGER_BACKOFFICE_PORT:-5184}:${PASSENGER_BACKOFFICE_PORT:-5184}" + env_file: + - apps/edr-passenger-web/backoffice/.env payment-api: build: From a8f9fc044595f5417136693b87525e62581a1f42 Mon Sep 17 00:00:00 2001 From: yaschalew Date: Thu, 25 Jun 2026 12:02:27 +0300 Subject: [PATCH 5/6] fix last mile --- .../modules/first-mile/first-mile.service.ts | 2 +- .../modules/last-mile/last-mile.service.ts | 55 +++---------------- 2 files changed, 9 insertions(+), 48 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index a00e54fff..ba06604ce 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -133,7 +133,7 @@ export class FirstMileService { async create(dto: CreateFirstMileDto): Promise { return this.firstMileRepository.create({ bookingId: dto.bookingId, - status: dto.status ?? 'PAYMENT_PENDING', + status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, remainingPayment: dto.remainingPayment ?? 0, estimatedKm: dto.estimatedKm ?? null, diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts index 55d5d2965..22b96a344 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; @@ -39,52 +39,15 @@ export class LastMileService { private readonly notificationsService: NotificationsService, ) {} - async acceptBooking(bookingReference: string): Promise { + async acceptBooking(bookingReference: string): Promise { const booking = await this.bookingsRepository.findByReference(bookingReference); if (!booking) { - throw new NotFoundException(`Booking ${bookingReference} not found`); + return null; } if (booking.paymentStatus !== 'PAID') { - throw new BadRequestException( - `Booking ${bookingReference} is not paid (payment status: ${booking.paymentStatus})`, - ); - } - - if ((booking.tradeDirection ?? '').toUpperCase() !== 'IMPORT') { - throw new BadRequestException(`Booking ${bookingReference} is not an import booking`); - } - - if (!booking.lastMileDeliveryAddress?.trim()) { - throw new BadRequestException(`Booking ${bookingReference} has no last-mile delivery address`); - } - - const [eligibleInventory] = await this.dataSource.query( - `SELECT inv.id - FROM freight.warehouse_inventory inv - WHERE inv.booking_id = $1 - AND inv.deleted_at IS NULL - AND inv.status = 'READY_FOR_PICKUP' - AND inv.inspection_status = 'PASSED' - LIMIT 1`, - [booking.id], - ); - if (!eligibleInventory) { - throw new BadRequestException( - `Booking ${bookingReference} is not eligible for last mile. Import inventory must pass inspection and be READY_FOR_PICKUP.`, - ); - } - - const [existing] = await this.dataSource.query( - `SELECT id - FROM freight.last_mile - WHERE booking_id = $1 AND deleted_at IS NULL - LIMIT 1`, - [booking.id], - ); - if (existing) { - return this.findById(existing.id); + return null; } return this.create({ @@ -93,17 +56,15 @@ export class LastMileService { }); } - async acceptBookingByReference(bookingReference: string): Promise { + async acceptBookingByReference(bookingReference: string): Promise { const booking = await this.bookingsRepository.findByReference(bookingReference); if (!booking) { - throw new NotFoundException(`Booking ${bookingReference} not found`); + return null; } if (booking.paymentStatus !== 'PAID') { - throw new BadRequestException( - `Booking ${bookingReference} is not paid (payment status: ${booking.paymentStatus})`, - ); + return null; } return this.create({ @@ -168,7 +129,7 @@ export class LastMileService { async create(dto: CreateLastMileDto): Promise { return this.lastMileRepository.create({ bookingId: dto.bookingId, - status: dto.status ?? 'PAYMENT_PENDING', + status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, remainingPayment: dto.remainingPayment ?? 0, estimatedKm: dto.estimatedKm ?? null, From f85a6a21963866f53d646b05d7feb05293a05316 Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 25 Jun 2026 09:25:50 +0000 Subject: [PATCH 6/6] fix --- .../strategies/notification.sms.strategy.ts | 61 +++-- .../src/pages/operations/LastMilePage.tsx | 208 +++++++++++++++++- 2 files changed, 248 insertions(+), 21 deletions(-) diff --git a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts index 127eae5cd..d8404e84c 100644 --- a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts +++ b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts @@ -1,11 +1,13 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import axios from "axios"; +import axios, { isAxiosError } from "axios"; import { NotificationStrategy } from "./notification.strategy"; @Injectable() export class SmsNotificationStrategy implements NotificationStrategy { + private readonly logger = new Logger(SmsNotificationStrategy.name); + constructor(private readonly configService: ConfigService) {} async send(recipient: string, message: string): Promise { @@ -13,24 +15,43 @@ export class SmsNotificationStrategy implements NotificationStrategy { this.configService.get("OZIKING_SMS_URL") ?? "https://notification-dev.license.aafda.gov.et/api/sms-services/ozeking/sms"; - await axios.post( - url, - { - to: recipient, - sourceId: this.configService.get("OZIKING_SOURCE_ID") ?? "EDR", - sourceName: this.configService.get("OZIKING_SOURCE_NAME") ?? "EDR Freight", - appKey: this.configService.get("OZIKING_APP_KEY") ?? "", - text: message, - callbackUrl: "", - }, - { - headers: { - accept: "*/*", - "Content-Type": "application/json", - }, - }, - ); + const appKey = this.configService.get("OZIKING_APP_KEY") ?? ""; + if (!appKey) { + this.logger.warn("OZIKING_APP_KEY is not set — SMS may be rejected by the API"); + } - return true; + this.logger.debug(`Sending SMS to ${recipient} via ${url}`); + + try { + const response = await axios.post( + url, + { + to: recipient, + sourceId: this.configService.get("OZIKING_SOURCE_ID") ?? "EDR", + sourceName: this.configService.get("OZIKING_SOURCE_NAME") ?? "EDR Freight", + appKey, + text: message, + callbackUrl: "", + }, + { + headers: { + accept: "*/*", + "Content-Type": "application/json", + }, + }, + ); + + this.logger.debug(`SMS API response: ${response.status} ${JSON.stringify(response.data)}`); + return true; + } catch (err) { + if (isAxiosError(err)) { + this.logger.error( + `SMS API error: ${err.message} | status=${err.response?.status} | body=${JSON.stringify(err.response?.data)}`, + ); + } else { + this.logger.error(`SMS send failed: ${String(err)}`); + } + throw err; + } } } diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index f7807f90a..bb121ddfc 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -21,12 +21,15 @@ import { Group, Menu, Modal, + ScrollArea, Select, SimpleGrid, Stack, Text, TextInput, } from "@mantine/core"; +import type { ArrivalQueueItem } from "@/types/warehouse"; +import { warehouseService } from "@/services/warehouse.service"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; @@ -297,6 +300,13 @@ const LastMilePage = () => { const [activeId, setActiveId] = useState(null); const [vehicleValue, setVehicleValue] = useState(null); + // 2-step "Assign Mile" accept modal (arrival queue → vehicle) + const [acceptOpen, setAcceptOpen] = useState(false); + const [acceptStep, setAcceptStep] = useState<1 | 2>(1); + const [selectedArrivalItems, setSelectedArrivalItems] = useState([]); + const [acceptVehicleValue, setAcceptVehicleValue] = useState(null); + const [arrivalSearch, setArrivalSearch] = useState(""); + const { data: listData, isLoading } = useQuery({ queryKey: QUERY_KEYS.LAST_MILE.list(), queryFn: async () => { @@ -338,6 +348,76 @@ const LastMilePage = () => { }, }); + const { data: arrivalQueueData, isLoading: arrivalLoading } = useQuery({ + queryKey: ["warehouse-inventory", "arrival-queue"], + queryFn: () => warehouseService.arrivalQueue().then((r) => r.data), + enabled: acceptOpen, + }); + const arrivalQueue = arrivalQueueData ?? []; + + const filteredArrivalQueue = useMemo(() => { + const term = arrivalSearch.trim().toLowerCase(); + if (!term) return arrivalQueue; + return arrivalQueue.filter((item) => + [item.bookingReference, item.customer, item.cargo, item.warehouse, item.yard] + .join(" ") + .toLowerCase() + .includes(term), + ); + }, [arrivalQueue, arrivalSearch]); + + const acceptMutation = useMutation({ + mutationFn: async ({ items, vehicleId }: { items: ArrivalQueueItem[]; vehicleId: string | null }) => { + const created = await Promise.all( + items.map((item) => lastMileService.accept(item.bookingReference).then((r) => r.data)), + ); + if (vehicleId) { + await Promise.all(created.map((record) => lastMileService.update(record.id, { vehicleId }))); + } + return created; + }, + onSuccess: (created) => { + void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT }); + toast({ + title: "Last-mile leg created", + description: `${created.length} ${created.length === 1 ? "delivery" : "deliveries"} accepted successfully.`, + }); + closeAccept(); + }, + onError: () => { + toast({ title: "Accept failed", variant: "destructive" }); + }, + }); + + const openAccept = () => { + setAcceptOpen(true); + setAcceptStep(1); + setSelectedArrivalItems([]); + setAcceptVehicleValue(null); + setArrivalSearch(""); + }; + + const closeAccept = () => { + setAcceptOpen(false); + setAcceptStep(1); + setSelectedArrivalItems([]); + setAcceptVehicleValue(null); + setArrivalSearch(""); + }; + + const toggleArrivalItem = (item: ArrivalQueueItem) => { + setSelectedArrivalItems((prev) => + prev.some((i) => i.bookingId === item.bookingId) + ? prev.filter((i) => i.bookingId !== item.bookingId) + : [...prev, item], + ); + }; + + const handleAcceptConfirm = () => { + if (!selectedArrivalItems.length) return; + acceptMutation.mutate({ items: selectedArrivalItems, vehicleId: acceptVehicleValue }); + }; + const activeRecord = useMemo( () => records.find((r) => r.id === activeId) ?? null, [records, activeId], @@ -633,7 +713,7 @@ const LastMilePage = () => { Assign vehicle ({selectedIds.length}) )} - @@ -688,6 +768,132 @@ const LastMilePage = () => { + {/* 2-step Assign Mile (arrival queue → vehicle) */} + + {acceptStep === 1 ? "Select Arrivals" : "Assign Mile"} + + } + size="xl" + radius="lg" + centered + > + {acceptStep === 1 ? ( + + setArrivalSearch(e.currentTarget.value)} + /> + + + {arrivalLoading ? ( + Loading arrivals… + ) : filteredArrivalQueue.length === 0 ? ( + No arrivals in queue. + ) : ( + filteredArrivalQueue.map((item) => { + const checked = selectedArrivalItems.some((i) => i.bookingId === item.bookingId); + return ( + toggleArrivalItem(item)} + > + + toggleArrivalItem(item)} + onClick={(e) => e.stopPropagation()} + /> + + + {item.bookingReference} + {item.arrivalDate ? item.arrivalDate.slice(0, 10) : "—"} + + {item.customer ?? "—"} + + {item.cargo && {item.cargo}} + {item.warehouse && · {item.warehouse}} + {item.yard && · {item.yard}} + + + + + ); + }) + )} + + + + + {selectedArrivalItems.length > 0 + ? `${selectedArrivalItems.length} selected` + : "Select one or more arrivals"} + + + + + + + + ) : ( + + + + Selected arrivals ({selectedArrivalItems.length}) + {selectedArrivalItems.map((item) => ( + + {item.bookingReference} + {item.customer ?? "—"} + {item.warehouse ?? item.yard ?? "—"} + + ))} + + + +