From 657e0bffc3ebca2111ca1ecb40c218e2d081924c Mon Sep 17 00:00:00 2001 From: natib21 Date: Sat, 27 Jun 2026 08:30:01 +0000 Subject: [PATCH 1/3] fix ui --- .../backoffice/src/pages/operations/FirstMilePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 075efd34c..4cbe12b53 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -407,7 +407,6 @@ const FirstMilePage = () => { paidBookings.filter( (booking) => booking.tradeDirection === "EXPORT" && - Boolean(booking.firstMilePickupAddress?.trim()) && !existingFirstMileBookingIds.has(booking.id), ), [existingFirstMileBookingIds, paidBookings], From 92df6c427c3ae4eb29467470b117420710337988 Mon Sep 17 00:00:00 2001 From: natib21 Date: Sat, 27 Jun 2026 09:07:28 +0000 Subject: [PATCH 2/3] fix --- .../modules/first-mile/first-mile.service.ts | 60 ++++++++++++------- .../src/pages/operations/FirstMilePage.tsx | 10 +++- 2 files changed, 48 insertions(+), 22 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 1ac78355a..73d98b8fe 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 @@ -1,4 +1,4 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; @@ -44,30 +44,19 @@ export class FirstMileService { * paid before any first-mile work proceeds. Throws if the reference is * unknown or the booking has not reached PAID status. */ - async acceptBooking(bookingId: string): Promise { + async acceptBooking(bookingId: string): Promise { const booking = await this.bookingsRepository.findById(bookingId, { relations: { serviceType: true }, }); if (!booking) { - return null; + throw new NotFoundException(`Booking ${bookingId} not found`); } - if (booking.paymentStatus !== 'PAID') { - return null; - } - - if (!this.bookingRequestsFirstMile(booking)) { - return null; - } - - return this.create({ - bookingId: booking.id, - advancedPayment: 0, - }); + return this.acceptEligibleBooking(booking); } - async acceptBookingByReference(bookingReference: string): Promise { + async acceptBookingByReference(bookingReference: string): Promise { const [booking] = await this.bookingsRepository.findAll({ where: { reference: bookingReference }, relations: { serviceType: true }, @@ -75,15 +64,39 @@ export class FirstMileService { }); if (!booking) { - return null; + throw new NotFoundException(`Booking ${bookingReference} not found`); } + return this.acceptEligibleBooking(booking); + } + + /** + * Shared accept path: validates payment + first-mile eligibility, rejects an + * already-assigned booking, then creates the first-mile record. Throws a + * meaningful HTTP error instead of returning null so the client can surface + * why an accept was refused. + */ + private async acceptEligibleBooking(booking: { + id: string; + reference?: string; + paymentStatus?: string | null; + tradeDirection?: string | null; + firstMilePickupAddress?: string | null; + serviceType?: { includesFirstMile?: boolean | null } | null; + }): Promise { + const label = booking.reference ?? booking.id; + if (booking.paymentStatus !== 'PAID') { - return null; + throw new BadRequestException(`Booking ${label} is not paid`); } if (!this.bookingRequestsFirstMile(booking)) { - return null; + throw new BadRequestException(`Booking ${label} does not require a first mile`); + } + + const existing = await this.findByBookingId(booking.id); + if (existing) { + throw new ConflictException(`Booking ${label} already has a first-mile assignment`); } return this.create({ @@ -174,10 +187,17 @@ export class FirstMileService { } private bookingRequestsFirstMile(booking: { + tradeDirection?: string | null; firstMilePickupAddress?: string | null; serviceType?: { includesFirstMile?: boolean | null } | null; }): boolean { - return Boolean(booking.firstMilePickupAddress?.trim() || booking.serviceType?.includesFirstMile); + // Export bookings always need a first mile (pickup → origin yard); the + // pickup address is captured at assignment time, not required upfront. + return Boolean( + booking.tradeDirection === 'EXPORT' || + booking.firstMilePickupAddress?.trim() || + booking.serviceType?.includesFirstMile, + ); } async update(id: string, dto: UpdateFirstMileDto): Promise { diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 4cbe12b53..208db85f8 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -379,6 +379,9 @@ const FirstMilePage = () => { mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => { const res = await firstMileService.accept(reference); const created = res.data; + if (!created?.id) { + throw new Error("First-mile leg was not created for this booking."); + } if (vehicleId) await firstMileService.update(created.id, { vehicleId }); return created; }, @@ -387,8 +390,11 @@ const FirstMilePage = () => { toast({ title: "Booking accepted", description: "First-mile leg created successfully." }); closeAccept(); }, - onError: () => { - toast({ title: "Accept failed", variant: "destructive" }); + onError: (err: unknown) => { + const description = + (err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? + (err instanceof Error ? err.message : undefined); + toast({ title: "Accept failed", description, variant: "destructive" }); }, }); From 421806e28277cb63f76b478c2d0e751a48c87f08 Mon Sep 17 00:00:00 2001 From: yaschalew Date: Sat, 27 Jun 2026 12:54:57 +0300 Subject: [PATCH 3/3] fix um --- .../backoffice/src/auth/api.ts | 2 +- .../UserManagementHostPage.tsx | 40 +++++---- .../edr-freight-web/backoffice/vite.config.ts | 90 +------------------ 3 files changed, 27 insertions(+), 105 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/auth/api.ts b/apps/edr-freight-web/backoffice/src/auth/api.ts index 8c5b11b54..f72e72949 100644 --- a/apps/edr-freight-web/backoffice/src/auth/api.ts +++ b/apps/edr-freight-web/backoffice/src/auth/api.ts @@ -18,6 +18,6 @@ export const verifyMfaRequest = async (payload: { }; export const getMeRequest = async () => { - const response = await api.get("/auth/me"); + const response = await api.get("/me"); return response.data; }; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx index dedc41084..d473bdc90 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx @@ -1,23 +1,34 @@ -import { useEffect, useRef } from "react"; -import { createRoot, type Root } from "react-dom/client"; +import { useEffect, useRef } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; import { UserManagementApp, type UserManagementRuntimeOptions, type UserManagementSessionSeed, -} from "@tria-plc/iamui"; +} from '@tria-plc/iamui'; +import { iamConfig } from './iamConfig'; -import { getCookie } from "@/auth/cookies"; +function readCookieValue(name: string): string | null { + const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1'); + const match = document.cookie.match( + new RegExp(`(?:^|; )${escaped}=([^;]*)`), + ); -import { iamConfig } from "./iamConfig"; + return match ? decodeURIComponent(match[1]) : null; +} function readInitialSession(): UserManagementSessionSeed | null { - const token = getCookie("auth-token"); + const token = + localStorage.getItem('fhc-backoffice-auth-token') ?? + readCookieValue('auth-token'); if (!token) { return null; } - const refreshToken = getCookie("refresh-token") ?? undefined; + const refreshToken = + localStorage.getItem('fhc-backoffice-auth-refresh-token') ?? + readCookieValue('refresh-token') ?? + undefined; return { token, @@ -47,15 +58,14 @@ export default function UserManagementHostPage() { rootRef.current = createRoot(mountNode); } - const apiBaseUrl = import.meta.env.VITE_BASE_API_URL.replace(/\/+$/, ""); - const iamApiUrl = "/um-api"; + const apiBaseUrl = import.meta.env.VITE_BASE_API_URL.replace(/\/+$/, ''); const runtime: UserManagementRuntimeOptions = { - basename: "/um", + basename: '/um', apiBaseUrl, - apiUrl: iamApiUrl, - recordApiUrl: iamApiUrl, - chronicleUrl: iamApiUrl, - auditApiUrl: iamApiUrl, + apiUrl: `${apiBaseUrl}/api`, + recordApiUrl: `${apiBaseUrl}/api`, + chronicleUrl: `${apiBaseUrl}/api`, + auditApiUrl: `${apiBaseUrl}/api`, }; rootRef.current.render( @@ -78,5 +88,5 @@ export default function UserManagementHostPage() { }; }, []); - return
; + return
; } diff --git a/apps/edr-freight-web/backoffice/vite.config.ts b/apps/edr-freight-web/backoffice/vite.config.ts index 840e831e5..341d2c7c3 100644 --- a/apps/edr-freight-web/backoffice/vite.config.ts +++ b/apps/edr-freight-web/backoffice/vite.config.ts @@ -11,97 +11,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const streamBrowserifyPath = require.resolve("stream-browserify"); -function createIamApiAdapter(apiBaseUrl: string): Plugin { - const upstreamBaseUrl = `${apiBaseUrl.replace(/\/+$/, "")}/api`; - - return { - name: "iam-api-adapter", - configureServer(server) { - server.middlewares.use("/um-api", async (req, res) => { - const requestPath = req.url ?? "/"; - const normalizedPath = requestPath.replace(/^\/+/, ""); - const targetUrl = new URL(normalizedPath, `${upstreamBaseUrl}/`); - - try { - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - if (!value || key.toLowerCase() === "host") { - continue; - } - - if (Array.isArray(value)) { - for (const item of value) { - headers.append(key, item); - } - continue; - } - - headers.set(key, value); - } - - const body = - req.method === "GET" || req.method === "HEAD" - ? undefined - : await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - req.on("data", (chunk) => - chunks.push( - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), - ), - ); - req.on("end", () => resolve(Buffer.concat(chunks))); - req.on("error", reject); - }); - - const upstreamResponse = await fetch(targetUrl, { - method: req.method, - headers, - body, - }); - - if (targetUrl.pathname.endsWith("/auth/me")) { - const payload = await upstreamResponse.json(); - const unwrappedPayload = - payload && - typeof payload === "object" && - "success" in payload && - "data" in payload - ? payload.data - : payload; - - res.statusCode = upstreamResponse.status; - res.setHeader("content-type", "application/json; charset=utf-8"); - res.end(JSON.stringify(unwrappedPayload)); - return; - } - - res.statusCode = upstreamResponse.status; - upstreamResponse.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - res.end(Buffer.from(await upstreamResponse.arrayBuffer())); - } catch (error) { - server.ssrFixStacktrace(error as Error); - res.statusCode = 502; - res.setHeader("content-type", "application/json; charset=utf-8"); - res.end( - JSON.stringify({ - message: "Failed to forward IAM request", - }), - ); - } - }); - }, - }; -} - export default defineConfig(({ mode }) => { - const env = loadEnv(mode, __dirname, ""); - const apiBaseUrl = - env.VITE_BASE_API_URL?.trim() || "http://localhost:3000"; - return { - plugins: [react(), tailwindcss(), createIamApiAdapter(apiBaseUrl)], + plugins: [react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src"),