From 2d51787383435b2143c787f928201f9b687840b0 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Tue, 23 Jun 2026 10:10:55 +0300 Subject: [PATCH] Backoffice updates, boarding pass, alternative schedule search and more --- .../20260605195213_init/migration.sql | 14 +- .../migration.sql | 3 +- .../migration.sql | 2 + .../migration.sql | 39 +- .../migration.sql | 172 +++--- .../migration.sql | 12 +- .../migration.sql | 6 +- .../migration.sql | 14 + apps/edr-passenger-api/prisma/schema.prisma | 9 +- .../src/modules/bookings/bookings.service.ts | 58 +- .../modules/bookings/guest-booking.service.ts | 23 +- .../modules/passengers/passengers.service.ts | 6 +- .../src/modules/payments/payments.service.ts | 146 +++-- .../src/modules/search/search.service.ts | 93 +++- .../src/modules/seats/seats.service.ts | 177 +++--- .../src/modules/stations/stations.dto.ts | 4 +- .../src/modules/stations/stations.service.ts | 5 +- .../src/modules/tickets/tickets.service.ts | 5 +- .../backoffice/src/app/bookings/page.tsx | 493 +++++++---------- .../backoffice/src/app/passengers/page.tsx | 522 ++++++++---------- .../backoffice/src/app/payments/page.tsx | 27 +- .../backoffice/src/app/reports/page.tsx | 6 +- .../backoffice/src/app/schedules/page.tsx | 4 +- .../backoffice/src/app/seats/page.tsx | 5 +- .../backoffice/src/app/stations/page.tsx | 26 +- .../backoffice/src/app/tickets/page.tsx | 304 +++++++--- .../backoffice/src/lib/utils.ts | 9 +- .../backoffice/src/types/edr.ts | 6 +- .../backoffice/src/types/index.ts | 2 +- .../portal/src/app/booking/review/page.tsx | 81 +-- .../portal/src/app/booking/seats/page.tsx | 22 +- .../portal/src/lib/booking-store.ts | 2 + .../providers/cac-bank/cac-bank.provider.ts | 2 +- .../src/providers/card/card.provider.ts | 2 +- .../providers/cbe-birr/cbe-birr.provider.ts | 2 +- .../src/providers/dmoney/dmoney.provider.ts | 2 +- .../src/providers/waafi/waafi.provider.ts | 2 +- 37 files changed, 1267 insertions(+), 1040 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql create mode 100644 apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql index 0f8484179..4ae48ea16 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql @@ -20,7 +20,7 @@ CREATE TYPE "IdDocumentType" AS ENUM ('NATIONAL_ID', 'PASSPORT', 'DRIVING_LICENS CREATE TYPE "Currency" AS ENUM ('ETB', 'DJF', 'USD'); -- CreateEnum -CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'COMPLETED', 'NO_SHOW', 'REFUNDED'); +CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'BOARDED', 'NO_SHOW', 'REFUNDED'); -- CreateEnum CREATE TYPE "PaymentRegion" AS ENUM ('ETHIOPIA', 'DJIBOUTI', 'INTERNATIONAL', 'GLOBAL'); @@ -76,7 +76,9 @@ CREATE TABLE "SeatClass" ( "coachTypeId" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT, - "baseFareMinor" INTEGER NOT NULL, + "baseFareMinor" INTEGER NOT NULL DEFAULT 0, + "premiumMinor" INTEGER NOT NULL DEFAULT 0, + "insuranceFeeMinor" INTEGER NOT NULL DEFAULT 0, "isActive" BOOLEAN NOT NULL DEFAULT true, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -94,6 +96,8 @@ CREATE TABLE "User" ( "role" "UserRole" NOT NULL DEFAULT 'PASSENGER', "nationality" TEXT, "nationalityCode" TEXT, + "gender" TEXT, + "dateOfBirth" TIMESTAMP(3), "passportNumber" TEXT, "nationalId" TEXT, "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0, @@ -155,10 +159,11 @@ CREATE TABLE "Station" ( "name" TEXT NOT NULL, "city" TEXT NOT NULL, "countryCode" TEXT, + "sequence" INTEGER NOT NULL DEFAULT 0, "isOperational" BOOLEAN NOT NULL DEFAULT true, "timezone" TEXT NOT NULL DEFAULT 'Africa/Addis_Ababa', - "lat" DECIMAL(9,6) NOT NULL, - "lng" DECIMAL(9,6) NOT NULL, + "lat" DECIMAL(9,6), + "lng" DECIMAL(9,6), CONSTRAINT "Station_pkey" PRIMARY KEY ("id") ); @@ -234,6 +239,7 @@ CREATE TABLE "Coach" ( "number" TEXT NOT NULL, "arrangement" TEXT NOT NULL DEFAULT '2+2', "capacity" INTEGER NOT NULL DEFAULT 0, + "sequence" INTEGER NOT NULL DEFAULT 0, "status" TEXT NOT NULL DEFAULT 'ACTIVE', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, diff --git a/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql index 7622faf86..577312395 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql @@ -140,8 +140,7 @@ ALTER TABLE "SeatClass" ALTER COLUMN "baseFareMinor" SET DEFAULT 0; -- AlterTable ALTER TABLE "Ticket" ALTER COLUMN "status" SET DEFAULT 'ACTIVE'; --- AlterTable -ALTER TABLE "User" ALTER COLUMN "gender" SET DATA TYPE TEXT; +-- gender column already TEXT from init migration -- CreateIndex CREATE INDEX "Booking_bookingType_idx" ON "Booking"("bookingType"); diff --git a/apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql new file mode 100644 index 000000000..7e7d9bd58 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql @@ -0,0 +1,2 @@ +-- Empty placeholder migration +SELECT 1; diff --git a/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql index 6c8da0d2c..1673a795b 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql @@ -1,36 +1,9 @@ --- Add sequence column to Station table if it doesn't exist -ALTER TABLE "passenger"."Station" ADD COLUMN IF NOT EXISTS "sequence" INTEGER NOT NULL DEFAULT 0; +CREATE INDEX IF NOT EXISTS "Station_sequence_idx" ON "Station"("sequence"); --- Add index on sequence for Station -CREATE INDEX IF NOT EXISTS "Station_sequence_idx" ON "passenger"."Station"("sequence"); - --- Add sequence column to Coach table if it doesn't exist -ALTER TABLE "passenger"."Coach" ADD COLUMN IF NOT EXISTS "sequence" INTEGER NOT NULL DEFAULT 0; - --- Add index on sequence for Coach -CREATE INDEX IF NOT EXISTS "Coach_sequence_idx" ON "passenger"."Coach"("sequence"); - --- Add missing columns to SeatClass if they don't exist -ALTER TABLE "passenger"."SeatClass" ADD COLUMN IF NOT EXISTS "premiumMinor" INTEGER NOT NULL DEFAULT 0; -ALTER TABLE "passenger"."SeatClass" ADD COLUMN IF NOT EXISTS "insuranceFeeMinor" INTEGER NOT NULL DEFAULT 0; - --- Add missing columns to User if they don't exist -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "gender" VARCHAR(255); -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "dateOfBirth" TIMESTAMP(3); -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "passportNumber" VARCHAR(255); -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "nationalId" VARCHAR(255); - --- Ensure Ticket has all required columns -ALTER TABLE "passenger"."Ticket" ADD COLUMN IF NOT EXISTS "validatedAt" TIMESTAMP(3); -ALTER TABLE "passenger"."Ticket" ADD COLUMN IF NOT EXISTS "boardedAt" TIMESTAMP(3); - --- Add missing columns to Booking if they don't exist -ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "bookingType" VARCHAR(255) NOT NULL DEFAULT 'ONE_WAY'; -ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "displayCurrency" VARCHAR(255); -ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "displayTotalMinor" INTEGER; +CREATE INDEX IF NOT EXISTS "Coach_sequence_idx" ON "Coach"("sequence"); -- Ensure all indexes exist -CREATE INDEX IF NOT EXISTS "Station_city_countryCode_idx" ON "passenger"."Station"("city", "countryCode"); -CREATE INDEX IF NOT EXISTS "Coach_coachTypeId_idx" ON "passenger"."Coach"("coachTypeId"); -CREATE INDEX IF NOT EXISTS "TrainSchedule_departureAt_originStationId_idx" ON "passenger"."TrainSchedule"("departureAt", "originStationId"); -CREATE INDEX IF NOT EXISTS "Booking_passengerId_status_idx" ON "passenger"."Booking"("passengerId", "status"); +CREATE INDEX IF NOT EXISTS "Station_city_countryCode_idx" ON "Station"("city", "countryCode"); +CREATE INDEX IF NOT EXISTS "Coach_coachTypeId_idx" ON "Coach"("coachTypeId"); +CREATE INDEX IF NOT EXISTS "TrainSchedule_departureAt_originStationId_idx" ON "TrainSchedule"("departureAt", "originStationId"); +CREATE INDEX IF NOT EXISTS "Booking_passengerId_status_idx" ON "Booking"("passengerId", "status"); diff --git a/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql index d047e5a0c..9f70a96b1 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql @@ -1,164 +1,164 @@ -- Add CASCADE delete to all foreign key constraints that are missing it -- TrainSchedule relations -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_trainId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_trainId_fkey" FOREIGN KEY ("trainId") REFERENCES "passenger"."Train"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_trainId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_trainId_fkey" FOREIGN KEY ("trainId") REFERENCES "Train"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_routeId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "passenger"."Route"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_routeId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_originStationId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_originStationId_fkey" FOREIGN KEY ("originStationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_originStationId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_originStationId_fkey" FOREIGN KEY ("originStationId") REFERENCES "Station"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_destinationStationId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_destinationStationId_fkey" FOREIGN KEY ("destinationStationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_destinationStationId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_destinationStationId_fkey" FOREIGN KEY ("destinationStationId") REFERENCES "Station"("id") ON DELETE CASCADE; -- Coach relation -ALTER TABLE "passenger"."Coach" DROP CONSTRAINT IF EXISTS "Coach_coachTypeId_fkey"; -ALTER TABLE "passenger"."Coach" ADD CONSTRAINT "Coach_coachTypeId_fkey" FOREIGN KEY ("coachTypeId") REFERENCES "passenger"."CoachType"("id") ON DELETE CASCADE; +ALTER TABLE "Coach" DROP CONSTRAINT IF EXISTS "Coach_coachTypeId_fkey"; +ALTER TABLE "Coach" ADD CONSTRAINT "Coach_coachTypeId_fkey" FOREIGN KEY ("coachTypeId") REFERENCES "CoachType"("id") ON DELETE CASCADE; -- CoachAssignment relations -ALTER TABLE "passenger"."CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_scheduleId_fkey"; -ALTER TABLE "passenger"."CoachAssignment" ADD CONSTRAINT "CoachAssignment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_scheduleId_fkey"; +ALTER TABLE "CoachAssignment" ADD CONSTRAINT "CoachAssignment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_coachId_fkey"; -ALTER TABLE "passenger"."CoachAssignment" ADD CONSTRAINT "CoachAssignment_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "passenger"."Coach"("id") ON DELETE CASCADE; +ALTER TABLE "CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_coachId_fkey"; +ALTER TABLE "CoachAssignment" ADD CONSTRAINT "CoachAssignment_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "Coach"("id") ON DELETE CASCADE; -- Booking relations -ALTER TABLE "passenger"."Booking" DROP CONSTRAINT IF EXISTS "Booking_passengerId_fkey"; -ALTER TABLE "passenger"."Booking" ADD CONSTRAINT "Booking_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE; +ALTER TABLE "Booking" DROP CONSTRAINT IF EXISTS "Booking_passengerId_fkey"; +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."Booking" DROP CONSTRAINT IF EXISTS "Booking_scheduleId_fkey"; -ALTER TABLE "passenger"."Booking" ADD CONSTRAINT "Booking_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "Booking" DROP CONSTRAINT IF EXISTS "Booking_scheduleId_fkey"; +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- BookingSeat relations -ALTER TABLE "passenger"."BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_bookingId_fkey"; -ALTER TABLE "passenger"."BookingSeat" ADD CONSTRAINT "BookingSeat_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_bookingId_fkey"; +ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_seatId_fkey"; -ALTER TABLE "passenger"."BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE; +ALTER TABLE "BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_seatId_fkey"; +ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE; -- PaymentIntent -ALTER TABLE "passenger"."PaymentIntent" DROP CONSTRAINT IF EXISTS "PaymentIntent_bookingId_fkey"; -ALTER TABLE "passenger"."PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "PaymentIntent" DROP CONSTRAINT IF EXISTS "PaymentIntent_bookingId_fkey"; +ALTER TABLE "PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- PaymentRefund -ALTER TABLE "passenger"."PaymentRefund" DROP CONSTRAINT IF EXISTS "PaymentRefund_paymentIntentId_fkey"; -ALTER TABLE "passenger"."PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "passenger"."PaymentIntent"("id") ON DELETE CASCADE; +ALTER TABLE "PaymentRefund" DROP CONSTRAINT IF EXISTS "PaymentRefund_paymentIntentId_fkey"; +ALTER TABLE "PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "PaymentIntent"("id") ON DELETE CASCADE; -- Ticket -ALTER TABLE "passenger"."Ticket" DROP CONSTRAINT IF EXISTS "Ticket_bookingId_fkey"; -ALTER TABLE "passenger"."Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "Ticket" DROP CONSTRAINT IF EXISTS "Ticket_bookingId_fkey"; +ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- TicketSeat -ALTER TABLE "passenger"."TicketSeat" DROP CONSTRAINT IF EXISTS "TicketSeat_seatId_fkey"; -ALTER TABLE "passenger"."TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE; +ALTER TABLE "TicketSeat" DROP CONSTRAINT IF EXISTS "TicketSeat_seatId_fkey"; +ALTER TABLE "TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE; -- WalletLedgerEntry -ALTER TABLE "passenger"."WalletLedgerEntry" DROP CONSTRAINT IF EXISTS "WalletLedgerEntry_walletId_fkey"; -ALTER TABLE "passenger"."WalletLedgerEntry" ADD CONSTRAINT "WalletLedgerEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "passenger"."WalletAccount"("id") ON DELETE CASCADE; +ALTER TABLE "WalletLedgerEntry" DROP CONSTRAINT IF EXISTS "WalletLedgerEntry_walletId_fkey"; +ALTER TABLE "WalletLedgerEntry" ADD CONSTRAINT "WalletLedgerEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "WalletAccount"("id") ON DELETE CASCADE; -- Notification -ALTER TABLE "passenger"."Notification" DROP CONSTRAINT IF EXISTS "Notification_passengerId_fkey"; -ALTER TABLE "passenger"."Notification" ADD CONSTRAINT "Notification_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE; +ALTER TABLE "Notification" DROP CONSTRAINT IF EXISTS "Notification_passengerId_fkey"; +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE; -- MenuItem -ALTER TABLE "passenger"."MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_scheduleId_fkey"; -ALTER TABLE "passenger"."MenuItem" ADD CONSTRAINT "MenuItem_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_scheduleId_fkey"; +ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_categoryId_fkey"; -ALTER TABLE "passenger"."MenuItem" ADD CONSTRAINT "MenuItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "passenger"."MenuCategory"("id") ON DELETE CASCADE; +ALTER TABLE "MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_categoryId_fkey"; +ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "MenuCategory"("id") ON DELETE CASCADE; -- FoodOrder -ALTER TABLE "passenger"."FoodOrder" DROP CONSTRAINT IF EXISTS "FoodOrder_bookingId_fkey"; -ALTER TABLE "passenger"."FoodOrder" ADD CONSTRAINT "FoodOrder_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "FoodOrder" DROP CONSTRAINT IF EXISTS "FoodOrder_bookingId_fkey"; +ALTER TABLE "FoodOrder" ADD CONSTRAINT "FoodOrder_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- FoodOrderItem -ALTER TABLE "passenger"."FoodOrderItem" DROP CONSTRAINT IF EXISTS "FoodOrderItem_orderId_fkey"; -ALTER TABLE "passenger"."FoodOrderItem" ADD CONSTRAINT "FoodOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "passenger"."FoodOrder"("id") ON DELETE CASCADE; +ALTER TABLE "FoodOrderItem" DROP CONSTRAINT IF EXISTS "FoodOrderItem_orderId_fkey"; +ALTER TABLE "FoodOrderItem" ADD CONSTRAINT "FoodOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "FoodOrder"("id") ON DELETE CASCADE; -- FaqArticle -ALTER TABLE "passenger"."FaqArticle" DROP CONSTRAINT IF EXISTS "FaqArticle_categoryId_fkey"; -ALTER TABLE "passenger"."FaqArticle" ADD CONSTRAINT "FaqArticle_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "passenger"."FaqCategory"("id") ON DELETE CASCADE; +ALTER TABLE "FaqArticle" DROP CONSTRAINT IF EXISTS "FaqArticle_categoryId_fkey"; +ALTER TABLE "FaqArticle" ADD CONSTRAINT "FaqArticle_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "FaqCategory"("id") ON DELETE CASCADE; -- SupportMessage -ALTER TABLE "passenger"."SupportMessage" DROP CONSTRAINT IF EXISTS "SupportMessage_conversationId_fkey"; -ALTER TABLE "passenger"."SupportMessage" ADD CONSTRAINT "SupportMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "passenger"."SupportConversation"("id") ON DELETE CASCADE; +ALTER TABLE "SupportMessage" DROP CONSTRAINT IF EXISTS "SupportMessage_conversationId_fkey"; +ALTER TABLE "SupportMessage" ADD CONSTRAINT "SupportMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "SupportConversation"("id") ON DELETE CASCADE; -- TripStopTime -ALTER TABLE "passenger"."TripStopTime" DROP CONSTRAINT IF EXISTS "TripStopTime_scheduleId_fkey"; -ALTER TABLE "passenger"."TripStopTime" ADD CONSTRAINT "TripStopTime_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "TripStopTime" DROP CONSTRAINT IF EXISTS "TripStopTime_scheduleId_fkey"; +ALTER TABLE "TripStopTime" ADD CONSTRAINT "TripStopTime_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- TripLiveStatus -ALTER TABLE "passenger"."TripLiveStatus" DROP CONSTRAINT IF EXISTS "TripLiveStatus_scheduleId_fkey"; -ALTER TABLE "passenger"."TripLiveStatus" ADD CONSTRAINT "TripLiveStatus_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "TripLiveStatus" DROP CONSTRAINT IF EXISTS "TripLiveStatus_scheduleId_fkey"; +ALTER TABLE "TripLiveStatus" ADD CONSTRAINT "TripLiveStatus_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- JourneySegment -ALTER TABLE "passenger"."JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey"; -ALTER TABLE "passenger"."JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "passenger"."Journey"("id") ON DELETE CASCADE; +ALTER TABLE "JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey"; +ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "Journey"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_scheduleId_fkey"; -ALTER TABLE "passenger"."JourneySegment" ADD CONSTRAINT "JourneySegment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_scheduleId_fkey"; +ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- AgentBooking -ALTER TABLE "passenger"."AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_agentId_fkey"; -ALTER TABLE "passenger"."AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE; +ALTER TABLE "AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_agentId_fkey"; +ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_bookingId_fkey"; -ALTER TABLE "passenger"."AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_bookingId_fkey"; +ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- AgentShift -ALTER TABLE "passenger"."AgentShift" DROP CONSTRAINT IF EXISTS "AgentShift_agentId_fkey"; -ALTER TABLE "passenger"."AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE; +ALTER TABLE "AgentShift" DROP CONSTRAINT IF EXISTS "AgentShift_agentId_fkey"; +ALTER TABLE "AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE; -- AgentCommission -ALTER TABLE "passenger"."AgentCommission" DROP CONSTRAINT IF EXISTS "AgentCommission_agentId_fkey"; -ALTER TABLE "passenger"."AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE; +ALTER TABLE "AgentCommission" DROP CONSTRAINT IF EXISTS "AgentCommission_agentId_fkey"; +ALTER TABLE "AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE; -- BookingModification -ALTER TABLE "passenger"."BookingModification" DROP CONSTRAINT IF EXISTS "BookingModification_bookingId_fkey"; -ALTER TABLE "passenger"."BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BookingModification" DROP CONSTRAINT IF EXISTS "BookingModification_bookingId_fkey"; +ALTER TABLE "BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- BookingCancellation -ALTER TABLE "passenger"."BookingCancellation" DROP CONSTRAINT IF EXISTS "BookingCancellation_bookingId_fkey"; -ALTER TABLE "passenger"."BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BookingCancellation" DROP CONSTRAINT IF EXISTS "BookingCancellation_bookingId_fkey"; +ALTER TABLE "BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- GateValidationLog -ALTER TABLE "passenger"."GateValidationLog" DROP CONSTRAINT IF EXISTS "GateValidationLog_ticketId_fkey"; -ALTER TABLE "passenger"."GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "passenger"."Ticket"("id") ON DELETE CASCADE; +ALTER TABLE "GateValidationLog" DROP CONSTRAINT IF EXISTS "GateValidationLog_ticketId_fkey"; +ALTER TABLE "GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE CASCADE; -- BaggageBooking -ALTER TABLE "passenger"."BaggageBooking" DROP CONSTRAINT IF EXISTS "BaggageBooking_bookingId_fkey"; -ALTER TABLE "passenger"."BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BaggageBooking" DROP CONSTRAINT IF EXISTS "BaggageBooking_bookingId_fkey"; +ALTER TABLE "BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- RouteFareRule -ALTER TABLE "passenger"."RouteFareRule" DROP CONSTRAINT IF EXISTS "RouteFareRule_seatClassId_fkey"; -ALTER TABLE "passenger"."RouteFareRule" ADD CONSTRAINT "RouteFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE; +ALTER TABLE "RouteFareRule" DROP CONSTRAINT IF EXISTS "RouteFareRule_seatClassId_fkey"; +ALTER TABLE "RouteFareRule" ADD CONSTRAINT "RouteFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE; -- SegmentFareRule -ALTER TABLE "passenger"."SegmentFareRule" DROP CONSTRAINT IF EXISTS "SegmentFareRule_seatClassId_fkey"; -ALTER TABLE "passenger"."SegmentFareRule" ADD CONSTRAINT "SegmentFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE; +ALTER TABLE "SegmentFareRule" DROP CONSTRAINT IF EXISTS "SegmentFareRule_seatClassId_fkey"; +ALTER TABLE "SegmentFareRule" ADD CONSTRAINT "SegmentFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE; -- StationCrowdSignal -ALTER TABLE "passenger"."StationCrowdSignal" DROP CONSTRAINT IF EXISTS "StationCrowdSignal_stationId_fkey"; -ALTER TABLE "passenger"."StationCrowdSignal" ADD CONSTRAINT "StationCrowdSignal_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE; +ALTER TABLE "StationCrowdSignal" DROP CONSTRAINT IF EXISTS "StationCrowdSignal_stationId_fkey"; +ALTER TABLE "StationCrowdSignal" ADD CONSTRAINT "StationCrowdSignal_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "Station"("id") ON DELETE CASCADE; -- SeatBlock -ALTER TABLE "passenger"."SeatBlock" DROP CONSTRAINT IF EXISTS "SeatBlock_seatId_fkey"; -ALTER TABLE "passenger"."SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE; +ALTER TABLE "SeatBlock" DROP CONSTRAINT IF EXISTS "SeatBlock_seatId_fkey"; +ALTER TABLE "SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE; -- SavedRoute -ALTER TABLE "passenger"."SavedRoute" DROP CONSTRAINT IF EXISTS "SavedRoute_passengerId_fkey"; -ALTER TABLE "passenger"."SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE; +ALTER TABLE "SavedRoute" DROP CONSTRAINT IF EXISTS "SavedRoute_passengerId_fkey"; +ALTER TABLE "SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE; -- LoyaltyLedgerEntry -ALTER TABLE "passenger"."LoyaltyLedgerEntry" DROP CONSTRAINT IF EXISTS "LoyaltyLedgerEntry_accountId_fkey"; -ALTER TABLE "passenger"."LoyaltyLedgerEntry" ADD CONSTRAINT "LoyaltyLedgerEntry_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "passenger"."LoyaltyAccount"("id") ON DELETE CASCADE; +ALTER TABLE "LoyaltyLedgerEntry" DROP CONSTRAINT IF EXISTS "LoyaltyLedgerEntry_accountId_fkey"; +ALTER TABLE "LoyaltyLedgerEntry" ADD CONSTRAINT "LoyaltyLedgerEntry_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "LoyaltyAccount"("id") ON DELETE CASCADE; -- LoyaltyReward -ALTER TABLE "passenger"."LoyaltyReward" DROP CONSTRAINT IF EXISTS "LoyaltyReward_accountId_fkey"; -ALTER TABLE "passenger"."LoyaltyReward" ADD CONSTRAINT "LoyaltyReward_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "passenger"."LoyaltyAccount"("id") ON DELETE CASCADE; +ALTER TABLE "LoyaltyReward" DROP CONSTRAINT IF EXISTS "LoyaltyReward_accountId_fkey"; +ALTER TABLE "LoyaltyReward" ADD CONSTRAINT "LoyaltyReward_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "LoyaltyAccount"("id") ON DELETE CASCADE; -- FareRule -ALTER TABLE "passenger"."FareRule" DROP CONSTRAINT IF EXISTS "FareRule_seatClassId_fkey"; -ALTER TABLE "passenger"."FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE; +ALTER TABLE "FareRule" DROP CONSTRAINT IF EXISTS "FareRule_seatClassId_fkey"; +ALTER TABLE "FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE; diff --git a/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql index e93fb8320..bc6e6c2a2 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql @@ -1,18 +1,18 @@ -- CreateEnum -CREATE TYPE "passenger"."ReturnLegStatus" AS ENUM ('NOT_APPLICABLE', 'BOTH_USED', 'OUTBOUND_ONLY', 'INBOUND_ONLY', 'NEITHER_USED'); +CREATE TYPE "ReturnLegStatus" AS ENUM ('NOT_APPLICABLE', 'BOTH_USED', 'OUTBOUND_ONLY', 'INBOUND_ONLY', 'NEITHER_USED'); -- AlterTable: add return leg tracking columns to Booking -ALTER TABLE "passenger"."Booking" - ADD COLUMN "returnLegStatus" "passenger"."ReturnLegStatus" NOT NULL DEFAULT 'NOT_APPLICABLE', +ALTER TABLE "Booking" + ADD COLUMN "returnLegStatus" "ReturnLegStatus" NOT NULL DEFAULT 'NOT_APPLICABLE', ADD COLUMN "outboundBoardedAt" TIMESTAMP(3), ADD COLUMN "returnBoardedAt" TIMESTAMP(3); -- Set NEITHER_USED for existing confirmed round-trip bookings -UPDATE "passenger"."Booking" +UPDATE "Booking" SET "returnLegStatus" = 'NEITHER_USED' WHERE "bookingType" = 'ROUND_TRIP' - AND "status" IN ('CONFIRMED', 'COMPLETED'); + AND "status" IN ('CONFIRMED', 'BOARDED'); -- AlterTable: add leg column to GateValidationLog -ALTER TABLE "passenger"."GateValidationLog" +ALTER TABLE "GateValidationLog" ADD COLUMN "leg" TEXT; diff --git a/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql index f252642f6..b0a5bc0b4 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql @@ -1,7 +1,7 @@ -- Create passenger schema if it doesn't exist CREATE SCHEMA IF NOT EXISTS passenger; --- Move all enums from public to passenger schema +-- Move enums from public to passenger schema (only if they exist in public) DO $$ DECLARE e text; @@ -13,9 +13,10 @@ BEGIN LOOP EXECUTE format('ALTER TYPE public.%I SET SCHEMA passenger', e); END LOOP; +EXCEPTION WHEN others THEN NULL; END $$; --- Move all tables from public to passenger schema +-- Move tables from public to passenger schema (only if they exist in public) DO $$ DECLARE t text; @@ -26,6 +27,7 @@ BEGIN LOOP EXECUTE format('ALTER TABLE public.%I SET SCHEMA passenger', t); END LOOP; +EXCEPTION WHEN others THEN NULL; END $$; -- Add missing columns to Booking diff --git a/apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql new file mode 100644 index 000000000..3f824b335 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql @@ -0,0 +1,14 @@ +-- Add bookingId to Journey for per-booking segment release +ALTER TABLE "passenger"."Journey" + ADD COLUMN IF NOT EXISTS "bookingId" TEXT; + +CREATE UNIQUE INDEX IF NOT EXISTS "Journey_bookingId_key" ON "passenger"."Journey"("bookingId"); +CREATE INDEX IF NOT EXISTS "Journey_bookingId_idx" ON "passenger"."Journey"("bookingId"); + +-- Ensure JourneySegment cascades on Journey delete +ALTER TABLE "passenger"."JourneySegment" + DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey"; + +ALTER TABLE "passenger"."JourneySegment" + ADD CONSTRAINT "JourneySegment_journeyId_fkey" + FOREIGN KEY ("journeyId") REFERENCES "passenger"."Journey"("id") ON DELETE CASCADE; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 423a03ec9..c9d19cbaf 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -108,7 +108,7 @@ enum BookingStatus { PENDING_PAYMENT CONFIRMED CANCELLED - COMPLETED + BOARDED NO_SHOW REFUNDED @@ -325,8 +325,8 @@ model Station { sequence Int @default(0) isOperational Boolean @default(true) timezone String @default("Africa/Addis_Ababa") - lat Decimal @db.Decimal(9, 6) - lng Decimal @db.Decimal(9, 6) + lat Decimal? @db.Decimal(9, 6) + lng Decimal? @db.Decimal(9, 6) originSchedules TrainSchedule[] @relation("OriginTrips") destinationSchedules TrainSchedule[] @relation("DestinationTrips") stopTimes TripStopTime[] @@ -548,6 +548,7 @@ model Booking { modifications BookingModification[] cancellation BookingCancellation? baggage BaggageBooking[] + journey Journey? @@index([passengerId, status]) @@index([bookingType]) @@ -939,10 +940,12 @@ model SavedRoute { model Journey { id String @id @default(uuid()) passengerId String + bookingId String? @unique status String totalMinor Int currency String @default("ETB") createdAt DateTime @default(now()) + booking Booking? @relation(fields: [bookingId], references: [id]) journeySegments JourneySegment[] @@schema("passenger") } diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index b40a5c0ee..385850b64 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -198,6 +198,9 @@ export class BookingsService { { contactEmail: { contains: search, mode: 'insensitive' } }, { contactPhone: { contains: search, mode: 'insensitive' } }, { passenger: { user: { fullName: { contains: search, mode: 'insensitive' } } } }, + { passenger: { user: { email: { contains: search, mode: 'insensitive' } } } }, + { passenger: { user: { phone: { contains: search, mode: 'insensitive' } } } }, + { seats: { some: { passengerName: { contains: search, mode: 'insensitive' } } } }, ]; } @@ -237,6 +240,7 @@ export class BookingsService { childCount: booking.childCount, createdAt: booking.createdAt, passenger: booking.passenger?.user, + passengerNames: [...new Set(booking.seats.map((s: any) => s.passengerName))], schedule: { train: booking.schedule.train, originStation: booking.schedule.originStation, @@ -262,10 +266,23 @@ export class BookingsService { return this.createOneWayBooking(dto); } + private validateSeatIdsAgainstHold(holdId: string, holdSeatIds: string[], requestedSeatIds: string[]) { + for (const seatId of requestedSeatIds) { + if (!holdSeatIds.includes(seatId)) { + throw new BadRequestException( + `Seat ${seatId} is not part of hold ${holdId}. Use seat IDs returned from POST /seats/hold.`, + ); + } + } + } + private async createOneWayBooking(dto: CreateBookingDto) { const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } }); if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired'); - + + const requestedSeatIds = (dto.passengers as any[]).map(p => p.seatId); + this.validateSeatIdsAgainstHold(dto.holdId, hold.seatIds, requestedSeatIds); + const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } @@ -335,6 +352,11 @@ export class BookingsService { if (!outboundHold || outboundHold.expiresAt < new Date()) throw new BadRequestException('Outbound seat hold expired'); if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired'); + const holdObSeatIds = (dto.passengers as any[]).map((p: any) => p.seatId ?? p.outboundSeatId).filter(Boolean); + const holdRetSeatIds = (dto.passengers as any[]).map((p: any) => p.returnSeatId).filter(Boolean); + if (holdObSeatIds.length) this.validateSeatIdsAgainstHold(dto.holdId, outboundHold.seatIds, holdObSeatIds); + if (holdRetSeatIds.length) this.validateSeatIdsAgainstHold(dto.returnHoldId!, returnHold.seatIds, holdRetSeatIds); + const [outboundSchedule, returnSchedule] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, @@ -478,6 +500,11 @@ export class BookingsService { if (!leg1Hold || leg1Hold.expiresAt < new Date()) throw new BadRequestException('Leg-1 seat hold expired'); if (!leg2Hold || leg2Hold.expiresAt < new Date()) throw new BadRequestException('Leg-2 seat hold expired'); + const leg1SeatIds = (dto.passengers as any[]).map(p => p.seatId); + const leg2SeatIds = (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId); + this.validateSeatIdsAgainstHold(dto.holdId, leg1Hold.seatIds, leg1SeatIds); + this.validateSeatIdsAgainstHold(dto.leg2HoldId!, leg2Hold.seatIds, leg2SeatIds); + const [leg1Schedule, leg2Schedule] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, @@ -624,6 +651,11 @@ export class BookingsService { if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 seat hold expired'); if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 seat hold expired'); + this.validateSeatIdsAgainstHold(dto.holdId, obL1Hold.seatIds, (dto.passengers as any[]).map(p => p.seatId)); + this.validateSeatIdsAgainstHold(dto.leg2HoldId!, obL2Hold.seatIds, (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId)); + this.validateSeatIdsAgainstHold(dto.returnHoldId!, retL1Hold.seatIds, (dto.passengers as any[]).map(p => p.returnSeatId)); + this.validateSeatIdsAgainstHold(dto.returnLeg2HoldId!, retL2Hold.seatIds, (dto.passengers as any[]).map(p => p.returnLeg2SeatId ?? p.returnSeatId)); + // Load all 4 schedules const [obL1Sched, obL2Sched, retL1Sched, retL2Sched] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), @@ -815,7 +847,21 @@ export class BookingsService { nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other'); } - processedPassengers.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality }); + processedPassengers.push({ + ...passenger, + passengerName, + dateOfBirth, + category, + verifaydaVerified, + verifaydaData, + nationality, + // Normalise: PassengerInputDto uses seatId/returnSeatId; RoundTripPassengerDto uses + // outboundSeatId/returnSeatId. Accept either form so both DTOs work. + outboundSeatId: passenger.outboundSeatId ?? passenger.seatId, + outboundLeg2SeatId: passenger.outboundLeg2SeatId ?? passenger.leg2SeatId, + returnSeatId: passenger.returnSeatId, + returnLeg2SeatId: passenger.returnLeg2SeatId, + }); } return processedPassengers; } @@ -994,7 +1040,7 @@ export class BookingsService { await this.prisma.bookingModification.create({ data: { bookingId: booking.id, modifiedBy: booking.passengerId, modificationType: 'SEAT_CHANGE', oldData: { scheduleId: booking.scheduleId, seatIds: oldSeats }, newData: { scheduleId: dto.newScheduleId, seatIds: dto.newSeatIds }, fareAdjustment: 0, reason: dto.reason }, }); - await this.seatsService.releaseSeats(oldSeats); + await this.seatsService.releaseSeats(booking.id); await this.seatsService.confirmSeats(dto.newSeatIds); return { modified: true, bookingRef: dto.bookingRef }; } @@ -1005,7 +1051,7 @@ export class BookingsService { if (booking.status === 'CANCELLED') throw new BadRequestException('Booking already cancelled'); const refundAmount = booking.status === 'CONFIRMED' ? Math.floor(booking.totalMinor * 0.8) : 0; await this.prisma.bookingCancellation.create({ data: { bookingId: booking.id, cancelledBy: booking.passengerId, reason, refundAmount, refundMethod: booking.paymentIntent?.method ?? 'ORIGINAL', refundStatus: 'PENDING' } }); - await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(booking.id); await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } }); return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' }; } @@ -1033,7 +1079,7 @@ export class BookingsService { const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } }); if (!booking) throw new NotFoundException('Booking not found'); - await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(booking.id); await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } }); await this.prisma.booking.delete({ where: { id } }); @@ -1069,7 +1115,7 @@ export class BookingsService { const cutoff = new Date(Date.now() - 20 * 60 * 1000); const expired = await this.prisma.booking.findMany({ where: { status: 'PENDING_PAYMENT', createdAt: { lt: cutoff } }, include: { seats: true } }); for (const b of expired) { - await this.seatsService.releaseSeats(b.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(b.id); await this.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } }); } } 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 48b84c4b6..a1b8ad6f4 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 @@ -14,6 +14,21 @@ function generateRef(): string { return 'EDR-' + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } +// Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx) +const ETH_MOBILE_PREFIXES = ['911','912','913','914','915','916','917','921','922','923','924','930','931','932','933','934','935','936','937','938','939','961','962','963','964']; + +function generateEthiopianPhone(): string { + const prefix = ETH_MOBILE_PREFIXES[Math.floor(Math.random() * ETH_MOBILE_PREFIXES.length)]; + const suffix = String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0'); + return `+251${prefix}${suffix}`; +} + +function generateGuestEmail(uniqueId: string): string { + const domains = ['gmail.com', 'yahoo.com', 'ethionet.et', 'telecom.et']; + const domain = domains[Math.floor(Math.random() * domains.length)]; + return `guest.edr.${uniqueId}@${domain}`; +} + function calculateAge(dateOfBirth: Date): number { const today = new Date(); let age = today.getFullYear() - dateOfBirth.getFullYear(); @@ -840,7 +855,7 @@ export class GuestBookingService { const existingPhone = await this.prisma.user.findUnique({ where: { phone: accountPhone } }); if (existingPhone) throw new BadRequestException('Phone number already registered. Please login instead.'); } - if (!accountPhone) accountPhone = `+guest-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + if (!accountPhone) accountPhone = generateEthiopianPhone(); const user = await this.prisma.user.create({ data: { @@ -860,17 +875,17 @@ export class GuestBookingService { } const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - let guestEmail = firstPassenger.email || `guest-${uniqueId}@edr-platform.com`; + let guestEmail = firstPassenger.email || generateGuestEmail(uniqueId); if (firstPassenger.email) { const existing = await this.prisma.user.findUnique({ where: { email: firstPassenger.email } }); - if (existing) guestEmail = `guest-${uniqueId}@edr-platform.com`; + if (existing) guestEmail = generateGuestEmail(uniqueId); } let guestPhone = firstPassenger.phone || null; if (guestPhone) { const existing = await this.prisma.user.findUnique({ where: { phone: guestPhone } }); if (existing) guestPhone = null; } - if (!guestPhone) guestPhone = `+guest-${uniqueId}`; + if (!guestPhone) guestPhone = generateEthiopianPhone(); const tempUser = await this.prisma.user.create({ data: { diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 7171b56ad..a4c9b8a30 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -157,11 +157,11 @@ export class PassengersService { async getStats(passengerId: string) { const [totalTrips, totalSpendResult, loyalty] = await Promise.all([ - this.prisma.booking.count({ where: { passengerId, status: 'COMPLETED' } }), - this.prisma.booking.aggregate({ where: { passengerId, status: 'COMPLETED' }, _sum: { totalMinor: true } }), + this.prisma.booking.count({ where: { passengerId, status: 'BOARDED' as any } }), + this.prisma.booking.aggregate({ where: { passengerId, status: 'BOARDED' as any }, _sum: { totalMinor: true } }), this.prisma.loyaltyAccount.findUnique({ where: { passengerId } }), ]); - const totalSpend = (totalSpendResult._sum.totalMinor ?? 0) / 100; + const totalSpend = ((totalSpendResult._sum?.totalMinor ?? 0) as number) / 100; return { totalTrips, totalSpend, loyaltyPoints: loyalty?.pointsBalance ?? 0, co2Saved: totalTrips * 6 }; } diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index c8d0580bf..cce74b476 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -445,7 +445,7 @@ export class PaymentsService { include: { seats: true }, }); if (booking) { - await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(booking.id); await this.prisma.booking.update({ where: { id: dto.bookingId }, data: { status: "CANCELLED" }, @@ -734,51 +734,125 @@ export class PaymentsService { private async createJourneySegments( booking: Prisma.BookingGetPayload<{ include: { seats: true } }>, ) { - const schedule = await this.prisma.trainSchedule.findUnique({ - where: { id: booking.scheduleId }, - include: { - stopTimes: { include: { station: true }, orderBy: { sequence: "asc" } }, - }, - }); - if (!schedule) return; + const b = booking as any; - const stopTimes = schedule.stopTimes; - if (stopTimes.length < 2) return; + // Build per-leg definitions: { scheduleId, originStationId, destinationStationId, seatIds[] } + // BookingSeat.leg: 1=outbound/leg-1, 2=return/leg-2, 3=return leg-1 (transit), 4=return leg-2 + type LegDef = { scheduleId: string; originStationId: string; destinationStationId: string; seatIds: string[] }; + const legDefs: LegDef[] = []; - const originSequence = stopTimes.findIndex( - (st) => st.stationId === schedule.originStationId, - ); - const destSequence = stopTimes.findIndex( - (st) => st.stationId === schedule.destinationStationId, - ); + const seatsForLeg = (legNum: number) => + booking.seats.filter((s: any) => s.leg === legNum).map((s: any) => s.seatId); - if ( - originSequence < 0 || - destSequence < 0 || - originSequence >= destSequence - ) - return; + if (booking.bookingType === 'ONE_WAY') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.destinationStationId, + seatIds: booking.seats.map((s: any) => s.seatId), + }); + } else if (booking.bookingType === 'ROUND_TRIP') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.destinationStationId, + seatIds: seatsForLeg(1), + }); + if (b.returnScheduleId && b.returnOriginStationId && b.returnDestinationStationId) { + legDefs.push({ + scheduleId: b.returnScheduleId, + originStationId: b.returnOriginStationId, + destinationStationId: b.returnDestinationStationId, + seatIds: seatsForLeg(2), + }); + } + } else if (booking.bookingType === 'TRANSIT') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.leg2OriginStationId, // transit station + seatIds: seatsForLeg(1), + }); + if (b.leg2ScheduleId && b.leg2OriginStationId && b.leg2DestinationStationId) { + legDefs.push({ + scheduleId: b.leg2ScheduleId, + originStationId: b.leg2OriginStationId, + destinationStationId: b.leg2DestinationStationId, + seatIds: seatsForLeg(2), + }); + } + } else if (booking.bookingType === 'ROUND_TRIP_TRANSIT') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.leg2OriginStationId, + seatIds: seatsForLeg(1), + }); + if (b.leg2ScheduleId && b.leg2OriginStationId && b.leg2DestinationStationId) { + legDefs.push({ + scheduleId: b.leg2ScheduleId, + originStationId: b.leg2OriginStationId, + destinationStationId: b.leg2DestinationStationId, + seatIds: seatsForLeg(2), + }); + } + if (b.returnScheduleId && b.returnOriginStationId && b.returnDestinationStationId) { + legDefs.push({ + scheduleId: b.returnScheduleId, + originStationId: b.returnOriginStationId, + destinationStationId: b.returnLeg2OriginStationId ?? b.returnDestinationStationId, + seatIds: seatsForLeg(3), + }); + } + if (b.returnLeg2ScheduleId && b.returnLeg2OriginStationId && b.returnLeg2DestStationId) { + legDefs.push({ + scheduleId: b.returnLeg2ScheduleId, + originStationId: b.returnLeg2OriginStationId, + destinationStationId: b.returnLeg2DestStationId, + seatIds: seatsForLeg(4), + }); + } + } + + if (legDefs.length === 0) return; const journey = await this.prisma.journey.create({ data: { passengerId: booking.passengerId, - status: "CONFIRMED", - totalMinor: booking.totalMinor, - currency: booking.currency, + bookingId: booking.id, + status: 'CONFIRMED', + totalMinor: booking.totalMinor, + currency: booking.currency, }, }); - const journeySegments = []; - for (const bookingSeat of booking.seats) { - for (let i = originSequence; i < destSequence; i++) { - journeySegments.push({ - journeyId: journey.id, - scheduleId: booking.scheduleId, - segmentOrder: i, - seatId: bookingSeat.seatId, - departureStationId: stopTimes[i].stationId, - arrivalStationId: stopTimes[i + 1].stationId, - }); + const journeySegments: any[] = []; + let segmentOrder = 0; + + for (const leg of legDefs) { + if (leg.seatIds.length === 0) continue; + + const stopTimes = await this.prisma.tripStopTime.findMany({ + where: { scheduleId: leg.scheduleId }, + orderBy: { sequence: 'asc' }, + select: { stationId: true, sequence: true }, + }); + + const originIdx = stopTimes.findIndex(st => st.stationId === leg.originStationId); + const destIdx = stopTimes.findIndex(st => st.stationId === leg.destinationStationId); + if (originIdx < 0 || destIdx < 0 || originIdx >= destIdx) continue; + + for (const seatId of leg.seatIds) { + for (let i = originIdx; i < destIdx; i++) { + journeySegments.push({ + journeyId: journey.id, + scheduleId: leg.scheduleId, + segmentOrder: segmentOrder++, + seatId, + departureStationId: stopTimes[i].stationId, + arrivalStationId: stopTimes[i + 1].stationId, + }); + } } } diff --git a/apps/edr-passenger-api/src/modules/search/search.service.ts b/apps/edr-passenger-api/src/modules/search/search.service.ts index 0ca9a8723..eb308e6e6 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -39,6 +39,23 @@ export class SearchService { const outbound = [...direct, ...transit]; + if (outbound.length === 0) { + const alternativesOutbound = await this.searchAlternatives( + dto.originStationId, + dto.destinationStationId, + dto.date, + dto.adultCount, + dto.childCount, + dto.nationality, + ); + return { + journeyType: dto.journeyType === 'ROUND_TRIP' ? 'ROUND_TRIP' : 'ONE_WAY', + outbound: [], + alternativeOutbound: alternativesOutbound, + requestedDate: dto.date, + }; + } + if (dto.journeyType === 'ROUND_TRIP') { const [returnDirect, returnTransit] = await Promise.all([ this.searchSchedules( @@ -68,12 +85,85 @@ export class SearchService { new Date(s.departureAt ?? s.leg1?.departureAt).getTime() > latestOutboundArrival ); + if (inbound.length === 0) { + const alternativeInbound = await this.searchAlternatives( + dto.destinationStationId, + dto.originStationId, + dto.returnDate ?? dto.date, + dto.adultCount, + dto.childCount, + dto.nationality, + ); + return { journeyType: 'ROUND_TRIP', outbound, inbound: [], alternativeInbound }; + } + return { journeyType: 'ROUND_TRIP', outbound, inbound }; } return { journeyType: 'ONE_WAY', outbound }; } + private async searchAlternatives( + originStationId: string, + destinationStationId: string, + dateStr: string, + adultCount: number, + childCount?: number, + nationality?: string, + ) { + const [y, m, d] = dateStr.split('-').map(Number); + const requestedDate = new Date(y, m - 1, d, 0, 0, 0, 0); + + const now = new Date(); + const daysBefore = Math.min(7, Math.floor(requestedDate.getTime() / 86_400_000)); + const daysAfter = 14 - daysBefore; + + const windowStart = new Date(requestedDate); + windowStart.setDate(windowStart.getDate() - daysBefore); + if (windowStart < now) windowStart.setTime(now.getTime()); + + const windowEnd = new Date(requestedDate); + windowEnd.setDate(windowEnd.getDate() + daysAfter + 1); // exclusive upper bound + + const totalPassengers = adultCount + (childCount ?? 0); + + const requestedNextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + + const schedules = await this.prisma.trainSchedule.findMany({ + where: { + status: { in: ['SCHEDULED', 'BOARDING'] }, + OR: [ + { departureAt: { gte: windowStart, lt: requestedDate } }, + { departureAt: { gte: requestedNextDay < now ? now : requestedNextDay, lt: windowEnd } }, + ], + stopTimes: { some: { stationId: originStationId } }, + }, + include: { + train: true, + originStation: true, + destinationStation: true, + stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } }, + coachAssignments: { + include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } }, + }, + }, + orderBy: { departureAt: 'asc' }, + }); + + const results: any[] = []; + for (const schedule of schedules) { + const result = await this.buildScheduleResult( + schedule, + originStationId, + destinationStationId, + totalPassengers, + nationality, + ); + if (result) results.push(result); + } + return results; + } + private async searchSchedules( originStationId: string, destinationStationId: string, @@ -85,12 +175,13 @@ export class SearchService { const [y, m, d] = dateStr.split('-').map(Number); const date = new Date(y, m - 1, d, 0, 0, 0, 0); const nextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + const now = new Date(); const totalPassengers = adultCount + (childCount ?? 0); const schedules = await this.prisma.trainSchedule.findMany({ where: { status: { in: ['SCHEDULED', 'BOARDING'] }, - departureAt: { gte: date, lt: nextDay }, + departureAt: { gte: date < now ? now : date, lt: nextDay }, stopTimes: { some: { stationId: originStationId } }, }, include: { diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 21060b595..0dbc1c653 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -11,7 +11,7 @@ export class SeatsService { private segmentsService: SegmentsService, ) {} - async getSeatMap(scheduleId: string, coachId?: string) { + async getSeatMap(scheduleId: string, coachId?: string, originStationId?: string, destinationStationId?: string) { const assignments = await this.prisma.coachAssignment.findMany({ where: { scheduleId, ...(coachId ? { coachId } : {}) }, include: { @@ -25,16 +25,14 @@ export class SeatsService { orderBy: { positionNumber: 'asc' }, }); - console.log(`[getSeatMap] scheduleId=${scheduleId}, coachId=${coachId}, found ${assignments.length} coach assignments`); - const allSeatIds = assignments.flatMap((a: any) => a.coach.seats.map((s: any) => s.id)); - const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds); + const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds, originStationId, destinationStationId); - const response = { + return { coaches: assignments.map((a) => { const allSeats = a.coach.seats; const seatClassNames = a.coach.coachType.seatClasses.map((sc: any) => sc.name); - + return { id: a.coach.id, assignmentId: a.id, @@ -68,43 +66,95 @@ export class SeatsService { }; }), }; - - console.log(`[getSeatMap] returning ${response.coaches.length} coaches with seats`); - return response; } async resolveEffectiveStatuses( scheduleId: string, seatIds: string[], + originStationId?: string, + destinationStationId?: string, ): Promise> { const statusMap = new Map(); - if (seatIds.length === 0) return statusMap; + // Resolve the requested leg's sequence range once + let reqFrom: number | undefined; + let reqTo: number | undefined; + let allStopTimes: { stationId: string; sequence: number }[] | null = null; + + const getStopTimes = async () => { + if (!allStopTimes) { + allStopTimes = await this.prisma.tripStopTime.findMany({ + where: { scheduleId }, + select: { stationId: true, sequence: true }, + }); + } + return allStopTimes; + }; + + if (originStationId && destinationStationId) { + const stops = await getStopTimes(); + const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence; + reqFrom = seqOf(originStationId); + reqTo = seqOf(destinationStationId); + } + + // ── Active holds ────────────────────────────────────────────────────────── const activeHolds = await this.prisma.seatHold.findMany({ - where: { - scheduleId, - expiresAt: { gt: new Date() }, - seatIds: { hasSome: seatIds }, - }, - select: { seatIds: true }, + where: { scheduleId, expiresAt: { gt: new Date() }, seatIds: { hasSome: seatIds } }, + select: { seatIds: true, createdBy: true }, }); + for (const hold of activeHolds) { + let holdFrom: number | undefined; + let holdTo: number | undefined; + try { + if (hold.createdBy?.trimStart().startsWith('{')) { + const meta = JSON.parse(hold.createdBy); + const stops = await getStopTimes(); + const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence; + holdFrom = seqOf(meta.originStationId); + holdTo = seqOf(meta.destinationStationId); + } + } catch { /* ignore */ } + for (const seatId of hold.seatIds) { - if (seatIds.includes(seatId)) statusMap.set(seatId, 'HELD'); + if (!seatIds.includes(seatId)) continue; + if (reqFrom !== undefined && reqTo !== undefined && holdFrom !== undefined && holdTo !== undefined) { + if (holdFrom < reqTo && reqFrom < holdTo) statusMap.set(seatId, 'HELD'); + } else { + statusMap.set(seatId, 'HELD'); + } } } + // ── Confirmed bookings via JourneySegment ───────────────────────────────── const bookedSegments = await this.prisma.journeySegment.findMany({ where: { scheduleId, seatId: { in: seatIds }, journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } }, }, - select: { seatId: true }, + select: { seatId: true, departureStationId: true, arrivalStationId: true }, }); - for (const seg of bookedSegments) { - if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED'); + + if (reqFrom !== undefined && reqTo !== undefined) { + const stops = await getStopTimes(); + const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence; + for (const seg of bookedSegments) { + if (!seg.seatId) continue; + const segFrom = seqOf(seg.departureStationId); + const segTo = seqOf(seg.arrivalStationId); + if (segFrom !== undefined && segTo !== undefined) { + if (segFrom < reqTo && reqFrom < segTo) statusMap.set(seg.seatId, 'BOOKED'); + } else { + statusMap.set(seg.seatId, 'BOOKED'); + } + } + } else { + for (const seg of bookedSegments) { + if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED'); + } } return statusMap; @@ -154,6 +204,7 @@ export class SeatsService { if (reqFrom >= reqTo) throw new BadRequestException('Origin must come before destination'); + // ── Check existing holds for overlap ──────────────────────────────────── const activeHolds = await tx.seatHold.findMany({ where: { scheduleId: dto.scheduleId, expiresAt: { gt: new Date() } }, select: { seatIds: true, createdBy: true }, @@ -197,6 +248,29 @@ export class SeatsService { } } + // ── Check confirmed JourneySegments for overlap ────────────────────────── + const bookedSegments = await tx.journeySegment.findMany({ + where: { + scheduleId: dto.scheduleId, + seatId: { in: seatIds }, + journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } }, + }, + select: { seatId: true, departureStationId: true, arrivalStationId: true }, + }); + + for (const seg of bookedSegments) { + if (!seg.seatId) continue; + const segFrom = seqOf(seg.departureStationId); + const segTo = seqOf(seg.arrivalStationId); + if (segFrom !== undefined && segTo !== undefined) { + if (segFrom < reqTo && reqFrom < segTo) { + throw new ConflictException( + `Seat ${seatLabelById[seg.seatId]} is already booked for this leg`, + ); + } + } + } + const holdMeta = { originStationId: dto.originStationId, destinationStationId: dto.destinationStationId, @@ -347,16 +421,12 @@ export class SeatsService { return { released: true, holdId }; } - async confirmSeats(seatIds: string[]) { - // No-op - } + // Physical seat.status stays AVAILABLE — segment rows are the source of truth for occupancy. + async confirmSeats(_seatIds: string[]) {} - async releaseSeats(seatIds: string[]) { - if (seatIds.length > 0) { - await this.prisma.journeySegment.deleteMany({ - where: { seatId: { in: seatIds } }, - }); - } + // Delete the Journey (and its JourneySegments) scoped to this booking. + async releaseSeats(bookingId: string) { + await this.prisma.journey.deleteMany({ where: { bookingId } }); } async autoAssignSeats(scheduleId: string, count: number, seatClassName: string): Promise { @@ -424,7 +494,7 @@ export class SeatsService { invalid++; continue; } - const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts; + const [coachId, , row, col, seatNumber] = parts; if (!coachId || !row || !col || !seatNumber) { errors.push(`Line ${i + 2}: Missing required fields`); invalid++; @@ -448,7 +518,7 @@ export class SeatsService { for (let i = 0; i < lines.length; i++) { try { const parts = lines[i].split(','); - const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts; + const [coachId, , row, col, seatNumber, kind, status, premiumFeeMinor] = parts; await this.prisma.seat.upsert({ where: { coachId_row_col: { coachId, row: parseInt(row), col } }, @@ -481,18 +551,8 @@ export class SeatsService { const seat = await this.prisma.seat.findUnique({ where: { id: seatId } }); if (!seat) throw new NotFoundException('Seat not found'); - await this.prisma.seat.update({ - where: { id: seatId }, - data: { status: 'BLOCKED' }, - }); - - await this.prisma.seatBlock.create({ - data: { - seatId, - reason, - blockedBy: 'system', - }, - }); + await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'BLOCKED' } }); + await this.prisma.seatBlock.create({ data: { seatId, reason, blockedBy: 'system' } }); return { blocked: true, seatId, reason }; } @@ -501,14 +561,8 @@ export class SeatsService { const seat = await this.prisma.seat.findUnique({ where: { id: seatId } }); if (!seat) throw new NotFoundException('Seat not found'); - await this.prisma.seat.update({ - where: { id: seatId }, - data: { status: 'AVAILABLE' }, - }); - - await this.prisma.seatBlock.deleteMany({ - where: { seatId }, - }); + await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'AVAILABLE' } }); + await this.prisma.seatBlock.deleteMany({ where: { seatId } }); return { unblocked: true, seatId }; } @@ -518,11 +572,9 @@ export class SeatsService { if (!seat) throw new NotFoundException('Seat not found'); if (!seat.seatNumber) throw new BadRequestException('Seat already removed'); - // Mark removed seat with negative seatNumber (e.g., '1' → '-1') to show empty space - const negatedNumber = `-${seat.seatNumber}`; await this.prisma.seat.update({ where: { id: seatId }, - data: { seatNumber: negatedNumber }, + data: { seatNumber: `-${seat.seatNumber}` }, }); return { removed: true, seatId, originalSeatNumber: seat.seatNumber }; @@ -535,26 +587,15 @@ export class SeatsService { throw new BadRequestException('Seat is not removed'); } - // Restore original seatNumber by removing the negative sign const originalNumber = seat.seatNumber.slice(1); - await this.prisma.seat.update({ - where: { id: seatId }, - data: { seatNumber: originalNumber }, - }); + await this.prisma.seat.update({ where: { id: seatId }, data: { seatNumber: originalNumber } }); return { restored: true, seatId, seatNumber: originalNumber }; } @Cron(CronExpression.EVERY_MINUTE) async expireHolds() { - const now = new Date(); - const expired = await this.prisma.seatHold.findMany({ where: { expiresAt: { lt: now } } }); - if (expired.length === 0) return; - - const expiredIds = expired.map(h => h.id); - for (const hold of expired) { - await this.releaseSeats(hold.seatIds); - } - await this.prisma.seatHold.deleteMany({ where: { id: { in: expiredIds } } }); + // Holds are temporary and don't create Journey rows — just delete expired ones. + await this.prisma.seatHold.deleteMany({ where: { expiresAt: { lt: new Date() } } }); } } diff --git a/apps/edr-passenger-api/src/modules/stations/stations.dto.ts b/apps/edr-passenger-api/src/modules/stations/stations.dto.ts index fc350dbcf..57e78caf6 100644 --- a/apps/edr-passenger-api/src/modules/stations/stations.dto.ts +++ b/apps/edr-passenger-api/src/modules/stations/stations.dto.ts @@ -7,8 +7,8 @@ export class CreateStationDto { @ApiProperty({ example: 'Addis Ababa' }) @IsString() city: string; @ApiPropertyOptional() @IsOptional() @IsString() timezone?: string; @ApiPropertyOptional() @IsOptional() @IsString() countryCode?: string; - @ApiProperty({ example: 9.0054 }) @IsNumber() lat: number; - @ApiProperty({ example: 38.7636 }) @IsNumber() lng: number; + @ApiPropertyOptional({ example: 9.0054 }) @IsOptional() @IsNumber() lat?: number; + @ApiPropertyOptional({ example: 38.7636 }) @IsOptional() @IsNumber() lng?: number; @ApiPropertyOptional({ example: 1 }) @IsOptional() @IsInt() sequence?: number; @ApiPropertyOptional({ example: true }) @IsOptional() @IsBoolean() isOperational?: boolean; } diff --git a/apps/edr-passenger-api/src/modules/stations/stations.service.ts b/apps/edr-passenger-api/src/modules/stations/stations.service.ts index 5dd1aa77d..bc1faa1bc 100644 --- a/apps/edr-passenger-api/src/modules/stations/stations.service.ts +++ b/apps/edr-passenger-api/src/modules/stations/stations.service.ts @@ -50,7 +50,10 @@ export class StationsService { } async create(dto: CreateStationDto) { - const station = await this.prisma.station.create({ data: dto }); + const { lat, lng, ...rest } = dto; + const station = await this.prisma.station.create({ + data: { ...rest, ...(lat !== undefined && { lat }), ...(lng !== undefined && { lng }) } as any, + }); await this.auditService.log({ userId: this.request?.user?.id, diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 2c57a59e3..ce940b07a 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -44,6 +44,7 @@ export class TicketsService { booking: { include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, + returnSchedule: { select: { departureAt: true, arrivalAt: true, originStation: true, destinationStation: true } }, seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } }, passenger: { include: { user: true } }, }, @@ -72,6 +73,8 @@ export class TicketsService { displayTotalMinor: t.booking.displayTotalMinor, passenger: t.booking.passenger?.user || { fullName: 'Guest', email: t.booking.contactEmail }, contactEmail: t.booking.contactEmail, + contactPhone: t.booking.contactPhone, + returnSchedule: (t.booking as any).returnSchedule ?? null, }, schedule: t.booking.schedule, seat: t.booking.seats[0]?.seat, @@ -105,7 +108,7 @@ export class TicketsService { legs: legSummary, }); const qrPayload = await QRCode.toDataURL(qrData); - const barcodePayload = `EDR${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`; + const barcodePayload = `${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`; const ticket = await this.prisma.ticket.upsert({ where: { bookingId }, diff --git a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx index 7b43fb677..e420c4e07 100644 --- a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Filter, Download, Eye, XCircle, Trash2 } from 'lucide-react'; +import { Download, Eye, XCircle, Trash2 } from 'lucide-react'; import DataTable from '@/components/ui/DataTable'; import Badge from '@/components/ui/Badge'; import Pagination from '@/components/ui/Pagination'; @@ -13,13 +13,21 @@ import { bookingsApi, apiClient } from '@/lib/api'; import { formatCurrency, formatDateTime } from '@/lib/utils'; import { BookingFilters } from '@/types'; +const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => ( +
+

{label}

+

{value || '—'}

+
+); + +const SectionHeader = ({ title }: { title: string }) => ( +

+ {title} +

+); + export default function BookingsPage() { - const [filters, setFilters] = useState({ - page: 1, - pageSize: 20, - search: '', - status: '', - }); + const [filters, setFilters] = useState({ page: 1, pageSize: 20, search: '', status: '' }); const [selectedBooking, setSelectedBooking] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [bookingToDelete, setBookingToDelete] = useState(null); @@ -28,14 +36,8 @@ export default function BookingsPage() { const [exportDateFrom, setExportDateFrom] = useState(''); const [exportDateTo, setExportDateTo] = useState(''); const [exportColumns, setExportColumns] = useState>({ - bookingRef: true, - passenger: true, - status: true, - bookingType: false, - passengerCount: false, - totalMinor: true, - paymentStatus: true, - createdAt: true, + bookingRef: true, bookingType: false, passengerNames: true, contactPhone: true, + contactEmail: true, passengerCount: false, paymentStatus: true, totalMinor: true, status: true, createdAt: true, }); const queryClient = useQueryClient(); @@ -45,10 +47,6 @@ export default function BookingsPage() { queryFn: () => bookingsApi.getAll(filters), }); - if (error) { - console.error('Bookings API Error:', error); - } - const cancelMutation = useMutation({ mutationFn: ({ id, reason }: { id: string; reason?: string }) => bookingsApi.cancel(id, reason), onSuccess: () => { @@ -56,9 +54,7 @@ export default function BookingsPage() { setSuccessMessage('Booking cancelled successfully'); setTimeout(() => setSuccessMessage(''), 3000); }, - onError: (error: any) => { - alert(`Error: ${error.message || 'Failed to cancel booking'}`); - }, + onError: (error: any) => alert(`Error: ${error.message || 'Failed to cancel booking'}`), }); const deleteMutation = useMutation({ @@ -77,26 +73,22 @@ export default function BookingsPage() { }); const handleCancel = async (booking: any) => { - if (window.confirm(`Are you sure you want to cancel booking ${booking.bookingRef}? This will process a refund.`)) { + if (window.confirm(`Cancel booking ${booking.bookingRef}? This will process a refund.`)) { await cancelMutation.mutateAsync({ id: booking.id, reason: 'Cancelled by admin' }); } }; - const handleDeleteClick = (booking: any) => { - setBookingToDelete(booking); - setDeleteConfirmOpen(true); - }; - - const handleConfirmDelete = async () => { - if (bookingToDelete) { - await deleteMutation.mutateAsync(bookingToDelete.id); - } - }; + const BOOKING_COLS = [ + { key: 'bookingRef', label: 'Booking Reference' }, { key: 'journeyType', label: 'Journey Type' }, + { key: 'passengerNames', label: 'Passenger Names' }, { key: 'contactPhone', label: 'Contact Phone' }, + { key: 'contactEmail', label: 'Contact Email' }, { key: 'passengerCount', label: 'Passenger Count' }, + { key: 'paymentStatus', label: 'Payment Status' }, { key: 'totalMinor', label: 'Amount' }, + { key: 'status', label: 'Status' }, { key: 'createdAt', label: 'Created At' }, + ]; const confirmExport = () => { const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k); - if (cols.length === 0) { alert('Please select at least one column'); return; } - + if (!cols.length) { alert('Please select at least one column'); return; } const exportItems = (data?.items || []).filter((b: any) => { if (!exportDateFrom && !exportDateTo) return true; const d = b.createdAt ? new Date(b.createdAt).toISOString().split('T')[0] : null; @@ -104,27 +96,27 @@ export default function BookingsPage() { if (exportDateTo && (!d || d > exportDateTo)) return false; return true; }); - const csv = [ - cols.join(','), + BOOKING_COLS.map(c => `"${c.label}"`).join(','), ...exportItems.map((booking: any) => { - const values = cols.map(col => { - switch (col) { + const values = BOOKING_COLS.filter(c => cols.includes(c.key)).map(({ key }) => { + switch (key) { case 'bookingRef': return booking.bookingRef; - case 'passenger': return booking.passenger?.fullName || booking.contactEmail || 'Guest'; - case 'status': return booking.status; - case 'bookingType': return booking.bookingType || 'N/A'; - case 'passengerCount': return booking.adultCount + booking.childCount; - case 'totalMinor': return booking.totalMinor; + case 'journeyType': return booking.bookingType || 'N/A'; + case 'passengerNames': return booking.passengerNames?.join(', ') || 'N/A'; + case 'contactPhone': return booking.contactPhone || 'N/A'; + case 'contactEmail': return booking.contactEmail || 'N/A'; + case 'passengerCount': return (booking.adultCount ?? 0) + (booking.childCount ?? 0); case 'paymentStatus': return booking.paymentIntent?.status || 'PENDING'; - case 'createdAt': return booking.createdAt; + case 'totalMinor': return formatCurrency(booking.totalMinor, booking.currency); + case 'status': return booking.status; + case 'createdAt': return booking.createdAt ? formatDateTime(booking.createdAt) : ''; default: return ''; } }); return values.map(v => `"${v}"`).join(','); }), ].join('\n'); - const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -136,91 +128,61 @@ export default function BookingsPage() { const columns = [ { - key: 'bookingRef', - label: 'Reference', - sortable: true, - render: (booking: any) => ( - {booking.bookingRef} - ), - }, - { - key: 'passenger', - label: 'Passenger', + key: 'bookingRef', label: 'Reference', sortable: true, render: (booking: any) => (
-
{booking.passenger?.fullName || booking.contactEmail || 'Guest'}
-
{booking.contactPhone || booking.passenger?.phone}
+
{booking.bookingRef}
+
{booking.bookingType || 'ONE_WAY'}
), }, { - key: 'bookingType', - label: 'Type', - sortable: true, - render: (booking: any) => booking.bookingType || 'ONE_WAY', - }, - { - key: 'passengerCount', - label: 'Passengers', + key: 'passengerNames', label: 'Names', render: (booking: any) => { - const adults = booking.adultCount || 0; - const children = booking.childCount || 0; - if (adults === 0 && children === 0) return '—'; - const parts = [`Adult: ${adults}`]; - if (children > 0) parts.push(`Child: ${children}`); - return parts.join(' / '); + const names: string[] = booking.passengerNames || []; + if (!names.length) return ; + return
{names.map((n, i) => {n})}
; }, }, { - key: 'status', - label: 'Status', + key: 'contact', label: 'Contact', render: (booking: any) => ( - {booking.status} +
+
{booking.contactPhone || booking.passenger?.phone}
+
{booking.contactEmail || booking.passenger?.email}
+
), }, { - key: 'totalMinor', - label: 'Amount', - sortable: true, - render: (booking: any) => formatCurrency(booking.totalMinor, booking.currency), + key: 'passengerCount', label: 'Passengers', + render: (booking: any) => { + const adults = booking.adultCount || 0, children = booking.childCount || 0; + if (!adults && !children) return '—'; + return <>
Adult: {adults}
Child: {children}
; + }, }, { - key: 'paymentStatus', - label: 'Payment', + key: 'paymentStatus', label: 'Payment', render: (booking: any) => ( - - {booking.paymentIntent?.status || 'PENDING'} - +
+ {booking.paymentIntent?.status || 'PENDING'} +
{formatCurrency(booking.totalMinor, booking.currency)}
+
), }, { - key: 'createdAt', - label: 'Created', - sortable: true, - render: (booking: any) => formatDateTime(booking.createdAt), + key: 'status', label: 'Status', + render: (booking: any) => {booking.status}, }, ]; const actions = [ + { label: 'View Details', onClick: (b: any) => setSelectedBooking(b), variant: 'secondary' as const, icon: Eye }, { - label: 'View Details', - onClick: (booking: any) => setSelectedBooking(booking), - variant: 'secondary' as const, - icon: Eye, - }, - { - label: 'Cancel Booking', - onClick: handleCancel, - variant: 'danger' as const, - icon: XCircle, - show: (booking: any) => booking.status !== 'CANCELLED' && booking.status !== 'COMPLETED', - }, - { - label: 'Delete', - onClick: handleDeleteClick, - variant: 'danger' as const, - icon: Trash2, + label: 'Cancel Booking', onClick: handleCancel, variant: 'danger' as const, icon: XCircle, + show: (b: any) => b.status !== 'CANCELLED' && b.status !== 'BOARDED', }, + { label: 'Delete', onClick: (b: any) => { setBookingToDelete(b); setDeleteConfirmOpen(true); }, variant: 'danger' as const, icon: Trash2 }, ]; return ( @@ -235,9 +197,7 @@ export default function BookingsPage() {
{successMessage && ( -
- ✓ {successMessage} -
+
✓ {successMessage}
)} {error && (
@@ -246,222 +206,195 @@ export default function BookingsPage() { )}
- setFilters({ ...filters, search: e.target.value, page: 1 })} - /> + setFilters({ ...filters, search: e.target.value, page: 1 })} />
- setFilters({ ...filters, status: e.target.value || undefined, page: 1 })}> - + - More Filters
- - - + {data?.meta && ( - setFilters({ ...filters, page })} - /> + setFilters({ ...filters, page })} /> )}
{/* Booking Details Modal */} setSelectedBooking(null)} title="Booking Details" size="xl"> - {selectedBooking && ( -
-
-
- -

{selectedBooking.bookingRef}

-
-
- -
- {selectedBooking.status} -
-
-
- -

{selectedBooking.bookingType || 'N/A'}

-
-
- -

{formatDateTime(selectedBooking.createdAt)}

-
-
- -
- + {selectedBooking && (() => { + const b = selectedBooking; + const isRoundTrip = b.bookingType === 'ROUND_TRIP' || b.bookingType === 'ROUND_TRIP_TRANSIT'; + return (
-

Passenger Information

-
-
- -

{selectedBooking.passenger?.fullName || selectedBooking.contactEmail || 'N/A'}

-
-
- -

{selectedBooking.contactEmail || selectedBooking.passenger?.email || 'N/A'}

-
-
- -

{selectedBooking.contactPhone || selectedBooking.passenger?.phone || 'N/A'}

-
-
- -

{selectedBooking.passengerId || 'N/A'}

-
-
-
- -
- -
-

Journey Details

-
-
- -

{selectedBooking.adultCount || 0}

-
-
- -

{selectedBooking.childCount || 0}

-
-
- -

{selectedBooking.scheduleId || 'N/A'}

-
-
- -

{selectedBooking.promoCode || 'None'}

-
-
-
- -
- -
-

Payment Information

-
-
- -

{formatCurrency(selectedBooking.totalMinor, selectedBooking.currency)}

-
-
- -
- - {selectedBooking.paymentIntent?.status || 'PENDING'} - + {/* Gradient header */} +
+
+
+

Booking Reference

+

{b.bookingRef}

+
+
+ {b.status} +

{formatDateTime(b.createdAt)}

-
- -

{selectedBooking.paidAt ? formatDateTime(selectedBooking.paidAt) : 'Not paid'}

-
-
- -

{selectedBooking.displayCurrency || selectedBooking.currency}

+
+ {[ + (b.bookingType || 'ONE_WAY').replace(/_/g, ' '), + `${b.adultCount ?? 0} Adult${(b.adultCount ?? 0) !== 1 ? 's' : ''}${(b.childCount ?? 0) > 0 ? ` · ${b.childCount} Child${b.childCount !== 1 ? 'ren' : ''}` : ''}`, + b.displayCurrency || b.currency || 'ETB', + ].map((tag) => ( + + {tag} + + ))}
-
-
+
+ {/* Passenger */} +
+ +
+ + + + +
+
-
-

Additional Information

-
-
- -

{selectedBooking.source || 'N/A'}

-
-
- -

{formatDateTime(selectedBooking.updatedAt)}

-
+ {/* Journey */} +
+ +
+ + + + + + + + +
+
+ + {/* Return leg */} + {isRoundTrip && ( +
+ +
+ + + + +
+
+ )} + + {/* Payment */} +
+ +
+
+

Total Amount

+

{formatCurrency(b.totalMinor, b.currency || 'ETB')}

+ {b.displayCurrency && b.displayCurrency !== (b.currency || 'ETB') && ( +

+ ≈ {formatCurrency(b.displayTotalMinor ?? b.totalMinor, b.displayCurrency)} +

+ )} +
+
+

Payment Status

+ {b.paymentIntent?.status || 'PENDING'} +
+ + + + +
+
+ + {/* Seats */} + {b.seats && b.seats.length > 0 && ( +
+ +
+ {b.seats.map((bs: any, i: number) => ( +
+
+ {i + 1} +
+

{bs.passengerName || '—'}

+

+ {bs.passengerCategory || '—'}{bs.leg ? ` · Leg ${bs.leg}` : ''}{bs.idDocumentType ? ` · ${bs.idDocumentType}` : ''} + {bs.verifaydaVerified ? ' · ✓ Verified' : ''} +

+
+
+
+

{bs.seat?.seatNumber || bs.seatId || '—'}

+

{formatCurrency(bs.fareMinor ?? 0, b.currency || 'ETB')}

+
+
+ ))} +
+
+ )} + + {/* Timestamps */} +
+ +
+ + + +
+
+
+ +
+ setSelectedBooking(null)}>Close
- -
- setSelectedBooking(null)}>Close -
-
- )} + ); + })()} - {/* Delete Confirmation Dialog */} { setDeleteConfirmOpen(false); setBookingToDelete(null); }} - onConfirm={handleConfirmDelete} + onConfirm={async () => { if (bookingToDelete) await deleteMutation.mutateAsync(bookingToDelete.id); }} title="Delete Booking" - message={`Are you sure you want to permanently delete booking ${bookingToDelete?.bookingRef}? This action cannot be undone and will release all associated seats.`} - confirmText="Delete" - cancelText="Cancel" - isLoading={deleteMutation.isPending} - isDanger={true} + message={`Permanently delete booking ${bookingToDelete?.bookingRef}? This cannot be undone and will release all associated seats.`} + confirmText="Delete" cancelText="Cancel" isLoading={deleteMutation.isPending} isDanger /> - {/* Export Modal */} setExportModalOpen(false)} title="Export Bookings" size="md">
-
- - setExportDateFrom(e.target.value)} /> -
-
- - setExportDateTo(e.target.value)} /> -
+
setExportDateFrom(e.target.value)} />
+
setExportDateTo(e.target.value)} />
-

Select Columns

- {[ - { key: 'bookingRef', label: 'Booking Reference' }, - { key: 'passenger', label: 'Passenger' }, - { key: 'status', label: 'Status' }, - { key: 'bookingType', label: 'Booking Type' }, - { key: 'passengerCount', label: 'Passenger Count' }, - { key: 'totalMinor', label: 'Amount' }, - { key: 'paymentStatus', label: 'Payment Status' }, - { key: 'createdAt', label: 'Created At' }, - ].map((col) => ( + {BOOKING_COLS.map((col) => ( ))}
-
setExportModalOpen(false)}>Cancel Export CSV diff --git a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx index ab588b085..015eaf3a1 100644 --- a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Download, Eye, Trash2 } from 'lucide-react'; +import { Download, Eye, Trash2, ShieldCheck, ShieldOff, Star, Wallet } from 'lucide-react'; import DataTable from '@/components/ui/DataTable'; import Badge from '@/components/ui/Badge'; import Pagination from '@/components/ui/Pagination'; @@ -13,13 +13,28 @@ import { passengersApi, apiClient } from '@/lib/api'; import { formatDate, formatDateTime } from '@/lib/utils'; import { PassengerFilters } from '@/types'; +const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => ( +
+

{label}

+

{value || '—'}

+
+); + +const SectionHeader = ({ title }: { title: string }) => ( +

+ {title} +

+); + +const TIER_COLORS: Record = { + BRONZE: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800', + SILVER: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600', + GOLD: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800', + PLATINUM: 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800', +}; + export default function PassengersPage() { - const [filters, setFilters] = useState({ - page: 1, - pageSize: 20, - search: '', - role: 'PASSENGER', - }); + const [filters, setFilters] = useState({ page: 1, pageSize: 20, search: '', role: 'PASSENGER' }); const [selectedPassenger, setSelectedPassenger] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; passenger: any | null }>({ isOpen: false, passenger: null }); const [exportModalOpen, setExportModalOpen] = useState(false); @@ -33,35 +48,23 @@ export default function PassengersPage() { const deleteMutation = useMutation({ mutationFn: (id: string) => apiClient.delete(`/passengers/${id}`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['passengers'] }); - }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['passengers'] }), }); - const handleDelete = (passenger: any) => { - setDeleteConfirm({ isOpen: true, passenger }); - }; - - const confirmDelete = async () => { - if (deleteConfirm.passenger) { - await deleteMutation.mutateAsync(deleteConfirm.passenger.id); - setDeleteConfirm({ isOpen: false, passenger: null }); - } - }; - const { data, isLoading, error } = useQuery({ queryKey: ['passengers', filters], queryFn: () => passengersApi.getAll(filters), }); - if (error) { - console.error('Passengers API Error:', error); - } + const PASSENGER_COLS = [ + { key: 'fullName', label: 'Full Name' }, { key: 'email', label: 'Email' }, { key: 'phone', label: 'Phone' }, + { key: 'dateOfBirth', label: 'Date of Birth' }, { key: 'gender', label: 'Gender' }, + { key: 'nationality', label: 'Nationality' }, { key: 'verified', label: 'Verified' }, + ]; const confirmExportPassengers = () => { const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k); - if (cols.length === 0) { alert('Please select at least one column'); return; } - + if (!cols.length) { alert('Please select at least one column'); return; } const exportItems = (data?.items || []).filter((p: any) => { if (!exportDateFrom && !exportDateTo) return true; const d = p.createdAt ? new Date(p.createdAt).toISOString().split('T')[0] : null; @@ -69,26 +72,24 @@ export default function PassengersPage() { if (exportDateTo && (!d || d > exportDateTo)) return false; return true; }); - const csv = [ - cols.join(','), - ...exportItems.map((passenger: any) => { - const values = cols.map(col => { - switch (col) { - case 'fullName': return passenger.fullName; - case 'email': return passenger.email || ''; - case 'phone': return passenger.phone || ''; - case 'dateOfBirth': return passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : ''; - case 'gender': return passenger.gender || ''; - case 'nationality': return passenger.nationality || ''; - case 'verified': return passenger.nationalId ? 'Yes' : 'No'; + PASSENGER_COLS.map(c => `"${c.label}"`).join(','), + ...exportItems.map((p: any) => { + const values = PASSENGER_COLS.filter(c => cols.includes(c.key)).map(({ key }) => { + switch (key) { + case 'fullName': return p.fullName; + case 'email': return p.email || ''; + case 'phone': return p.phone || ''; + case 'dateOfBirth': return p.dateOfBirth ? formatDate(p.dateOfBirth) : ''; + case 'gender': return p.gender || ''; + case 'nationality': return p.nationality || ''; + case 'verified': return p.nationalId ? 'Yes' : 'No'; default: return ''; } }); return values.map(v => `"${v}"`).join(','); }), ].join('\n'); - const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -99,65 +100,32 @@ export default function PassengersPage() { }; const columns = [ - { - key: 'fullName', - label: 'Name', - sortable: true, - render: (passenger: any) => ( + { + key: 'fullName', label: 'Name', sortable: true, + render: (p: any) => (
-
{passenger.fullName}
-
{passenger.email}
+
{p.fullName}
+
{p.email}
), }, - { - key: 'phone', - label: 'Phone', - sortable: true, - render: (passenger: any) => passenger.phone, - }, - { - key: 'gender', - label: 'Gender', - sortable: true, - render: (passenger: any) => passenger.gender || 'N/A', - }, - { - key: 'nationality', - label: 'Nationality', - sortable: true, - render: (passenger: any) => passenger.nationality || 'N/A', - }, - { - key: 'dateOfBirth', - label: 'Date of Birth', - sortable: true, - render: (passenger: any) => passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : 'N/A', - }, - { - key: 'verified', - label: 'Status', - render: (passenger: any) => ( - - {passenger.nationalId ? 'Verified' : 'Unverified'} + { key: 'phone', label: 'Phone', sortable: true, render: (p: any) => p.phone }, + { key: 'gender', label: 'Gender', sortable: true, render: (p: any) => p.gender || 'N/A' }, + { key: 'nationality', label: 'Nationality', sortable: true, render: (p: any) => p.nationality || 'N/A' }, + { key: 'dateOfBirth', label: 'Date of Birth', sortable: true, render: (p: any) => p.dateOfBirth ? formatDate(p.dateOfBirth) : 'N/A' }, + { + key: 'verified', label: 'Status', + render: (p: any) => ( + + {p.nationalId ? 'Verified' : 'Unverified'} ), }, ]; const actions = [ - { - label: 'View Details', - onClick: (passenger: any) => setSelectedPassenger(passenger), - variant: 'secondary' as const, - icon: Eye, - }, - { - label: 'Delete', - onClick: handleDelete, - variant: 'danger' as const, - icon: Trash2, - }, + { label: 'View Details', onClick: (p: any) => setSelectedPassenger(p), variant: 'secondary' as const, icon: Eye }, + { label: 'Delete', onClick: (p: any) => setDeleteConfirm({ isOpen: true, passenger: p }), variant: 'danger' as const, icon: Trash2 }, ]; return ( @@ -167,9 +135,7 @@ export default function PassengersPage() {

Passengers

Manage passenger profiles and verification

-
- setExportModalOpen(true)}>Export -
+ setExportModalOpen(true)}>Export
@@ -180,254 +146,218 @@ export default function PassengersPage() { )}
- setFilters({ ...filters, search: e.target.value, page: 1 })} - /> + setFilters({ ...filters, search: e.target.value, page: 1 })} />
- setFilters({ ...filters, verified: e.target.value ? e.target.value === 'true' : undefined, page: 1 })}>
- - - + {data?.meta && ( - setFilters({ ...filters, page })} - /> + setFilters({ ...filters, page })} /> )}
- {/* Delete Confirmation */} setDeleteConfirm({ isOpen: false, passenger: null })} - onConfirm={confirmDelete} + onConfirm={async () => { + if (deleteConfirm.passenger) { + await deleteMutation.mutateAsync(deleteConfirm.passenger.id); + setDeleteConfirm({ isOpen: false, passenger: null }); + } + }} title="Delete Passenger" message={`Are you sure you want to delete ${deleteConfirm.passenger?.fullName}?`} - confirmText="Delete" - isDanger={true} + confirmText="Delete" isDanger warning="This passenger may have active bookings, loyalty points, and wallet balance. Deleting will impact these systems and records." /> {/* Passenger Details Modal */} - setSelectedPassenger(null)} - title="Passenger Details" - size="xl" - > - {selectedPassenger && ( -
- {/* Personal Information */} + setSelectedPassenger(null)} title="Passenger Details" size="xl"> + {selectedPassenger && (() => { + const p = selectedPassenger; + const isVerified = !!p.faydaVerified || !!p.nationalId; + const tier = p.passenger?.loyalty?.tier || p.loyalty?.tier; + const tierColor = TIER_COLORS[tier] || TIER_COLORS.BRONZE; + + return (
-

Personal Information

-
-
- -

{selectedPassenger.fullName}

-
-
- -

- {selectedPassenger.dateOfBirth ? formatDate(selectedPassenger.dateOfBirth) : 'N/A'} -

-
-
- -

{selectedPassenger.gender || 'N/A'}

-
-
- -

{selectedPassenger.nationality || 'N/A'}

-
-
-
- -
- - {/* Contact Information */} -
-

Contact Information

-
-
- -

{selectedPassenger.email || 'N/A'}

-
-
- -

{selectedPassenger.phone || 'N/A'}

-
-
-
- -
- - {/* Identification */} -
-

Identification

-
-
- -

{selectedPassenger.passportNumber || 'N/A'}

-
-
- -

{selectedPassenger.passportCountry || 'N/A'}

-
-
- -
- - {selectedPassenger.nationalId ? 'Verified' : 'Unverified'} - + {/* Gradient header with avatar */} +
+
+
+ {(p.fullName || p.email || '?')[0].toUpperCase()} +
+
+

{p.fullName}

+

{p.email}

+
+
+
+ + {isVerified ? '✓ Verified' : 'Unverified'} + +
+ {tier && ( + + {tier} + + )}
-
-
-
- - {/* Account Information */} -
-

Account Information

-
-
- -

{selectedPassenger.id}

-
-
- -

{selectedPassenger.userId || 'N/A'}

-
-
-
- - {/* Loyalty & Wallet (if available) */} - {(selectedPassenger.loyalty || selectedPassenger.wallet) && ( - <> -
-
- {selectedPassenger.loyalty && ( -
-

Loyalty Account

-
-
- -

{selectedPassenger.loyalty.tier || 'N/A'}

-
-
- -

{selectedPassenger.loyalty.pointsBalance || 0}

-
-
+ {/* Quick stats */} +
+ {[ + { label: 'Loyalty Points', value: (p.passenger?.loyalty?.pointsBalance ?? p.loyalty?.pointsBalance ?? 0).toLocaleString() }, + { label: 'Wallet Balance', value: p.passenger?.wallet || p.wallet ? `ETB ${((p.passenger?.wallet?.balanceMinor ?? p.wallet?.balanceMinor ?? 0) / 100).toFixed(2)}` : '—' }, + { label: 'Nationality', value: p.nationality || '—' }, + ].map(({ label, value }) => ( +
+

{label}

+

{value}

- )} - {selectedPassenger.wallet && ( -
-

Wallet

-
-
- -

- {(selectedPassenger.wallet.balanceMinor / 100).toFixed(2)} {selectedPassenger.wallet.currency} -

-
-
-
- )} -
- - )} - -
- - {/* Timestamps */} -
-

Timestamps

-
-
- -

{selectedPassenger.createdAt ? formatDateTime(selectedPassenger.createdAt) : 'N/A'}

-
-
- -

{selectedPassenger.updatedAt ? formatDateTime(selectedPassenger.updatedAt) : 'N/A'}

+ ))}
-
-
- setSelectedPassenger(null)} - > - Close - +
+ {/* Personal */} +
+ +
+ + + + + + + + +
+
+ + {/* Contact */} +
+ +
+ + + +
+
+ + {/* Identification */} +
+ +
+
+

Fayda (National ID)

+
+ {isVerified + ? + : } + + {isVerified ? 'Verified' : 'Not verified'} + +
+ {p.faydaVerifiedAt &&

{formatDateTime(p.faydaVerifiedAt)}

} +
+ + + +
+
+ + {/* Loyalty & Wallet */} + {(p.passenger?.loyalty || p.loyalty || p.passenger?.wallet || p.wallet) && ( +
+ +
+ {(p.passenger?.loyalty || p.loyalty) && (() => { + const loyalty = p.passenger?.loyalty || p.loyalty; + return ( + <> +
+

Tier

+
+ + {loyalty.tier} +
+
+ + + + ); + })()} + {(p.passenger?.wallet || p.wallet) && (() => { + const wallet = p.passenger?.wallet || p.wallet; + return ( +
+

Wallet Balance

+

+ ETB {((wallet.balanceMinor ?? 0) / 100).toFixed(2)} +

+
+ ); + })()} +
+
+ )} + + {/* Account */} +
+ +
+ + +
+
+ + {/* Timestamps */} +
+ +
+ + + +
+
+
+ +
+ setSelectedPassenger(null)}>Close +
-
- )} + ); + })()} - {/* Export Modal */} + setExportModalOpen(false)} title="Export Passengers" size="md">
-
- - setExportDateFrom(e.target.value)} /> -
-
- - setExportDateTo(e.target.value)} /> -
+
setExportDateFrom(e.target.value)} />
+
setExportDateTo(e.target.value)} />
-

