From 73ed58955e0bb2467c3d72ae7876f882f7b77eea Mon Sep 17 00:00:00 2001
From: Nathnael
Date: Fri, 3 Jul 2026 14:00:54 +0000
Subject: [PATCH 01/17] fix: refetch on focus
---
apps/edr-freight-web/backoffice/src/lib/queryClient.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/edr-freight-web/backoffice/src/lib/queryClient.ts b/apps/edr-freight-web/backoffice/src/lib/queryClient.ts
index 32b94f325..781ffba74 100644
--- a/apps/edr-freight-web/backoffice/src/lib/queryClient.ts
+++ b/apps/edr-freight-web/backoffice/src/lib/queryClient.ts
@@ -27,7 +27,6 @@ export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
- refetchOnWindowFocus: false,
staleTime: 30_000,
},
},
From 2f6f06b7c4870d168ddea43b7017a24224558c10 Mon Sep 17 00:00:00 2001
From: Nathnael
Date: Sat, 4 Jul 2026 06:47:20 +0000
Subject: [PATCH 02/17] feat: add check validity to the user signup
---
.../auth/check-availability.controller.ts | 22 +++++++++
.../auth/check-availability.service.ts | 47 +++++++++++++++++++
.../src/modules/auth/freight-auth.module.ts | 10 +++-
.../portal/src/constants/URLS.ts | 1 +
.../portal/src/pages/accounts/SignupPage.tsx | 22 ++++++++-
.../portal/src/services/api.ts | 7 +++
.../portal/src/services/auth.service.ts | 34 +++++++++-----
apps/edr-freight-web/portal/src/types/auth.ts | 10 ++++
8 files changed, 136 insertions(+), 17 deletions(-)
create mode 100644 apps/edr-freight-api/src/modules/auth/check-availability.controller.ts
create mode 100644 apps/edr-freight-api/src/modules/auth/check-availability.service.ts
diff --git a/apps/edr-freight-api/src/modules/auth/check-availability.controller.ts b/apps/edr-freight-api/src/modules/auth/check-availability.controller.ts
new file mode 100644
index 000000000..13084d1c8
--- /dev/null
+++ b/apps/edr-freight-api/src/modules/auth/check-availability.controller.ts
@@ -0,0 +1,22 @@
+import { Controller, Get, Query } from "@nestjs/common";
+import { ApiOperation, ApiTags } from "@nestjs/swagger";
+import { Public } from "@edr/api-common";
+
+import { CheckAvailabilityService } from "./check-availability.service";
+
+@ApiTags("auth")
+@Controller("auth")
+@Public()
+export class CheckAvailabilityController {
+ constructor(
+ private readonly checkAvailabilityService: CheckAvailabilityService,
+ ) {}
+
+ @Get("check-availability")
+ @ApiOperation({
+ summary: "Check whether an email and/or phone number is already registered",
+ })
+ check(@Query("email") email?: string, @Query("phone") phone?: string) {
+ return this.checkAvailabilityService.check({ email, phone });
+ }
+}
diff --git a/apps/edr-freight-api/src/modules/auth/check-availability.service.ts b/apps/edr-freight-api/src/modules/auth/check-availability.service.ts
new file mode 100644
index 000000000..c9ce84b72
--- /dev/null
+++ b/apps/edr-freight-api/src/modules/auth/check-availability.service.ts
@@ -0,0 +1,47 @@
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Repository } from "typeorm";
+
+import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";
+
+export interface CheckAvailabilityQuery {
+ email?: string;
+ phone?: string;
+}
+
+export interface CheckAvailabilityResult {
+ emailTaken: boolean;
+ phoneTaken: boolean;
+}
+
+@Injectable()
+export class CheckAvailabilityService {
+ constructor(
+ @InjectRepository(User)
+ private readonly userRepository: Repository,
+ ) {}
+
+ async check({
+ email,
+ phone,
+ }: CheckAvailabilityQuery): Promise {
+ if (!email && !phone) {
+ throw new BadRequestException("email or phone is required");
+ }
+
+ const matches = await this.userRepository.find({
+ where: [
+ ...(email ? [{ email }] : []),
+ ...(phone ? [{ phoneNumber: phone }] : []),
+ ],
+ select: { id: true, email: true, phoneNumber: true },
+ });
+
+ return {
+ emailTaken: email ? matches.some((user) => user.email === email) : false,
+ phoneTaken: phone
+ ? matches.some((user) => user.phoneNumber === phone)
+ : false,
+ };
+ }
+}
diff --git a/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts b/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts
index a689ba24e..16fbeffda 100644
--- a/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts
+++ b/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts
@@ -1,10 +1,16 @@
import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity';
+
+import { CheckAvailabilityController } from './check-availability.controller';
+import { CheckAvailabilityService } from './check-availability.service';
import { FreightMeController } from './freight-me.controller';
import { FreightMeService } from './freight-me.service';
@Module({
- controllers: [FreightMeController],
- providers: [FreightMeService],
+ imports: [TypeOrmModule.forFeature([User])],
+ controllers: [FreightMeController, CheckAvailabilityController],
+ providers: [FreightMeService, CheckAvailabilityService],
})
export class FreightAuthModule {}
diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts
index a4e20b029..a8cafc0d1 100644
--- a/apps/edr-freight-web/portal/src/constants/URLS.ts
+++ b/apps/edr-freight-web/portal/src/constants/URLS.ts
@@ -14,6 +14,7 @@ export const URL_CONSTANTS = {
SET_PASSWORD: "/api/auth/set-password",
ME: "/api/auth/me",
GENERATE_VERIFICATION_CODE: "/users/generate-verification-code",
+ CHECK_AVAILABILITY: "/api/auth/check-availability",
},
OTP: {
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
index 50320f699..05fa411b5 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
@@ -132,12 +132,30 @@ export default function SignupPage() {
const passwordValue = watch("password") ?? "";
- // Step 1 — form is valid: send a fresh code to the chosen channel, then
- // move to the OTP challenge.
+ // Step 1 — form is valid: make sure the email/phone aren't already
+ // registered, then send a fresh code to the chosen channel and move to
+ // the OTP challenge.
const requestOtp = async (data: FormData) => {
setError(null);
setSending(true);
try {
+ const availability = await api.auth.checkAvailability.call({
+ email: data.email,
+ phone: data.phone,
+ });
+ if (availability.emailTaken && availability.phoneTaken) {
+ setError("An account with this email and phone number already exists.");
+ return;
+ }
+ if (availability.emailTaken) {
+ setError("An account with this email already exists.");
+ return;
+ }
+ if (availability.phoneTaken) {
+ setError("An account with this phone number already exists.");
+ return;
+ }
+
await api.auth.sendOTP.call(
channel === "email" ? { email: data.email } : { phone: data.phone },
);
diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts
index 81a2488a9..932f6fc46 100644
--- a/apps/edr-freight-web/portal/src/services/api.ts
+++ b/apps/edr-freight-web/portal/src/services/api.ts
@@ -66,6 +66,8 @@ import type {
import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
import type {
AuthUser,
+ CheckAvailabilityPayload,
+ CheckAvailabilityResponse,
GenerateVerificationCodePayload,
LoginPayload,
LoginResponse,
@@ -107,6 +109,11 @@ export const api = {
"setPassword",
authService.setPassword,
),
+ checkAvailability: endpoint(
+ "auth",
+ "checkAvailability",
+ authService.checkAvailability,
+ ),
sendOTP: endpoint(
"auth",
"sendOTP",
diff --git a/apps/edr-freight-web/portal/src/services/auth.service.ts b/apps/edr-freight-web/portal/src/services/auth.service.ts
index e1de1889a..58b81c5ba 100644
--- a/apps/edr-freight-web/portal/src/services/auth.service.ts
+++ b/apps/edr-freight-web/portal/src/services/auth.service.ts
@@ -1,14 +1,16 @@
import { URL_CONSTANTS } from "@/constants/URLS";
import type {
- AuthUser,
- GenerateVerificationCodePayload,
- LoginPayload,
- LoginResponse,
- OtpPayload,
- OtpResponse,
- SetPasswordPayload,
- SignupPayload,
- SignupResponse,
+ AuthUser,
+ CheckAvailabilityPayload,
+ CheckAvailabilityResponse,
+ GenerateVerificationCodePayload,
+ LoginPayload,
+ LoginResponse,
+ OtpPayload,
+ OtpResponse,
+ SetPasswordPayload,
+ SignupPayload,
+ SignupResponse,
} from "@/types/auth";
import { client } from "@/utils/api";
import { ApiResponse } from "@edr/types";
@@ -23,7 +25,7 @@ export const authService = {
},
createUser: async (body: SignupPayload) => {
- const res = await client.post> (
+ const res = await client.post>(
URL_CONSTANTS.USERS.SIGN_UP,
body,
);
@@ -31,9 +33,7 @@ export const authService = {
},
getMyInfo: async () => {
- const res = await client.get(
- URL_CONSTANTS.USERS.ME,
- );
+ const res = await client.get(URL_CONSTANTS.USERS.ME);
return res.data;
},
@@ -53,6 +53,14 @@ export const authService = {
return res.data.data;
},
+ checkAvailability: async (params: CheckAvailabilityPayload) => {
+ const res = await client.get>(
+ URL_CONSTANTS.USERS.CHECK_AVAILABILITY,
+ { params },
+ );
+ return res.data;
+ },
+
sendOTP: async (body: OtpPayload) => {
const res = await client.post>(
URL_CONSTANTS.OTP.SEND,
diff --git a/apps/edr-freight-web/portal/src/types/auth.ts b/apps/edr-freight-web/portal/src/types/auth.ts
index 7b58f573b..04357a9ca 100644
--- a/apps/edr-freight-web/portal/src/types/auth.ts
+++ b/apps/edr-freight-web/portal/src/types/auth.ts
@@ -45,6 +45,16 @@ export interface OtpResponse {
message: string;
}
+export interface CheckAvailabilityPayload {
+ email?: string;
+ phone?: string;
+}
+
+export interface CheckAvailabilityResponse {
+ emailTaken: boolean;
+ phoneTaken: boolean;
+}
+
export interface SetPasswordPayload {
newPassword: string;
confirmPassword: string;
From 1c625cfb827aaf4d6894c50cc54a45935691e0a8 Mon Sep 17 00:00:00 2001
From: Abubeker Yasin
Date: Sat, 4 Jul 2026 10:10:26 +0300
Subject: [PATCH 03/17] feat: ( iam ) OTP-gate registration via IAM signup +
set-password
---
.../src/modules/auth/auth.controller.ts | 20 +-
.../src/modules/auth/auth.dto.ts | 17 +-
.../modules/auth/passenger-auth.service.ts | 101 +++++++-
.../modules/bookings/guest-booking.service.ts | 5 +-
.../portal/src/app/register/page.tsx | 60 ++---
.../portal/src/app/verify-account/page.tsx | 216 ++++++++++++++++++
.../portal/src/lib/api/auth.ts | 4 +
.../portal/src/lib/auth-store.ts | 31 +--
8 files changed, 370 insertions(+), 84 deletions(-)
create mode 100644 apps/edr-passenger-web/portal/src/app/verify-account/page.tsx
diff --git a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts
index d53ef40d7..a6f596bf6 100644
--- a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts
+++ b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts
@@ -3,7 +3,7 @@ import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nes
import { Throttle, SkipThrottle } from '@nestjs/throttler';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { PassengerAuthService } from './passenger-auth.service';
-import { RegisterDto, LoginDto, FaydaRequestPasswordSetupDto, FaydaVerifyAndLoginDto } from './auth.dto';
+import { RegisterDto, LoginDto, ResendRegistrationCodeDto, FaydaRequestPasswordSetupDto, FaydaVerifyAndLoginDto } from './auth.dto';
import { JwtGuard } from '../../common/jwt.guard';
@ApiTags('Passenger Auth')
@@ -14,14 +14,28 @@ export class AuthController {
@Post('register')
@IsPublic()
- @ApiOperation({ summary: 'Register new passenger account' })
- @ApiResponse({ status: 201, description: 'Account created. Returns token + user.' })
+ @ApiOperation({ summary: 'Register new passenger account (sends SMS verification code)' })
+ @ApiResponse({
+ status: 201,
+ description:
+ 'Account created as pending. A verification code is sent via SMS — complete signup via PATCH /v1/auth/set-password.',
+ })
@ApiResponse({ status: 409, description: 'Email or phone already registered' })
@ApiBody({ type: RegisterDto })
register(@Request() req: any, @Body() dto: RegisterDto) {
return this.passengerAuthService.register(dto, req);
}
+ @Post('register/resend-code')
+ @IsPublic()
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Resend the registration verification code for a pending account' })
+ @ApiResponse({ status: 200, description: 'Verification code re-sent if the account is pending.' })
+ @ApiBody({ type: ResendRegistrationCodeDto })
+ resendRegistrationCode(@Request() req: any, @Body() dto: ResendRegistrationCodeDto) {
+ return this.passengerAuthService.resendRegistrationCode(dto, req);
+ }
+
@Post('login')
@IsPublic()
@HttpCode(HttpStatus.OK)
diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts
index 6f43e7212..d0a691b0f 100644
--- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts
+++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts
@@ -1,6 +1,6 @@
-import { IsEmail, IsString, MinLength, ValidateNested } from 'class-validator';
+import { IsEmail, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
-import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
+import { ApiProperty } from '@nestjs/swagger';
export class NameDto {
@ApiProperty({ example: 'ቀለሙ ቀጸላ' })
@@ -29,15 +29,16 @@ export class RegisterDto {
@ValidateNested()
@Type(() => NameDto)
name: NameDto;
+}
- @ApiProperty({ example: 'SecurePass123', minLength: 8, format: 'password' })
- @IsString()
- @MinLength(8)
- password: string;
+export class ResendRegistrationCodeDto {
+ @ApiProperty({ example: 'kelemu@email.com' })
+ @IsEmail()
+ email: string;
- @ApiProperty({ example: 'SecurePass123', format: 'password' })
+ @ApiProperty({ example: '+251912345678' })
@IsString()
- confirmPassword: string;
+ phoneNumber: string;
}
export class LoginDto {
diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts
index 0213bb8f6..1784261e5 100644
--- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts
+++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts
@@ -50,14 +50,17 @@ export class PassengerAuthService {
const iamAuthService = await this.resolveIamAuthService(req);
- const { token, refreshToken } = await iamAuthService.signupWithPassword({
+ // IAM `signup` creates the user as PENDING/isActive=false with NO credential and
+ // SMS-sends a 6-digit verification code. The account cannot log in until the code is
+ // redeemed via PATCH /v1/auth/set-password. We intentionally discard the session
+ // token `signup` returns — the account is not verified yet, so it must never reach
+ // the client.
+ await iamAuthService.signup({
email: dto.email,
username: dto.username,
phoneNumber: dto.phoneNumber,
userType: EUserType.INDIVIDUAL,
name: dto.name,
- password: dto.password,
- confirmPassword: dto.confirmPassword,
});
const iamRows = await this.dataSource.query(
@@ -70,20 +73,98 @@ export class PassengerAuthService {
}
const iamUserId = iamRows[0].id;
- let passengerId: string;
+ // The Prisma "passenger satellite" (Passenger + wallet + loyalty) is NOT provisioned
+ // here — `login()` lazy-provisions it on first successful login, so satellites exist
+ // only for verified users who complete set-password and sign in.
+ return {
+ iamUserId,
+ email: dto.email,
+ phoneNumber: dto.phoneNumber,
+ requiresPasswordSetup: true,
+ };
+ }
+
+ /**
+ * Immediate-activation account creation used by the payment-gated guest-checkout
+ * "create account" path only. Unlike the public `register()` (OTP-gated), this creates a
+ * ready-to-use account from the password entered at checkout and provisions the passenger
+ * satellite synchronously so the booking can attach to it. Do NOT wire this to the public
+ * registration form — that flow must stay behind SMS verification.
+ */
+ async registerWithPassword(
+ dto: {
+ email: string;
+ username: string;
+ phoneNumber: string;
+ name: { en: string; am: string };
+ password: string;
+ },
+ req: any,
+ ): Promise<{ iamUserId: string; passengerId: string }> {
+ const existing = await this.dataSource.query<{ id: string }[]>(
+ `SELECT id FROM iam.users WHERE email = $1 OR phone_number = $2 LIMIT 1`,
+ [dto.email, dto.phoneNumber],
+ );
+ if (existing.length) throw new ConflictException('Email or phone already registered');
+
+ const iamAuthService = await this.resolveIamAuthService(req);
+ await iamAuthService.signupWithPassword({
+ email: dto.email,
+ username: dto.username,
+ phoneNumber: dto.phoneNumber,
+ userType: EUserType.INDIVIDUAL,
+ name: dto.name,
+ password: dto.password,
+ confirmPassword: dto.password,
+ });
+
+ const iamRows = await this.dataSource.query(
+ `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 LIMIT 1`,
+ [dto.email],
+ );
+ if (!iamRows.length) {
+ await this.compensateIamSignup(dto.email);
+ throw new InternalServerErrorException('Account creation failed. Please try again.');
+ }
+ const iamUserId = iamRows[0].id;
+
try {
const result = await this.provisionPassengerSatellite({ iamUserId, auditAction: 'USER_REGISTERED' });
- passengerId = result.passengerId;
+ return { iamUserId, passengerId: result.passengerId };
} catch {
await this.compensateIamSignup(dto.email);
throw new InternalServerErrorException('Account creation failed. Please try again.');
}
+ }
- return {
- token,
- refreshToken,
- user: { id: iamUserId, iamUserId, email: dto.email, fullName: dto.name.en, passengerId },
- };
+ async resendRegistrationCode(
+ dto: { email: string; phoneNumber: string },
+ req: any,
+ ): Promise<{ sent: boolean }> {
+ // Only regenerate for accounts still pending password setup. A fully-registered user
+ // should use forgot-password instead. Always return { sent: true } to avoid leaking
+ // whether the email/phone maps to a pending account (enumeration guard).
+ const users = await this.dataSource.query<{ email: string; phone_number: string }[]>(
+ `SELECT email, phone_number FROM iam.users
+ WHERE email = $1 AND phone_number = $2 AND has_set_password = false LIMIT 1`,
+ [dto.email, dto.phoneNumber],
+ );
+ if (!users.length) return { sent: true };
+
+ const iamAuthService = await this.resolveIamAuthService(req);
+ try {
+ await iamAuthService.generateVerificationCode({
+ email: users[0].email,
+ phoneNumber: users[0].phone_number,
+ type: EOtpType.VERIFY_PHONE_NUMBER,
+ });
+ } catch (err) {
+ this.logger.error(
+ `[PassengerAuthService] resend registration code failed for ${dto.email}`,
+ (err as Error).message,
+ );
+ }
+ return { sent: true };
}
async login(dto: LoginDto, req: any) {
diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts
index 6907d14a3..d2c65db42 100644
--- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts
+++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts
@@ -886,18 +886,17 @@ export class GuestBookingService {
): Promise<{ guestPassengerId: string; iamUserId: string | null; createdAccount: boolean }> {
if (dto.createAccount && firstPassenger.email && dto.password) {
const guestName = firstPassenger.passengerName ?? 'Guest';
- const result = await this.passengerAuthService.register(
+ const result = await this.passengerAuthService.registerWithPassword(
{
email: firstPassenger.email,
username: firstPassenger.email,
phoneNumber: firstPassenger.phone || `+251900000000`,
name: { en: guestName, am: guestName },
password: dto.password,
- confirmPassword: dto.password,
},
req,
);
- return { guestPassengerId: result.user.passengerId, iamUserId: result.user.iamUserId, createdAccount: true };
+ return { guestPassengerId: result.passengerId, iamUserId: result.iamUserId, createdAccount: true };
}
// Create guest passenger with basic profile
diff --git a/apps/edr-passenger-web/portal/src/app/register/page.tsx b/apps/edr-passenger-web/portal/src/app/register/page.tsx
index c335d9e98..a39810be1 100644
--- a/apps/edr-passenger-web/portal/src/app/register/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/register/page.tsx
@@ -9,18 +9,11 @@ import { useAuthStore } from '@/lib/auth-store';
import { useState } from 'react';
import { Train, ShieldCheck } from 'lucide-react';
-const registerSchema = z
- .object({
- fullName: z.string().min(2, 'Full name is required'),
- email: z.string().email('Invalid email address'),
- phone: z.string().min(9, 'Phone number is required'),
- password: z.string().min(8, 'Password must be at least 8 characters'),
- confirmPassword: z.string(),
- })
- .refine((data) => data.password === data.confirmPassword, {
- message: 'Passwords do not match',
- path: ['confirmPassword'],
- });
+const registerSchema = z.object({
+ fullName: z.string().min(2, 'Full name is required'),
+ email: z.string().email('Invalid email address'),
+ phone: z.string().min(9, 'Phone number is required'),
+});
type RegisterForm = z.infer;
@@ -38,14 +31,17 @@ export default function RegisterPage() {
setLoading(true);
setError('');
try {
- await registerUser({
+ const result = await registerUser({
fullName: data.fullName,
email: data.email,
phone: data.phone,
- password: data.password,
- confirmPassword: data.confirmPassword,
});
- router.push('/booking/search');
+ const params = new URLSearchParams({
+ email: result.email,
+ userId: result.iamUserId,
+ phone: result.phoneNumber,
+ });
+ router.push(`/verify-account?${params.toString()}`);
} catch (err: any) {
if (err.response?.status === 409) {
setError('An account with this email or phone number already exists.');
@@ -67,7 +63,7 @@ export default function RegisterPage() {
Create account
- Book faster and manage your trips
+ We'll text you a code to verify your phone
@@ -120,36 +116,8 @@ export default function RegisterPage() {
)}
-
-
-
- {errors.password && (
-
{errors.password.message}
- )}
-
-
-
-
-
- {errors.confirmPassword && (
-
{errors.confirmPassword.message}
- )}
-
-
diff --git a/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx b/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx
new file mode 100644
index 000000000..f9f0b61c9
--- /dev/null
+++ b/apps/edr-passenger-web/portal/src/app/verify-account/page.tsx
@@ -0,0 +1,216 @@
+'use client';
+
+import { Suspense, useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import { Train, ArrowLeft, ShieldCheck } from 'lucide-react';
+import { iamAuthApi } from '@/lib/api/auth';
+import { useAuthStore } from '@/lib/auth-store';
+
+// Mirrors the IAM set-password requirement (class-validator @IsStrongPassword defaults):
+// min length 8, with lower- and upper-case letters, a number, and a symbol.
+function isStrongPassword(pw: string): boolean {
+ return (
+ pw.length >= 8 &&
+ /[a-z]/.test(pw) &&
+ /[A-Z]/.test(pw) &&
+ /[0-9]/.test(pw) &&
+ /[^A-Za-z0-9]/.test(pw)
+ );
+}
+
+function VerifyAccountContent() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const login = useAuthStore((s) => s.login);
+
+ const email = searchParams.get('email') || '';
+ const userId = searchParams.get('userId') || '';
+ const phone = searchParams.get('phone') || '';
+ const linkValid = Boolean(email && userId);
+
+ const [verificationCode, setVerificationCode] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [resending, setResending] = useState(false);
+ const [resent, setResent] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ if (!verificationCode.trim()) {
+ setError('Enter the verification code sent to your phone.');
+ return;
+ }
+ if (!isStrongPassword(newPassword)) {
+ setError('Password must be at least 8 characters and include upper- and lower-case letters, a number, and a symbol.');
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ setError('Passwords do not match.');
+ return;
+ }
+ setLoading(true);
+ try {
+ // Completes signup: PATCH /v1/auth/set-password with the SMS code, which activates
+ // the account and sets the password.
+ await iamAuthApi.resetPassword({
+ userId,
+ email,
+ verificationCode: verificationCode.trim(),
+ newPassword,
+ confirmPassword,
+ });
+ // Auto-login with the freshly-set password; login lazy-provisions the passenger record.
+ await login(email, newPassword);
+ router.push('/booking/search');
+ } catch (err: any) {
+ const msg = err.response?.data?.message || err.message || '';
+ setError(msg || 'Could not verify your account. Check the code and try again, or resend it.');
+ setLoading(false);
+ }
+ };
+
+ const handleResend = async () => {
+ setError('');
+ setResent(false);
+ setResending(true);
+ try {
+ await iamAuthApi.resendRegistrationCode({ email, phoneNumber: phone });
+ setResent(true);
+ } catch {
+ setError('Could not resend the code. Please try again in a moment.');
+ } finally {
+ setResending(false);
+ }
+ };
+
+ return (
+
+
+
+
+
Verify your account
+ {linkValid && (
+
+ Enter the code we sent to your phone and choose a password for{' '}
+ {email}.
+
+ )}
+
+
+
+ {!linkValid ? (
+
+
+ This verification link is invalid or incomplete. Please start registration again.
+
+
+ Back to registration
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export default function VerifyAccountPage() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/edr-passenger-web/portal/src/lib/api/auth.ts b/apps/edr-passenger-web/portal/src/lib/api/auth.ts
index aa5272173..14e9a16a1 100644
--- a/apps/edr-passenger-web/portal/src/lib/api/auth.ts
+++ b/apps/edr-passenger-web/portal/src/lib/api/auth.ts
@@ -11,6 +11,10 @@ export const iamAuthApi = {
forgotPassword: (email: string) =>
axios.post(`${API_URL}/v1/auth/forgot-password`, { email }),
+ // Re-sends the registration verification code for a still-pending account.
+ resendRegistrationCode: (data: { email: string; phoneNumber: string }) =>
+ axios.post(`${API_URL}/auth/register/resend-code`, data),
+
// Completes the forgot-password flow using the link sent via SMS:
// ${FE_BASE_URL}/reset-password?email=..&userId=..&verificationCode=..
resetPassword: (data: {
diff --git a/apps/edr-passenger-web/portal/src/lib/auth-store.ts b/apps/edr-passenger-web/portal/src/lib/auth-store.ts
index 2157cfc0a..0d6e46fd9 100644
--- a/apps/edr-passenger-web/portal/src/lib/auth-store.ts
+++ b/apps/edr-passenger-web/portal/src/lib/auth-store.ts
@@ -31,7 +31,7 @@ interface AuthState {
isAuthenticated: boolean;
isInitialized: boolean;
login: (email: string, password: string) => Promise;
- register: (data: RegisterData) => Promise;
+ register: (data: RegisterData) => Promise;
logout: () => Promise;
setUser: (user: User, token: string) => void;
updateUser: (userData: Partial) => void;
@@ -43,8 +43,12 @@ interface RegisterData {
fullName: string;
email: string;
phone: string;
- password: string;
- confirmPassword: string;
+}
+
+interface RegisterResult {
+ iamUserId: string;
+ email: string;
+ phoneNumber: string;
}
export const useAuthStore = create((set, get) => ({
@@ -118,25 +122,24 @@ export const useAuthStore = create((set, get) => ({
set({ user, token, isAuthenticated: true });
},
- register: async (data: RegisterData) => {
+ register: async (data: RegisterData): Promise => {
// Shape required by the passenger-api RegisterDto; username = email by convention.
+ // Registration no longer takes a password — the account is created as pending and
+ // an SMS verification code is sent. The user completes signup on the verify-account
+ // page (set-password). No token is issued here; the user is NOT logged in yet.
const payload = {
email: data.email,
username: data.email,
phoneNumber: data.phone,
name: { en: data.fullName, am: data.fullName },
- password: data.password,
- confirmPassword: data.confirmPassword,
};
const response: any = await apiClient.post('/auth/register', payload);
- const { token, user } = response.data || response;
-
- if (typeof window !== 'undefined') {
- localStorage.setItem('auth_token', token);
- localStorage.setItem('auth_user', JSON.stringify(user));
- }
-
- set({ user, token, isAuthenticated: true });
+ const result = response.data || response;
+ return {
+ iamUserId: result.iamUserId,
+ email: result.email,
+ phoneNumber: result.phoneNumber,
+ };
},
logout: async () => {
From 74118165c6530cb3f9c58f8221e376e325dd4e76 Mon Sep 17 00:00:00 2001
From: Marshal
Date: Sat, 4 Jul 2026 07:16:23 +0000
Subject: [PATCH 04/17] chages
---
.../src/modules/train-scheduling/booking-batch.service.ts | 5 ++++-
.../src/modules/train-scheduling/train-scheduling.service.ts | 4 +++-
.../src/components/contracts/GlUpcomingWindowsSection.tsx | 2 ++
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts
index 46175c7ef..0b239990b 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts
@@ -310,7 +310,10 @@ export class BookingBatchService implements OnModuleInit {
private async openRouteDayGroups(): Promise {
const open = (
await this.trainSchedulesRepository.findAll({
- where: { bookingWindowStatus: "OPEN" },
+ where: [
+ { bookingWindowStatus: "OPEN", status: TrainScheduleStatusEnum.Draft },
+ { bookingWindowStatus: "OPEN", status: TrainScheduleStatusEnum.Scheduled },
+ ],
})
).filter((s) => s.windowPhase == null);
const groups = new Map();
diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts
index 32a046356..3b36fdc9a 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts
@@ -2049,7 +2049,9 @@ export class TrainSchedulingService {
await this.trainSchedulesRepository.updateStatus(
id,
TrainScheduleStatusEnum.Cancelled,
- {},
+ // Retire the booking window so a canceled schedule never lingers as an
+ // "open window" in booking-window lists or the legacy batch fill.
+ { bookingWindowStatus: 'CLOSED', windowPhase: 'DONE' },
manager,
);
if (schedule.trainSetId) {
diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlUpcomingWindowsSection.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlUpcomingWindowsSection.tsx
index e5e998721..dc95729c1 100644
--- a/apps/edr-freight-web/backoffice/src/components/contracts/GlUpcomingWindowsSection.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlUpcomingWindowsSection.tsx
@@ -235,6 +235,8 @@ export function GlUpcomingWindowsSection() {
const rows = (data ?? []).filter(
(w) => w.windowPhase != null && w.windowPhase !== "DONE" && !isPast(w),
);
+ // Canceled schedules are retired to windowPhase='DONE' server-side, so the
+ // guard above already excludes them; they never reach the upcoming list.
// Open lanes first, then by opening time.
return rows.sort((a, b) => {
const openDiff = Number(b.isOpenNow) - Number(a.isOpenNow);
From 619ffa5419de62addd46a3f9c5b439a2c61787d1 Mon Sep 17 00:00:00 2001
From: Nathnael
Date: Sat, 4 Jul 2026 07:17:05 +0000
Subject: [PATCH 05/17] style(WIP): auth ui clean up.
---
.../src/components/auth/AuthShell.tsx | 139 +++++++
.../backoffice/src/pages/auth/LoginPage.tsx | 392 +++++-------------
.../portal/src/pages/accounts/LoginPage.tsx | 81 ++--
3 files changed, 274 insertions(+), 338 deletions(-)
create mode 100644 apps/edr-freight-web/backoffice/src/components/auth/AuthShell.tsx
diff --git a/apps/edr-freight-web/backoffice/src/components/auth/AuthShell.tsx b/apps/edr-freight-web/backoffice/src/components/auth/AuthShell.tsx
new file mode 100644
index 000000000..ecf06c7c9
--- /dev/null
+++ b/apps/edr-freight-web/backoffice/src/components/auth/AuthShell.tsx
@@ -0,0 +1,139 @@
+import type { ReactNode } from "react";
+import { ArrowUpRight, ChevronDown, Globe } from "lucide-react";
+
+const LOGIN_IMAGE = "/assets/login.png";
+const EDR_LOGO = "/assets/logo.svg";
+
+const LeftPanelDecor = () => (
+
+
+
+
+);
+
+const RightPanelDecor = () => (
+
+);
+
+export interface AuthShellProps {
+ children: ReactNode;
+ /** Tagline shown in the highlighted card over the left image panel. */
+ tagline?: string;
+ taglineBody?: string;
+}
+
+const LeftPanel = ({
+ tagline,
+ taglineBody,
+}: Pick) => (
+
+

+
+
+
+
+

+
+
+
+
+
+
+
+ {tagline ?? "Empower Your Freight Operations"}
+
+
+
+ {taglineBody ??
+ "Sign in to manage bookings, track cargo, and run logistics operations on the Ethio Djibouti Railway freight platform."}
+
+
+
+
+);
+
+const LanguageSelector = () => (
+
+
+ Eng
+
+
+);
+
+export default function AuthShell({
+ children,
+ tagline,
+ taglineBody,
+}: AuthShellProps) {
+ return (
+
+ );
+}
diff --git a/apps/edr-freight-web/backoffice/src/pages/auth/LoginPage.tsx b/apps/edr-freight-web/backoffice/src/pages/auth/LoginPage.tsx
index 89e8557c3..849049bc2 100644
--- a/apps/edr-freight-web/backoffice/src/pages/auth/LoginPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/auth/LoginPage.tsx
@@ -1,162 +1,40 @@
import { type FormEvent, useState } from "react";
import {
- Eye,
- EyeOff,
- ArrowUpRight,
- Globe,
- ChevronDown,
-} from "lucide-react";
+ Alert,
+ Box,
+ Button,
+ Center,
+ Group,
+ Image,
+ PasswordInput,
+ PinInput,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+} from "@mantine/core";
+import { AlertCircle, ArrowLeft } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "@/auth/useAuth";
+import AuthShell from "@/components/auth/AuthShell";
+import { extractApiError } from "@/utils/result";
/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */
const normaliseIdentifier = (raw: string): string => {
const v = raw.trim();
const digits = v.replace(/\D/g, "");
if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) {
- const local = digits.startsWith("251") ? digits.slice(3) : digits.replace(/^0/, "");
+ const local = digits.startsWith("251")
+ ? digits.slice(3)
+ : digits.replace(/^0/, "");
return `+251${local}`;
}
return v.toLowerCase();
};
-const LOGIN_IMAGE = "/assets/login.png";
const EDR_LOGO = "/assets/logo.svg";
-const fieldClass =
- "h-11 w-full rounded-xl border border-gray-200/90 bg-white px-4 text-sm text-gray-900 shadow-sm placeholder:text-gray-400 outline-none transition-all duration-200 hover:border-gray-300 focus:border-primary focus:bg-white focus:ring-4 focus:ring-primary/10";
-
-const primaryButtonClass =
- "h-11 w-full rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow-[0_8px_20px_-6px_rgba(16,94,52,0.5)] transition-all duration-200 hover:bg-primary/90 hover:shadow-[0_10px_24px_-6px_rgba(16,94,52,0.55)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none";
-
-const LeftPanelDecor = () => (
-
-
-
-
-);
-
-const RightPanelDecor = () => (
-
-);
-
-const LeftPanel = () => (
-
-

-
-
-
-
-
-
-
-
-
-
- Empower Your Freight Operations
-
-
-
- Sign in to manage bookings, track cargo, and run logistics operations
- on the Ethio Djibouti Railway freight platform.
-
-
-
-
-);
-
-const LanguageSelector = () => (
-
-
- Eng
-
-
-);
-
-const FormFooter = () => (
-
-);
-
const LoginPage = () => {
const navigate = useNavigate();
const { login, verifyMfa } = useAuth();
@@ -165,7 +43,6 @@ const LoginPage = () => {
const [otp, setOtp] = useState("");
const [needsMfa, setNeedsMfa] = useState(false);
const [submitting, setSubmitting] = useState(false);
- const [showPassword, setShowPassword] = useState(false);
const [normalizedIdentifier, setNormalizedIdentifier] = useState("");
const [error, setError] = useState(null);
@@ -179,15 +56,14 @@ const LoginPage = () => {
setNormalizedIdentifier(normalized);
const result = await login({ email: normalized, password });
- console.log(result);
if (result.mfaRequired) {
setNeedsMfa(true);
return;
}
// navigate("/dashboard/overview", { replace: true });
- } catch {
- setError("Unable to sign in with those credentials.");
+ } catch (err) {
+ setError(extractApiError(err).message);
} finally {
setSubmitting(false);
}
@@ -201,194 +77,132 @@ const LoginPage = () => {
try {
await verifyMfa({ email: normalizedIdentifier, otp: otp.trim() });
navigate("/dashboard/overview", { replace: true });
- } catch {
- setError("Unable to verify the one-time code.");
+ } catch (err) {
+ setError(extractApiError(err).message);
} finally {
setSubmitting(false);
}
};
const loginForm = (
-
+
+
+
);
const mfaForm = (
-
+ Verify
+
+
+
+
);
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {!needsMfa ? loginForm : mfaForm}
-
-
-
-
-
-
-
-
- >
- );
+ return {!needsMfa ? loginForm : mfaForm};
};
export default LoginPage;
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx
index bdd10871b..323b5d5a8 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx
@@ -1,9 +1,11 @@
import { type FormEvent, useState } from "react";
-import { Eye, EyeOff } from "lucide-react";
+import { Alert, Button, PasswordInput, Stack, TextInput } from "@mantine/core";
+import { AlertCircle } from "lucide-react";
import { useLocation, useNavigate } from "react-router-dom";
import useAuth from "@/hooks/useAuth";
-import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
+import AuthShell from "@/components/auth/AuthShell";
+import { extractApiError } from "@/utils/result";
const EDR_LOGO = "/assets/edr-logo.png";
@@ -24,7 +26,6 @@ export default function LoginPage() {
const { login } = useAuth();
const [identifier, setIdentifier] = useState("");
const [password, setPassword] = useState("");
- const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
@@ -41,8 +42,8 @@ export default function LoginPage() {
} else {
setError(result.error.message);
}
- } catch {
- setError("An unexpected error occurred");
+ } catch (err) {
+ setError(extractApiError(err).message);
} finally {
setLoading(false);
}
@@ -64,60 +65,42 @@ export default function LoginPage() {
-
-
-
- setIdentifier(event.target.value)}
- placeholder="name@company.com or 09XXXXXXXX"
- disabled={loading}
- autoComplete="username"
- className={fieldClass}
- />
-
+
+ setIdentifier(event.target.value)}
+ />
-
-
-
+
+
-
- setPassword(event.target.value)}
- placeholder="Enter your password"
- disabled={loading}
- className={`${fieldClass} pr-11`}
- />
-
-
+
setPassword(event.target.value)}
+ />
{error ? (
-
+
) : null}
-
+
Don't have an account?{" "}
@@ -129,7 +112,7 @@ export default function LoginPage() {
Create an account
-
+
);
From 145240d3bded71b8da3c36ded965f89c27e6d93d Mon Sep 17 00:00:00 2001
From: Marshal
Date: Sat, 4 Jul 2026 07:43:03 +0000
Subject: [PATCH 06/17] refactor: remove gate pass granting logic from
clearance services and UI
- Removed the gate pass granting functionality from the BookingClearanceService and ContractClearanceService, replacing it with a new method to retrieve gate pass status from train schedules.
- Updated the ContractsController to eliminate endpoints related to gate pass granting.
- Refactored the UI components (ExportClearanceStepper and PhasedClearanceActionPanel) to reflect the new gate pass securing process, linking to the train scheduling interface instead.
- Cleaned up related constants and query hooks, removing unused code and references to the gate pass functionality.
- Adjusted types in the contracts to accommodate changes in the gate pass handling logic.
---
.../contracts/booking-clearance.service.ts | 12 +-
.../contracts/contract-clearance.service.ts | 14 +-
.../modules/contracts/contracts.controller.ts | 40 --
.../contracts/dto/phased-clearance.dto.ts | 8 -
.../contracts/gl-operations.service.ts | 222 +++--------
.../contracts/ExportClearanceStepper.tsx | 102 ++---
.../contracts/PhasedClearanceActionPanel.tsx | 103 +-----
.../backoffice/src/constants/QUERY_KEYS.ts | 1 -
.../backoffice/src/constants/URLS.ts | 5 -
.../src/hooks/contracts/useContracts.ts | 9 -
.../contracts/GlDjiboutiClearanceListPage.tsx | 349 +++---------------
.../src/services/contracts.service.ts | 29 --
packages/types/src/freight/contracts.ts | 26 +-
13 files changed, 136 insertions(+), 784 deletions(-)
diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts
index 3f169c49c..61ac93925 100644
--- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts
+++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts
@@ -205,7 +205,7 @@ export class BookingClearanceService {
const finalInvoice = await this.glOperationsService.finalInvoiceSummary(bookingId);
const bookingMilestone = (code: string) =>
milestones.find((m) => m.milestoneCode === code);
- const gatepassMilestone = bookingMilestone('GATEPASS_GRANTED');
+ const gatepass = await this.glOperationsService.gatepassForBooking(bookingId);
const t1ClosedMilestone = bookingMilestone('T1_CLOSED');
const riskMilestone = bookingMilestone('RISK_ASSIGNED');
const secondDuty = this.glOperationsService.secondDutyState(milestones, files);
@@ -242,14 +242,8 @@ export class BookingClearanceService {
workflowFiles,
t1,
train,
- gatepassGranted: gatepassMilestone?.status === 'COMPLETED',
- gatepassAt:
- gatepassMilestone?.status === 'COMPLETED'
- ? (gatepassMilestone.metadata?.gatepassAt ??
- (gatepassMilestone.triggeredAt
- ? gatepassMilestone.triggeredAt.toISOString()
- : null))
- : null,
+ gatepassGranted: gatepass.granted,
+ gatepassAt: gatepass.grantedAt,
t1Closed: t1ClosedMilestone?.status === 'COMPLETED',
t1ClosedAt:
t1ClosedMilestone?.status === 'COMPLETED' && t1ClosedMilestone.triggeredAt
diff --git a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts
index 532d26359..2c79ba42f 100644
--- a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts
+++ b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts
@@ -278,7 +278,9 @@ export class ContractClearanceService {
}
const bookingMilestone = (code: string) =>
bookingMilestones.find((m) => m.milestoneCode === code);
- const gatepassMilestone = bookingMilestone('GATEPASS_GRANTED');
+ const gatepass = cycle?.bookingId
+ ? await this.glOperationsService.gatepassForBooking(cycle.bookingId)
+ : { granted: false, grantedAt: null };
const t1ClosedMilestone = bookingMilestone('T1_CLOSED');
const riskMilestone = bookingMilestone('RISK_ASSIGNED');
const secondDuty = this.glOperationsService.secondDutyState(
@@ -344,14 +346,8 @@ export class ContractClearanceService {
workflowFiles,
t1,
train,
- gatepassGranted: gatepassMilestone?.status === 'COMPLETED',
- gatepassAt:
- gatepassMilestone?.status === 'COMPLETED'
- ? (gatepassMilestone.metadata?.gatepassAt ??
- (gatepassMilestone.triggeredAt
- ? gatepassMilestone.triggeredAt.toISOString()
- : null))
- : null,
+ gatepassGranted: gatepass.granted,
+ gatepassAt: gatepass.grantedAt,
t1Closed: t1ClosedMilestone?.status === 'COMPLETED',
t1ClosedAt:
t1ClosedMilestone?.status === 'COMPLETED' && t1ClosedMilestone.triggeredAt
diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts
index 06ac31d68..a22c7cad4 100644
--- a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts
+++ b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts
@@ -77,7 +77,6 @@ import {
} from './dto/gl-operations.dto';
import {
AdviseContractDutyDto,
- GatepassDto,
RoAmendmentDto,
} from './dto/phased-clearance.dto';
@@ -688,30 +687,6 @@ export class ContractsController {
return this.clearanceService.djQueue(filter);
}
- @Get('clearance/dj-schedules')
- @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
- @ApiOperation({ summary: 'Train schedules carrying customs bookings — GL DJ gate-pass table' })
- djClearanceSchedules() {
- return this.glOperationsService.djSchedules();
- }
-
- @Post('clearance/schedules/:scheduleId/gatepass')
- @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
- @ApiOperation({
- summary: 'GL DJ grants the gate pass for every customs booking on a train schedule',
- })
- grantScheduleGatepass(
- @Param('scheduleId', ParseUUIDPipe) scheduleId: string,
- @Body() dto: GatepassDto,
- @CurrentUser() user: AuthUserPayload,
- ) {
- return this.glOperationsService.grantScheduleGatepass(
- scheduleId,
- dto?.gatepassAt,
- resolveAuthUserId(user),
- );
- }
-
// ── Path A self-clearance — Operations reviews the customer's own docs ───────
@Get('clearance/ops-queue')
@@ -947,21 +922,6 @@ export class ContractsController {
return this.glOperationsService.closeT1(bookingId, resolveAuthUserId(user));
}
- @Post('bookings/:bookingId/gatepass')
- @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
- @ApiOperation({ summary: 'GL DJ grants the gate pass for a customs booking (captures time)' })
- grantGatepass(
- @Param('bookingId', ParseUUIDPipe) bookingId: string,
- @Body() dto: GatepassDto,
- @CurrentUser() user: AuthUserPayload,
- ) {
- return this.glOperationsService.grantGatepass(
- bookingId,
- dto?.gatepassAt,
- resolveAuthUserId(user),
- );
- }
-
@Post('bookings/:bookingId/final-invoice')
@BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
@UseInterceptors(FileInterceptor('file'))
diff --git a/apps/edr-freight-api/src/modules/contracts/dto/phased-clearance.dto.ts b/apps/edr-freight-api/src/modules/contracts/dto/phased-clearance.dto.ts
index 6b784073b..34a903427 100644
--- a/apps/edr-freight-api/src/modules/contracts/dto/phased-clearance.dto.ts
+++ b/apps/edr-freight-api/src/modules/contracts/dto/phased-clearance.dto.ts
@@ -36,11 +36,3 @@ export class RoAmendmentDto {
note?: string;
}
-export class GatepassDto {
- @ApiPropertyOptional({
- description: 'When the gate pass was granted (ISO datetime; defaults to now)',
- })
- @IsOptional()
- @IsString()
- gatepassAt?: string;
-}
diff --git a/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts b/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts
index 8fed3a8ff..e639f0867 100644
--- a/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts
+++ b/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts
@@ -4,7 +4,7 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
-import { DataSource, In, IsNull } from 'typeorm';
+import { DataSource, IsNull } from 'typeorm';
import { Freight, GL_FINAL_INVOICE_TYPE, isT1TransportFileCode } from '@edr/types';
import { BillingService } from '../billing/billing.service';
@@ -17,7 +17,6 @@ import {
ClearanceIncident,
IncidentType,
} from './entities/clearance-incident.entity';
-import { ClearanceMilestone } from './entities/clearance-milestone.entity';
import { ContractClearanceCycle } from './entities/contract-clearance-cycle.entity';
import { ClearanceMilestoneService } from './clearance-milestone.service';
import {
@@ -198,6 +197,7 @@ export class GlOperationsService {
}
return {
+ scheduleId: schedule?.id ?? null,
wagonAllocated,
departedAt: schedule?.actualDepartureAt
? new Date(schedule.actualDepartureAt).toISOString()
@@ -208,6 +208,41 @@ export class GlOperationsService {
};
}
+ /**
+ * Gate pass status for a booking, sourced from the train schedule's Djibouti
+ * gate-pass operation (secured via the train-scheduling "Save as Secured"
+ * action) rather than a clearance milestone. For EXPORT bookings this also
+ * backfills the arrival-chain milestones once secured, same as the retired
+ * clearance-side grant action used to.
+ */
+ async gatepassForBooking(
+ bookingId: string,
+ ): Promise<{ granted: boolean; grantedAt: string | null }> {
+ const train = await this.trainState(bookingId);
+ if (!train.scheduleId) return { granted: false, grantedAt: null };
+ const operation = await this.dataSource
+ .getRepository(ImportDjiboutiOperation)
+ .findOne({ where: { trainScheduleId: train.scheduleId } });
+ const grantedAt = operation?.gatepassGrantedAt
+ ? new Date(operation.gatepassGrantedAt).toISOString()
+ : null;
+
+ if (grantedAt) {
+ const booking = await this.getBooking(bookingId);
+ if ((booking.tradeDirection ?? 'IMPORT') === 'EXPORT') {
+ const milestones = await this.milestoneService.listForBooking(bookingId);
+ const byCode = new Map(milestones.map((m) => [m.milestoneCode, m]));
+ for (const code of GlOperationsService.EXPORT_ARRIVAL_CHAIN) {
+ if (byCode.get(code)?.status === 'PENDING') {
+ await this.milestoneService.completeForBooking(bookingId, code);
+ }
+ }
+ }
+ }
+
+ return { granted: Boolean(grantedAt), grantedAt };
+ }
+
/**
* T1 transit-document lifecycle state for an import shipment booking. Wagon
* allocation opens the upload window; train departure locks it; train arrival
@@ -302,8 +337,11 @@ export class GlOperationsService {
'The transport document must be uploaded before T1 can be closed.',
);
}
- if (!done('GATEPASS_GRANTED')) {
- throw new BadRequestException('Grant the gate pass before closing T1.');
+ const gatepass = await this.gatepassForBooking(bookingId);
+ if (!gatepass.granted) {
+ throw new BadRequestException(
+ 'Secure the Djibouti gate pass on the train schedule before closing T1.',
+ );
}
// Export bookings seeded before T1_CLOSED joined the catalog lack the row.
await this.milestoneService.ensureForBooking(bookingId, 'T1_CLOSED', tradeDirection);
@@ -322,182 +360,6 @@ export class GlOperationsService {
'ARRIVED_AT_DJIBOUTI',
];
- /**
- * GL Djibouti grants the gate pass for a customs booking, capturing the time.
- * Export: requires the train to have arrived at Djibouti; back-fills the
- * arrival-chain milestones. Import: requires wagon allocation (pre-loading).
- */
- async grantGatepass(
- bookingId: string,
- gatepassAt?: string,
- userId?: string,
- ): Promise<{ bookingId: string; gatepassAt: string }> {
- const booking = await this.getBooking(bookingId);
- if (!booking.customsClearingEnabled) {
- throw new BadRequestException('Gate pass applies to customs bookings only.');
- }
- const tradeDirection = booking.tradeDirection ?? 'IMPORT';
- const milestones = await this.milestoneService.listForBooking(bookingId);
- const byCode = new Map(milestones.map((m) => [m.milestoneCode, m]));
-
- const existing = byCode.get('GATEPASS_GRANTED');
- if (existing?.status === 'COMPLETED') {
- return {
- bookingId,
- gatepassAt:
- existing.metadata?.gatepassAt ??
- (existing.triggeredAt ? new Date(existing.triggeredAt).toISOString() : ''),
- };
- }
-
- const train = await this.trainState(bookingId);
- if (tradeDirection === 'EXPORT') {
- if (!train.arrivedAt) {
- throw new BadRequestException(
- 'The train has not arrived at Djibouti yet — gate pass can be granted after arrival.',
- );
- }
- for (const code of GlOperationsService.EXPORT_ARRIVAL_CHAIN) {
- if (byCode.get(code)?.status === 'PENDING') {
- await this.milestoneService.completeForBooking(bookingId, code, userId);
- }
- }
- } else if (!train.wagonAllocated) {
- throw new BadRequestException(
- 'Wagons must be allocated before the gate pass can be granted.',
- );
- }
-
- const at = gatepassAt?.trim() || new Date().toISOString();
- await this.milestoneService.completeWithMetadataForBooking(
- bookingId,
- 'GATEPASS_GRANTED',
- { gatepassAt: at },
- userId,
- );
- return { bookingId, gatepassAt: at };
- }
-
- /** Train schedules carrying ≥1 customs booking — the GL Djibouti gate-pass table. */
- async djSchedules(): Promise {
- const schedules = await this.dataSource.getRepository(TrainSchedule).find({
- relations: {
- scheduleBookings: { booking: true },
- originStation: true,
- destinationStation: true,
- },
- order: { scheduledDepartureDate: 'DESC' },
- });
-
- const withCustoms = schedules
- .filter((s) => s.status !== 'CANCELLED')
- .map((s) => ({
- schedule: s,
- customs: (s.scheduleBookings ?? [])
- .map((sb) => sb.booking)
- .filter((b): b is Booking => Boolean(b?.customsClearingEnabled)),
- }))
- .filter((s) => s.customs.length > 0);
-
- const bookingIds = withCustoms.flatMap((s) => s.customs.map((b) => b.id));
- const gatepassRows = bookingIds.length
- ? await this.dataSource.getRepository(ClearanceMilestone).find({
- where: { bookingId: In(bookingIds), milestoneCode: 'GATEPASS_GRANTED' },
- })
- : [];
- const gatepassByBooking = new Map(gatepassRows.map((m) => [m.bookingId, m]));
-
- return withCustoms.map(({ schedule, customs }) => {
- const freightTypes = [...new Set(customs.map((b) => b.freightType).filter(Boolean))];
- return {
- id: schedule.id,
- trainNumber: schedule.trainNumber ?? null,
- routeName: null,
- origin: schedule.originStation?.label ?? schedule.originStation?.code ?? null,
- destination:
- schedule.destinationStation?.label ?? schedule.destinationStation?.code ?? null,
- status: schedule.status,
- scheduledDepartureDate: schedule.scheduledDepartureDate
- ? new Date(schedule.scheduledDepartureDate).toISOString()
- : null,
- actualDepartureAt: schedule.actualDepartureAt
- ? new Date(schedule.actualDepartureAt).toISOString()
- : null,
- actualArrivalAt: schedule.actualArrivalAt
- ? new Date(schedule.actualArrivalAt).toISOString()
- : null,
- freightType:
- freightTypes.length === 1 ? (freightTypes[0] as string) : freightTypes.length ? 'MIXED' : null,
- customsBookings: customs.map((b) => {
- const m = gatepassByBooking.get(b.id);
- const granted = m?.status === 'COMPLETED';
- return {
- bookingId: b.id,
- reference: b.reference ?? b.id,
- tradeDirection: b.tradeDirection ?? 'IMPORT',
- contractId: b.contractId ?? null,
- gatepassGranted: granted,
- gatepassAt: granted
- ? (m?.metadata?.gatepassAt ??
- (m?.triggeredAt ? new Date(m.triggeredAt).toISOString() : null))
- : null,
- };
- }),
- };
- });
- }
-
- /**
- * One-click gate pass for every customs booking on a train schedule. Per-booking
- * guard failures are collected, not fatal. Import schedules also get the
- * schedule-level ImportDjiboutiOperation gate pass so loading unblocks.
- */
- async grantScheduleGatepass(
- scheduleId: string,
- gatepassAt?: string,
- userId?: string,
- ): Promise<{ granted: number; skipped: Array<{ bookingId: string; error: string }> }> {
- const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
- where: { id: scheduleId },
- relations: { scheduleBookings: { booking: true } },
- });
- if (!schedule) throw new NotFoundException(`Train schedule ${scheduleId} not found`);
-
- const customs = (schedule.scheduleBookings ?? [])
- .map((sb) => sb.booking)
- .filter((b): b is Booking => Boolean(b?.customsClearingEnabled));
- if (customs.length === 0) {
- throw new BadRequestException('No customs bookings ride this schedule.');
- }
-
- let granted = 0;
- const skipped: Array<{ bookingId: string; error: string }> = [];
- for (const booking of customs) {
- try {
- await this.grantGatepass(booking.id, gatepassAt, userId);
- granted += 1;
- } catch (e) {
- skipped.push({
- bookingId: booking.id,
- error: e instanceof Error ? e.message : 'Failed',
- });
- }
- }
-
- if (granted > 0 && customs.some((b) => (b.tradeDirection ?? 'IMPORT') === 'IMPORT')) {
- const opRepo = this.dataSource.getRepository(ImportDjiboutiOperation);
- let operation = await opRepo.findOne({ where: { trainScheduleId: scheduleId } });
- if (!operation) {
- operation = opRepo.create({ trainScheduleId: scheduleId });
- }
- if (!operation.gatepassGrantedAt) {
- operation.gatepassGrantedAt = gatepassAt ? new Date(gatepassAt) : new Date();
- await opRepo.save(operation);
- }
- }
-
- return { granted, skipped };
- }
/**
* GL Djibouti raises the post-offload final invoice (export): manual amount +
diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx
index d7cd9207b..b13e38961 100644
--- a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx
@@ -14,7 +14,7 @@ import {
Text,
Textarea,
} from "@mantine/core";
-import { DateInput, DateTimePicker } from "@mantine/dates";
+import { DateInput } from "@mantine/dates";
import {
AlertTriangle,
CheckCircle2,
@@ -397,15 +397,10 @@ export function ExportClearanceStepper({
: }
>
-
+
void;
-}) {
- const [opened, setOpened] = useState(false);
- const [at, setAt] = useState(new Date());
- const [loading, setLoading] = useState(false);
+/**
+ * Gate pass status, read-only. Secured on the train schedule's "Save as
+ * Secured" action (train-scheduling-v2) — clearance no longer grants it directly.
+ */
+function GatepassStep({ clearance }: { clearance: ClearanceViewLike }) {
+ const scheduleId = clearance.train?.scheduleId ?? null;
if (clearance.gatepassGranted) {
return (
@@ -526,68 +513,21 @@ function GatepassStep({
done={false}
pendingLabel={
arrived
- ? "Train arrived — GL Djibouti can grant the gate pass."
+ ? "Train arrived — secure the gate pass on the train schedule."
: "Available once the train arrives at Djibouti."
}
doneLabel=""
/>
- {canAct && bookingId ? (
- <>
- }
- disabled={!arrived}
- onClick={() => {
- setAt(new Date());
- setOpened(true);
- }}
- >
- Grant gate pass
-
- setOpened(false)}
- title={Grant gate pass}
- radius="md"
- size="sm"
- >
-
- setAt(v ? new Date(v) : null)}
- required
- />
-
-
-
-
-
-
- >
+ {scheduleId ? (
+ }
+ >
+ Secure gate pass on train schedule
+
) : null}
);
diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx
index 82ac61382..d0b7f2c87 100644
--- a/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/contracts/PhasedClearanceActionPanel.tsx
@@ -4,7 +4,6 @@ import {
Badge,
Button,
Group,
- Modal,
NumberInput,
Paper,
SegmentedControl,
@@ -15,7 +14,6 @@ import {
Text,
TextInput,
} from "@mantine/core";
-import { DateTimePicker } from "@mantine/dates";
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
import {
TransitPermitMultiUpload,
@@ -536,17 +534,12 @@ export function PhasedClearanceActionPanel({
:
}
>
-
+
void;
-}) {
- const [opened, setOpened] = useState(false);
- const [at, setAt] = useState(new Date());
- const [loading, setLoading] = useState(false);
+/**
+ * Gate pass status, read-only. Secured on the train schedule's "Save as
+ * Secured" action (train-scheduling-v2) — clearance no longer grants it directly.
+ */
+function ImportGatepassStep({ clearance }: { clearance: ClearanceViewLike }) {
+ const scheduleId = clearance.train?.scheduleId ?? null;
if (clearance.gatepassGranted) {
return (
@@ -852,68 +836,21 @@ function ImportGatepassStep({
done={false}
pendingLabel={
wagonAllocated
- ? "Wagons allocated — GL Djibouti can grant the gate pass."
+ ? "Wagons allocated — secure the gate pass on the train schedule."
: "Available once wagons are allocated."
}
doneLabel=""
/>
- {canAct && bookingId ? (
- <>
- }
- disabled={!wagonAllocated}
- onClick={() => {
- setAt(new Date());
- setOpened(true);
- }}
- >
- Grant gate pass
-
- setOpened(false)}
- title={Grant gate pass}
- radius="md"
- size="sm"
- >
-
- setAt(v ? new Date(v) : null)}
- required
- />
-
-
-
-
-
-
- >
+ {scheduleId ? (
+ }
+ >
+ Secure gate pass on train schedule
+
) : null}
);
diff --git a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts
index 3275ef662..bd1c51e47 100644
--- a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts
+++ b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts
@@ -70,7 +70,6 @@ export const QUERY_KEYS = {
["contracts", "clearance-queue", region ?? "ET"] as const,
clearanceHistory: (region?: string) =>
["contracts", "clearance-history", region ?? "ET"] as const,
- djSchedules: ["contracts", "clearance-dj-schedules"] as const,
milestones: (id: string) => ["contracts", "milestones", id] as const,
capacity: (id: string) => ["contracts", "capacity", id] as const,
bookingMilestones: (bookingId: string) =>
diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts
index d0f09f508..c22427881 100644
--- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts
+++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts
@@ -232,11 +232,6 @@ export const URL_CONSTANTS = {
`/contracts/bookings/${bookingId}/t1-documents`,
BOOKING_T1_CLOSE: (bookingId: string) =>
`/contracts/bookings/${bookingId}/t1-close`,
- CLEARANCE_DJ_SCHEDULES: "/contracts/clearance/dj-schedules",
- CLEARANCE_SCHEDULE_GATEPASS: (scheduleId: string) =>
- `/contracts/clearance/schedules/${scheduleId}/gatepass`,
- BOOKING_GATEPASS: (bookingId: string) =>
- `/contracts/bookings/${bookingId}/gatepass`,
BOOKING_FINAL_INVOICE: (bookingId: string) =>
`/contracts/bookings/${bookingId}/final-invoice`,
BOOKING_FINAL_INVOICE_CONFIRM: (bookingId: string) =>
diff --git a/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts b/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts
index 56229415f..04a4720aa 100644
--- a/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts
+++ b/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts
@@ -68,15 +68,6 @@ export function useDjClearanceQueue(enabled = true) {
});
}
-/** Train schedules carrying customs bookings — GL DJ gate-pass table. */
-export function useDjClearanceSchedules(enabled = true) {
- return useQuery({
- queryKey: QUERY_KEYS.CONTRACTS.djSchedules,
- queryFn: () => contractsService.getDjClearanceSchedules(),
- enabled,
- });
-}
-
/** Path A self-clearance queue (Operations reviews non-customs contracts). */
export function useOpsClearanceQueue(enabled = true) {
return useQuery({
diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx
index 3957fb7a5..0b7456851 100644
--- a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx
@@ -1,326 +1,65 @@
-import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
-import {
- Badge,
- Button,
- Card,
- Group,
- Loader,
- Modal,
- Stack,
- Tabs,
- Text,
-} from "@mantine/core";
-import { DateTimePicker } from "@mantine/dates";
-import { ChevronRight, Ship, Train, Truck } from "lucide-react";
-import { DataTable, type ColumnDef } from "@edr/ui-common";
-import type { Freight } from "@edr/types";
-import toast from "react-hot-toast";
+import { Badge, Card, Group, Loader, Stack, Text } from "@mantine/core";
+import { ChevronRight, Ship } from "lucide-react";
import { PageContainer } from "@/components/page/PageContainer";
import { PageHeader } from "@/components/page/PageHeader";
-import {
- useDjClearanceQueue,
- useDjClearanceSchedules,
-} from "@/hooks/contracts/useContracts";
-import { contractsService } from "@/services/contracts.service";
+import { useDjClearanceQueue } from "@/hooks/contracts/useContracts";
export default function GlDjiboutiClearanceListPage() {
const navigate = useNavigate();
const { data: contractQueue, isLoading: contractsLoading } = useDjClearanceQueue();
- const schedulesQuery = useDjClearanceSchedules();
const contractItems = contractQueue?.items ?? [];
- const scheduleItems = schedulesQuery.data ?? [];
-
- const [gatepassTarget, setGatepassTarget] =
- useState(null);
- const [gatepassAt, setGatepassAt] = useState(new Date());
- const [granting, setGranting] = useState(false);
-
- const columns = useMemo[]>(
- () => [
- {
- header: "Train",
- accessorKey: "trainNumber",
- cell: ({ row }) => (
-
- {row.original.trainNumber ?? "—"}
-
- ),
- },
- {
- header: "Route",
- id: "route",
- cell: ({ row }) => (
-
- {row.original.origin ?? "—"} → {row.original.destination ?? "—"}
-
- ),
- },
- {
- header: "Scheduled departure",
- id: "scheduled",
- cell: ({ row }) => (
-
- {row.original.scheduledDepartureDate
- ? new Date(row.original.scheduledDepartureDate).toLocaleDateString()
- : "—"}
-
- ),
- },
- {
- header: "Departed",
- id: "departed",
- cell: ({ row }) => (
-
- {row.original.actualDepartureAt
- ? new Date(row.original.actualDepartureAt).toLocaleString()
- : "—"}
-
- ),
- },
- {
- header: "Arrived",
- id: "arrived",
- cell: ({ row }) => (
-
- {row.original.actualArrivalAt
- ? new Date(row.original.actualArrivalAt).toLocaleString()
- : "—"}
-
- ),
- },
- {
- header: "Status",
- accessorKey: "status",
- cell: ({ row }) => (
-
- {row.original.status}
-
- ),
- },
- {
- header: "Customs bookings",
- id: "customs",
- cell: ({ row }) => {
- const bookings = row.original.customsBookings;
- const directions = [...new Set(bookings.map((b) => b.tradeDirection))];
- return (
-
-
- {bookings.length}
-
- {directions.map((d) => (
-
- {d}
-
- ))}
-
- );
- },
- },
- {
- header: "Gate pass",
- id: "gatepass",
- cell: ({ row }) => {
- const bookings = row.original.customsBookings;
- const allGranted =
- bookings.length > 0 && bookings.every((b) => b.gatepassGranted);
- const grantedAt = bookings.find((b) => b.gatepassAt)?.gatepassAt ?? null;
- if (allGranted) {
- return (
-
- Granted{grantedAt ? ` · ${new Date(grantedAt).toLocaleString()}` : ""}
-
- );
- }
- return (
- }
- onClick={(e) => {
- e.stopPropagation();
- setGatepassAt(new Date());
- setGatepassTarget(row.original);
- }}
- >
- Gate pass
-
- );
- },
- },
- ],
- [],
- );
return (
-
-
- Contracts ({contractItems.length})
- }>
- Schedules ({scheduleItems.length})
-
-
-
-
- {contractsLoading ? (
-
-
-
- ) : (
-
- {contractItems.length === 0 ? (
-
- No Djibouti customs contracts yet.
-
- ) : (
- contractItems.map((c) => (
- navigate(`/dashboard/gl-djibouti/clearance/${c.id}`)}
- >
-
-
-
-
- {c.reference}
-
- {c.tradeDirection} · {c.status}
-
-
-
-
-
- Contract
-
-
-
-
-
- ))
- )}
-
- )}
-
-
-
- void schedulesQuery.refetch(),
- }
- : undefined
- }
- emptyMessage="No train schedules carry customs bookings yet."
- />
-
-
-
- setGatepassTarget(null)}
- title={
-
-
-
- Gate pass — train {gatepassTarget?.trainNumber ?? ""}
+ {contractsLoading ? (
+
+
+
+ ) : (
+
+ {contractItems.length === 0 ? (
+
+ No Djibouti customs contracts yet.
-
- }
- radius="md"
- size="sm"
- >
-
-
- Grants the gate pass for all{" "}
- {gatepassTarget?.customsBookings.length ?? 0} customs booking
- {(gatepassTarget?.customsBookings.length ?? 0) === 1 ? "" : "s"} on this
- train.
-
- setGatepassAt(v ? new Date(v) : null)}
- required
- />
-
-
- }
- onClick={async () => {
- if (!gatepassTarget) return;
- setGranting(true);
- try {
- const result = await contractsService.grantScheduleGatepass(
- gatepassTarget.id,
- (gatepassAt ?? new Date()).toISOString(),
- );
- if (result.skipped.length > 0) {
- toast.error(
- `${result.granted} granted, ${result.skipped.length} skipped: ${result.skipped[0]?.error ?? ""}`,
- );
- } else {
- toast.success(
- `Gate pass granted for ${result.granted} booking${result.granted === 1 ? "" : "s"}`,
- );
- }
- setGatepassTarget(null);
- void schedulesQuery.refetch();
- } catch (e) {
- toast.error(e instanceof Error ? e.message : "Failed");
- } finally {
- setGranting(false);
- }
- }}
- >
- Grant gate pass
-
-
+ ) : (
+ contractItems.map((c) => (
+ navigate(`/dashboard/gl-djibouti/clearance/${c.id}`)}
+ >
+
+
+
+
+ {c.reference}
+
+ {c.tradeDirection} · {c.status}
+
+
+
+
+
+ Contract
+
+
+
+
+
+ ))
+ )}
-
+ )}
);
}
-
-function statusColor(status: string): string {
- switch (status) {
- case "SCHEDULED":
- return "blue";
- case "DISPATCHED":
- return "yellow";
- case "ARRIVED":
- return "edr-green";
- default:
- return "gray";
- }
-}
diff --git a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts
index 8860df62a..7ad051ce7 100644
--- a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts
+++ b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts
@@ -391,35 +391,6 @@ export const contractsService = {
return unwrap(response.data) as Freight.ClearanceT1State;
},
- /** Train schedules carrying customs bookings — GL DJ gate-pass table. */
- getDjClearanceSchedules: async (): Promise => {
- const response = await client.get(C.CLEARANCE_DJ_SCHEDULES);
- return unwrap(response.data) as Freight.DjClearanceSchedule[];
- },
-
- /** Gate pass for every customs booking on a train schedule (captures time). */
- grantScheduleGatepass: async (
- scheduleId: string,
- gatepassAt?: string,
- ): Promise<{ granted: number; skipped: Array<{ bookingId: string; error: string }> }> => {
- const response = await client.post(C.CLEARANCE_SCHEDULE_GATEPASS(scheduleId), {
- gatepassAt,
- });
- return unwrap(response.data) as {
- granted: number;
- skipped: Array<{ bookingId: string; error: string }>;
- };
- },
-
- /** Gate pass for a single customs booking (captures time). */
- grantGatepass: async (
- bookingId: string,
- gatepassAt?: string,
- ): Promise<{ bookingId: string; gatepassAt: string }> => {
- const response = await client.post(C.BOOKING_GATEPASS(bookingId), { gatepassAt });
- return unwrap(response.data) as { bookingId: string; gatepassAt: string };
- },
-
/** GL DJ raises the post-offload final invoice (amount + invoice document). */
sendFinalInvoice: async (
bookingId: string,
diff --git a/packages/types/src/freight/contracts.ts b/packages/types/src/freight/contracts.ts
index fe653573d..829ae3217 100644
--- a/packages/types/src/freight/contracts.ts
+++ b/packages/types/src/freight/contracts.ts
@@ -265,6 +265,7 @@ export interface ClearanceT1State {
/** Train link state for the booking tied to a customs clearance flow. */
export interface ClearanceTrainState {
+ scheduleId: string | null;
wagonAllocated: boolean;
departedAt: string | null;
arrivedAt: string | null;
@@ -305,31 +306,6 @@ export interface ClearanceSecondDuty {
paid: boolean;
}
-/** A customs booking riding a train schedule, as shown on the GL DJ schedules tab. */
-export interface DjClearanceScheduleBooking {
- bookingId: string;
- reference: string;
- tradeDirection: string;
- contractId: string | null;
- gatepassGranted: boolean;
- gatepassAt: string | null;
-}
-
-/** Train schedule row for the GL Djibouti gate-pass table. */
-export interface DjClearanceSchedule {
- id: string;
- trainNumber: string | null;
- routeName: string | null;
- origin: string | null;
- destination: string | null;
- status: string;
- scheduledDepartureDate: string | null;
- actualDepartureAt: string | null;
- actualArrivalAt: string | null;
- freightType: string | null;
- customsBookings: DjClearanceScheduleBooking[];
-}
-
export interface ContractClearanceView {
contractId: string;
/** Overall contract status (e.g. CLEARANCE_UNDER_REVIEW). */
From de87845f2212221aeea904936d8360b2fb9b4fd4 Mon Sep 17 00:00:00 2001
From: Nathnael
Date: Sat, 4 Jul 2026 08:22:18 +0000
Subject: [PATCH 07/17] style: auth clean up
---
.../backoffice/public/assets/edr_image.jpg | Bin 0 -> 161230 bytes
.../backoffice/public/assets/edr_image.png | Bin 0 -> 881851 bytes
.../src/components/auth/AuthShell.tsx | 147 +++++++++--------
.../portal/public/assets/edr_image.jpg | Bin 0 -> 161230 bytes
.../portal/public/assets/edr_image.png | Bin 0 -> 881851 bytes
.../portal/src/components/auth/AuthShell.tsx | 149 +++++++++---------
.../portal/src/pages/accounts/LoginPage.tsx | 9 +-
.../portal/src/pages/accounts/SignupPage.tsx | 60 +++++--
8 files changed, 203 insertions(+), 162 deletions(-)
create mode 100644 apps/edr-freight-web/backoffice/public/assets/edr_image.jpg
create mode 100644 apps/edr-freight-web/backoffice/public/assets/edr_image.png
create mode 100644 apps/edr-freight-web/portal/public/assets/edr_image.jpg
create mode 100644 apps/edr-freight-web/portal/public/assets/edr_image.png
diff --git a/apps/edr-freight-web/backoffice/public/assets/edr_image.jpg b/apps/edr-freight-web/backoffice/public/assets/edr_image.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b89941eea96f0a16527471640f4aac8f2636ff55
GIT binary patch
literal 161230
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