Select Columns

- {[ - { key: 'fullName', label: 'Full Name' }, - { key: 'email', label: 'Email' }, - { key: 'phone', label: 'Phone' }, - { key: 'dateOfBirth', label: 'Date of Birth' }, - { key: 'gender', label: 'Gender' }, - { key: 'nationality', label: 'Nationality' }, - { key: 'verified', label: 'Verified' }, - ].map((col) => ( + {PASSENGER_COLS.map((col) => ( ))}
-
setExportModalOpen(false)}>Cancel Export CSV diff --git a/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx b/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx index 2c1c8bcf5..c6a5cbc5b 100644 --- a/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx @@ -28,6 +28,15 @@ export default function PaymentsPage() { }), }); + const PAYMENT_COLS = [ + { key: 'reference', label: 'Reference' }, + { key: 'booking', label: 'Booking Reference' }, + { key: 'amount', label: 'Amount' }, + { key: 'method', label: 'Payment Method' }, + { key: 'status', label: 'Status' }, + { key: 'createdAt', label: 'Created At' }, + ]; + const confirmExport = () => { const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k); if (cols.length === 0) { alert('Please select at least one column'); return; } @@ -42,16 +51,16 @@ export default function PaymentsPage() { }); const csv = [ - cols.join(','), + PAYMENT_COLS.map(c => `"${c.label}"`).join(','), ...exportItems.map((payment: any) => { - const values = cols.map(col => { - switch (col) { + const values = PAYMENT_COLS.filter(c => cols.includes(c.key)).map(({ key }) => { + switch (key) { case 'reference': return payment.reference || payment.id?.substring(0, 8) || ''; - case 'booking': return payment.booking?.bookingRef || 'N/A'; - case 'amount': return formatCurrency(payment.amountMinor, payment.currency); - case 'method': return payment.method || ''; - case 'status': return payment.status || ''; - case 'createdAt': return payment.createdAt || ''; + case 'booking': return payment.booking?.bookingRef || 'N/A'; + case 'amount': return formatCurrency(payment.amountMinor, payment.currency); + case 'method': return payment.method || ''; + case 'status': return payment.status || ''; + case 'createdAt': return payment.createdAt ? new Date(payment.createdAt).toLocaleString() : ''; default: return ''; } }); @@ -84,7 +93,7 @@ export default function PaymentsPage() {

Payments

Manage payment transactions and refunds

- setExportModalOpen(true)}>Export + setExportModalOpen(true)}>Export
diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx index 353b1fd3a..ceeb078d1 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx @@ -239,9 +239,9 @@ export default function ReportsPage() { b.status === 'CONFIRMED').length }, - { name: 'Completed', value: bookings.filter((b: any) => b.status === 'COMPLETED').length }, + { name: 'Completed', value: bookings.filter((b: any) => b.status === 'BOARDED').length }, { name: 'Cancelled', value: bookings.filter((b: any) => b.status === 'CANCELLED').length }, - { name: 'Other', value: bookings.filter((b: any) => !['CONFIRMED', 'COMPLETED', 'CANCELLED'].includes(b.status)).length }, + { name: 'Other', value: bookings.filter((b: any) => !['CONFIRMED', 'BOARDED', 'CANCELLED'].includes(b.status)).length }, ].filter(d => d.value > 0)} cx="50%" cy="50%" @@ -306,7 +306,7 @@ export default function ReportsPage() {

Completed Bookings

-

{bookings.filter((b: any) => b.status === 'COMPLETED').length}

+

{bookings.filter((b: any) => b.status === 'BOARDED').length}

Cancelled Bookings

diff --git a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx index 33a03e8f5..60a7f80b8 100644 --- a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx @@ -563,7 +563,7 @@ export default function SchedulesPage() { {trains.map((train: Train) => ( ))} @@ -580,7 +580,7 @@ export default function SchedulesPage() { {routes.map((route: Route) => ( ))} diff --git a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx index cb26bd431..82db4e9af 100644 --- a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx @@ -443,13 +443,12 @@ export default function SeatsPage() { className="input" > - {schedules.map((schedule: any) => { - const trainNumber = schedule.train?.trainNumber || schedule.train?.name || 'N/A'; + {schedules.map((schedule: any) => { const routeName = schedule.route?.name || 'N/A'; const date = schedule.departureAt ? new Date(schedule.departureAt).toLocaleDateString() : 'N/A'; return ( ); })} diff --git a/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx b/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx index 97a1cf7ba..50d7ab822 100644 --- a/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx @@ -72,8 +72,8 @@ export default function StationsPage() { name: formData.get('name') as string, city: formData.get('city') as string, countryCode: formData.get('countryCode') as string, - lat: parseFloat(formData.get('lat') as string) || null, - lng: parseFloat(formData.get('lng') as string) || null, + lat: parseFloat(formData.get('lat') as string) || undefined, + lng: parseFloat(formData.get('lng') as string) || undefined, timezone: formData.get('timezone') as string, sequence, isOperational: formData.get('isOperational') === 'true', @@ -304,28 +304,6 @@ export default function StationsPage() {
-
- - -
-
- - -