diff --git a/README.md b/README.md index 125561b9d..50e0b55f6 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ pnpm --filter @edr/passenger-api run prisma:seed - 3 User accounts (Admin, Passenger, Agent) - Fare rules for ADULT and CHILD passenger categories - Currency exchange rates (ETB, DJF, USD) -- Baggage allowance rules +- Luggage allowance rules - Notification templates - Promotions and FAQ content - Menu items and station crowd signals diff --git a/apps/edr-passenger-api/src/main.ts b/apps/edr-passenger-api/src/main.ts index cbdae37e3..26f57e506 100644 --- a/apps/edr-passenger-api/src/main.ts +++ b/apps/edr-passenger-api/src/main.ts @@ -60,7 +60,7 @@ async function bootstrap() { Enterprise-grade REST API for the Ethio-Djibouti Railway passenger booking and management platform. Built with NestJS, TypeScript, PostgreSQL, and Prisma ORM. ## Latest Updates -- **Enhanced Module Coverage:** Complete API coverage with 25+ core modules including System Config, Excess Baggage, Packages, and comprehensive CRUD operations across all entities. +- **Enhanced Module Coverage:** Complete API coverage with 25+ core modules including System Config, Excess Luggage, Packages, and comprehensive CRUD operations across all entities. - **Health Check Endpoints:** Three public probes added under \`/health\`. Liveness (\`GET /health\`), readiness with live DB ping (\`GET /health/ready\`), and app info (\`GET /health/info\`). All are exempt from rate limiting. - **Rate Limiting:** Global throttle enforced via ThrottlerGuard with three named tiers: auth (5 req/min on \`/auth\` and \`/fayda/verification\`), strict (20 req/min on \`/bookings\`, \`/passengers\`, \`/payments\`, \`/wallet\`), default (100 req/min everywhere else). Health probes, webhook handlers, and internal service endpoints are exempt. - **Boarding Pass on Gate Validation:** Every successful gate validation at \`POST /tickets/:ref/validate\` now automatically delivers a boarding pass to the passenger via email (full HTML with QR code, route, seat table) and SMS (compact text with ref, route, seats, barcode). The leg label (OUTBOUND, RETURN, LEG1, etc.) is included so passengers know which boarding it covers. @@ -328,7 +328,7 @@ Payment providers send notifications to: "JWT-auth", ) .addTag("Agents", "Counter booking, shift management, commission tracking, and reconciliation") - .addTag("Excess Baggage", "IAM-protected agent/supervisor endpoints to log excess baggage charges, waive fees, resend payment links, and manage allowance rules per seat class. Public token-based endpoints let passengers self-pay outstanding charges.") + .addTag("Excess Luggage", "IAM-protected agent/supervisor endpoints to log excess baggage charges, waive fees, resend payment links, and manage allowance rules per seat class. Public token-based endpoints let passengers self-pay outstanding charges.") .addTag("Packages", "Bundled travel packages with tiered pricing. Public endpoints for browsing and booking; JWT-authenticated endpoints for purchase history; IAM-protected endpoints for admin CRUD and tier management.") .addTag("Config", "System-wide configuration management including feature flags, maintenance modes, and operational parameters. IAM-protected endpoints for administrative control.") .addTag("Audit", "User activity logging, system changes, compliance tracking, and audit trails") 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 a2ea23e09..50d953010 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -17,6 +17,7 @@ function generateRef(): string { return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } + /** * For package round-trip bookings, totalMinor in the DB may have been stored as a * single-leg amount before the server fix. Recompute from the tier price when needed. 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 1025a42b3..5d773860c 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,7 +14,7 @@ const BOOKING_CUTOFF_MS = 30 * 60 * 1000; function generateRef(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - return 'EDR-' + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); + return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } // Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx) diff --git a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts index e65a6be68..8d01e7113 100644 --- a/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts +++ b/apps/edr-passenger-api/src/modules/excess-baggage/excess-baggage.controller.ts @@ -17,7 +17,7 @@ class UpsertBaggageAllowanceDto { } // ── IAM-protected agent/supervisor routes ──────────────────────────────────── -@ApiTags('Excess Baggage') +@ApiTags('Excess Luggage') @Controller('agents/excess-baggage') @UseGuards(IamJwtGuard) @ApiBearerAuth('IAM-auth') @@ -100,7 +100,7 @@ export class ExcessBaggageAgentController { } // ── Public pay-by-token routes (passenger self-service) ────────────────────── -@ApiTags('Excess Baggage') +@ApiTags('Excess Luggage') @Controller('excess-baggage') export class ExcessBaggagePublicController { constructor(private service: ExcessBaggageService) {} diff --git a/apps/edr-passenger-api/src/modules/search/search.module.ts b/apps/edr-passenger-api/src/modules/search/search.module.ts index baadcf90c..b7788c2fe 100644 --- a/apps/edr-passenger-api/src/modules/search/search.module.ts +++ b/apps/edr-passenger-api/src/modules/search/search.module.ts @@ -4,9 +4,10 @@ import { SearchService } from './search.service'; import { CurrencyModule } from '../currency/currency.module'; import { FareEngineModule } from '../fare-engine/fare-engine.module'; import { SegmentsModule } from '../segments/segments.module'; +import { SystemConfigModule } from '../system-config/system-config.module'; @Module({ - imports: [CurrencyModule, FareEngineModule, SegmentsModule], + imports: [CurrencyModule, FareEngineModule, SegmentsModule, SystemConfigModule], controllers: [SearchController], providers: [SearchService], exports: [SearchService], 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 355446544..a02c7b7e3 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -6,6 +6,7 @@ import { FareEngineService } from '../fare-engine/fare-engine.service'; import { SegmentsService } from '../segments/segments.service'; import { resolveCurrencyFromNationality } from '../fare-engine/fare-engine.dto'; import { Currency } from '@prisma/client'; +import { SystemConfigService, CONFIG_KEYS } from '../system-config/system-config.service'; const POINTS_TO_MINOR = 10; @@ -40,8 +41,13 @@ export class SearchService { private currencyService: CurrencyService, private fareEngine: FareEngineService, private segmentsService: SegmentsService, + private systemConfig: SystemConfigService, ) {} + private async getCutoffHours(): Promise { + return this.systemConfig.getNumber(CONFIG_KEYS.HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE); + } + async searchTrips(dto: SearchTripsDto) { const [direct, transit] = await Promise.all([ this.searchSchedules( @@ -166,13 +172,14 @@ export class SearchService { const schedules = await this.prisma.trainSchedule.findMany({ where: { - status: { in: ['SCHEDULED', 'BOARDING'] }, + status: 'SCHEDULED', isPackageOnly: false, OR: [ { departureAt: { gte: windowStart, lt: requestedDate } }, { departureAt: { gte: requestedNextDay < now ? now : requestedNextDay, lt: windowEnd } }, ], stopTimes: { some: { stationId: originStationId } }, + coachAssignments: { some: {} }, }, include: SCHEDULE_INCLUDE, orderBy: { departureAt: 'asc' }, @@ -183,7 +190,7 @@ export class SearchService { this.buildScheduleResult(schedule as any, originStationId, destinationStationId, totalPassengers, nationality) ) ); - return results.filter(Boolean); + return results.filter((r): r is NonNullable => !!r && r.hasAvailability); } private async searchSchedules( @@ -200,12 +207,17 @@ export class SearchService { const now = new Date(); const totalPassengers = adultCount + (childCount ?? 0); + const cutoffHours = await this.getCutoffHours(); + const cutoffThreshold = new Date(now.getTime() + cutoffHours * 60 * 60 * 1000); + const earliest = new Date(Math.max((date < now ? now : date).getTime(), cutoffThreshold.getTime())); + const schedules = await this.prisma.trainSchedule.findMany({ where: { - status: { in: ['SCHEDULED', 'BOARDING'] }, + status: 'SCHEDULED', isPackageOnly: false, - departureAt: { gte: date < now ? now : date, lt: nextDay }, + departureAt: { gte: earliest, lt: nextDay }, stopTimes: { some: { stationId: originStationId } }, + coachAssignments: { some: {} }, }, include: SCHEDULE_INCLUDE, }); @@ -215,7 +227,7 @@ export class SearchService { this.buildScheduleResult(schedule as any, originStationId, destinationStationId, totalPassengers, nationality) ) ); - return results.filter(Boolean); + return results.filter((r): r is NonNullable => !!r && r.hasAvailability); } // ── Transit search ───────────────────────────────────────────────────────── @@ -241,26 +253,31 @@ export class SearchService { const [leg1Schedules, allCandidates] = await Promise.all([ this.prisma.trainSchedule.findMany({ where: { - status: { in: ['SCHEDULED', 'BOARDING'] }, + status: 'SCHEDULED', isPackageOnly: false, departureAt: { gte: dayStart, lt: dayEnd }, stopTimes: { some: { stationId: originStationId } }, + coachAssignments: { some: {} }, }, include: SCHEDULE_INCLUDE, }), this.prisma.trainSchedule.findMany({ where: { - status: { in: ['SCHEDULED', 'BOARDING'] }, + status: 'SCHEDULED', isPackageOnly: false, departureAt: { gte: dayStart, lt: leg2WindowEnd }, + coachAssignments: { some: {} }, }, include: SCHEDULE_INCLUDE, }), ]); + const cutoffHours = await this.getCutoffHours(); + const cutoffThreshold = new Date(Date.now() + cutoffHours * 60 * 60 * 1000); + const results: any[] = []; - for (const leg1 of leg1Schedules as ScheduleWithIncludes[]) { + for (const leg1 of (leg1Schedules as ScheduleWithIncludes[]).filter(s => new Date(s.departureAt) > cutoffThreshold)) { const originStop = leg1.stopTimes.find(s => s.stationId === originStationId); if (!originStop) continue; @@ -354,6 +371,9 @@ export class SearchService { .map((s: any) => s.id as string) ); + // Exclude schedules with no seats at all + if (allValidSeatIds.length === 0) return null; + // Run availability batch and fare calculation in parallel const [freeSeats, faresByClass] = await Promise.all([ this.segmentsService.getFreeSeatIds( diff --git a/apps/edr-passenger-web/backoffice/public/docs.md b/apps/edr-passenger-web/backoffice/public/docs.md index 0a4922fa9..428c365ae 100644 --- a/apps/edr-passenger-web/backoffice/public/docs.md +++ b/apps/edr-passenger-web/backoffice/public/docs.md @@ -34,7 +34,7 @@ The Passenger Backoffice Application is a comprehensive management system for th - **Live Tracking**: Monitor trip status and real-time updates - **Security Monitoring**: Fraud detection and audit logging - **Comprehensive Analytics**: Revenue, occupancy, and performance reports -- **🆕 Excess Baggage Management**: Handle boarding baggage charges with agent tools +- **🆕 Excess Luggage Management**: Handle boarding baggage charges with agent tools - **🆕 Travel Packages**: Manage pilgrimage and group travel packages with tiered pricing - **🆕 System Health Monitoring**: Real-time API health checks and system status - **🆕 Advanced Fare Configuration**: Dynamic pricing with segment-based rules @@ -94,7 +94,7 @@ The application is organized into 8 main sections: ├── System Config └── Settings └── Enhanced Features - ├── Excess Baggage + ├── Excess Luggage ├── Travel Packages ├── Package Inquiries ├── Health Monitoring @@ -2862,7 +2862,7 @@ Action: Block user ### Version 1.0.0 (January 15, 2026) - **Complete Platform Release** - Full-featured passenger management system -- **Excess Baggage Management** - Complete boarding baggage handling with agent tools and passenger self-pay options +- **Excess Luggage Management** - Complete boarding baggage handling with agent tools and passenger self-pay options - **Travel Packages** - Pilgrimage and group travel packages with tiered pricing, capacity management, and inquiry handling - **System Health Monitoring** - Real-time API health checks with liveness, readiness, and performance metrics - **System Configuration** - Centralized config management with feature flags, rate limiting, and operational controls @@ -2920,7 +2920,7 @@ Action: Block user ## Enhanced Features -### Excess Baggage +### Excess Luggage **Purpose**: Manage excess baggage charges at boarding with agent tools and passenger self-pay **Access Level**: Agent, Supervisor, Admin @@ -2941,7 +2941,7 @@ Action: Block user └──────────────────────────────┘ ``` -#### Excess Baggage Process +#### Excess Luggage Process 1. **At Boarding**: Agent weighs passenger baggage 2. **If Excess**: Agent creates charge in system @@ -2962,8 +2962,8 @@ Action: Block user ##### READ (List Charges) -1. **Access Excess Baggage Page**: - - Click **Excess Baggage** in Enhanced Features section +1. **Access Excess Luggage Page**: + - Click **Excess Luggage** in Enhanced Features section - Shows all baggage charges 2. **Search & Filter**: @@ -3000,7 +3000,7 @@ Action: Block user #### Agent Workflow -1. **Weigh Baggage**: Use station scales +1. **Weigh Luggage**: Use station scales 2. **Check Allowance**: Compare to passenger's seat class allowance 3. **Create Charge**: If excess weight found 4. **Offer Payment Options**: diff --git a/apps/edr-passenger-web/backoffice/src/app/classes/page.tsx b/apps/edr-passenger-web/backoffice/src/app/classes/page.tsx index 348268ca2..c6d9af92b 100644 --- a/apps/edr-passenger-web/backoffice/src/app/classes/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/classes/page.tsx @@ -311,41 +311,25 @@ export default function ClassesPage() {

Per-km distance-based fare rate

-
-
- - -

Flat fee per passenger (e.g., lounge access, extra legroom)

-
- -
- - -

Flat fee per passenger (e.g., travel insurance)

-
+ +
+ + +

Flat fee per passenger (e.g., travel insurance)

Total Fare Calculation:

-

Total = (Base Fare × Distance) + Premium + Insurance

-

• Premium applies per passenger

-

• Insurance applies per passenger

+

Total = (Base Fare × Distance) + Insurance

+

• Insurance applies per passenger

diff --git a/apps/edr-passenger-web/backoffice/src/app/currencies/page.tsx b/apps/edr-passenger-web/backoffice/src/app/currencies/page.tsx index 4cf393713..cee6944f6 100644 --- a/apps/edr-passenger-web/backoffice/src/app/currencies/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/currencies/page.tsx @@ -220,13 +220,6 @@ export default function CurrenciesPage() { )} -
-

How it works

-

• ETB is the transaction currency — all fares are stored in ETB minor units (1 ETB = 100 minor)

-

• DJF and USD rates are used to display prices to passengers in their preferred currency

-

• Rates apply globally; changes take effect immediately on the next booking or fare quote

-
- { setShowAddModal(false); setError(null); }} diff --git a/apps/edr-passenger-web/backoffice/src/app/docs/sections/OperationsSection.tsx b/apps/edr-passenger-web/backoffice/src/app/docs/sections/OperationsSection.tsx index 7c14cc357..0f9b9beb5 100644 --- a/apps/edr-passenger-web/backoffice/src/app/docs/sections/OperationsSection.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/docs/sections/OperationsSection.tsx @@ -82,7 +82,7 @@ export default function OperationsSection() { {/* EXCESS BAGGAGE */}
-

📦 Luggage (Excess Baggage)

+

📦 Luggage (Excess Luggage)

Handle excess baggage charges at boarding — passenger self-pay or agent cash collection. Access level: Agent, Supervisor, Admin.

{['PENDING','PAID','CASH_COLLECTED','EXPIRED','WAIVED'].map(s => ( @@ -91,7 +91,7 @@ export default function OperationsSection() {
-

📦 How-To: Handle Excess Baggage

+

📦 How-To: Handle Excess Luggage

  1. Click Luggage in Operations
  2. Search by booking reference; filter by status or date
diff --git a/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx b/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx index 3e221839b..73ce87383 100644 --- a/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx @@ -609,7 +609,7 @@ export default function PricingPage() { className={`px-4 py-2 font-medium border-b-2 transition-colors ${tab === 'baggage' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground' }`} > - Excess Baggage Rates + Excess Luggage Rates
@@ -765,24 +765,6 @@ export default function PricingPage() {
-
-

Pricing Structure

-
    -
  • - • Segment Fares: Set fares for specific stop-to-stop segments (e.g., Addis → Dire Dawa) -
  • -
  • - • Schedule Fares: Set custom pricing for each schedule by seat class and passenger type -
  • -
  • - • Passenger Type: ADULT (5+ years) or CHILD (<5) — first child travels free, subsequent children pay full fare -
  • -
  • - • Nationality-based: Override fares for specific nationalities (Ethiopian, Djiboutian, Other) -
  • -
-
- {/* Delete Confirmation */}
- {/* Baggage Allowance Modal */} + {/* Luggage Allowance Modal */} { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }} title={editingAllowance ? 'Edit Allowance Rule' : 'Add Allowance Rule'} size="md">
{baggageError &&
{baggageError}
} 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 a5ed35666..413383b7a 100644 --- a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx @@ -52,15 +52,28 @@ export default function SeatsPage() { queryFn: async () => { if (!selectedRoute) return null; const template: any[] = await routeCoachTemplatesApi.get(selectedRoute); - if (!template?.length) return []; - const fullCoaches = await Promise.all( - template.map((entry: any) => fleetApi.getCoach(entry.coachId ?? entry.coach?.id)) - ); - return fullCoaches.map((coach: any, i: number) => ({ + if (template?.length) { + const fullCoaches = await Promise.all( + template.map((entry: any) => fleetApi.getCoach(entry.coachId ?? entry.coach?.id)) + ); + return fullCoaches.map((coach: any, i: number) => ({ + ...coach, + coachNumber: coach.number, + positionNumber: template[i].positionNumber, + seatArrangement: coach.arrangement, + })); + } + // No template — fetch coaches from the most recent schedule for this route + const schedules: any = await schedulesApi.getAll({ routeId: selectedRoute }); + const scheduleList: any[] = schedules?.items || schedules?.data || (Array.isArray(schedules) ? schedules : []); + if (!scheduleList.length) return []; + const latestSchedule = scheduleList[scheduleList.length - 1]; + const seatMap: any = await seatsApi.getSeatMap(latestSchedule.id); + return (seatMap?.coaches || []).map((coach: any, i: number) => ({ ...coach, - coachNumber: coach.number, - positionNumber: template[i].positionNumber, - seatArrangement: coach.arrangement, + coachNumber: coach.number ?? coach.coachNumber, + positionNumber: coach.positionNumber ?? i + 1, + seatArrangement: coach.arrangement ?? coach.seatArrangement, })); }, enabled: !!selectedRoute, diff --git a/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx index bc8f31eaf..7b5ff3102 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx @@ -237,16 +237,6 @@ export default function TariffRatesPage() {
- {/* Tariff reference card */} -
-

Official Tariff Formula

-

- Fare = KM × rate × 1.02 × ExchangeRate -

-

- Rate is stored as baseFareMinor = tariff_decimal × 100,000 (e.g. 0.03 → 3000). The ×1.02 insurance coefficient is applied automatically by the fare engine. -

-
diff --git a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx index 53ae0584a..275291415 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx @@ -884,11 +884,11 @@ export default function TicketsPage() { })()} - {/* Excess Baggage Modal */} + {/* Excess Luggage Modal */} { setExcessModalOpen(false); setExcessTicket(null); setExcessResult(null); }} - title="Log Excess Baggage" + title="Log Excess Luggage" size="sm" > {excessResult ? ( diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts index 9f3764227..7824cef07 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts @@ -444,7 +444,7 @@ export const packageInquiriesApi = { remove: (id: string) => apiClient.delete(`/packages/inquiries/${id}`), }; -// Excess Baggage API +// Excess Luggage API export const excessBaggageApi = { logCharge: (data: any) => apiClient.post('/agents/excess-baggage', data), getCharge: (id: string) => apiClient.get(`/agents/excess-baggage/${id}`), diff --git a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx index 7b3463355..786196731 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx @@ -26,6 +26,20 @@ import { import { useEffect, useRef, useState, useCallback } from "react"; import ModernDatePicker from "@/components/ModernDatePicker"; +function useDarkMode() { + const [dark, setDark] = useState(() => + typeof window !== 'undefined' && document.documentElement.classList.contains('dark') + ); + useEffect(() => { + const obs = new MutationObserver(() => + setDark(document.documentElement.classList.contains('dark')) + ); + obs.observe(document.documentElement, { attributeFilter: ['class'] }); + return () => obs.disconnect(); + }, []); + return dark; +} + const searchSchema = z .object({ tripType: z.enum(["ONE_WAY", "ROUND_TRIP"]), @@ -535,6 +549,7 @@ export default function SearchPage() { const queryClient = useQueryClient(); const { user, isAuthenticated } = useAuthStore(); + const dark = useDarkMode(); const [passengerModalOpen, setPassengerModalOpen] = useState(false); const [promoVisible, setPromoVisible] = useState(false); const [promoCode, setPromoCode] = useState(""); @@ -922,12 +937,14 @@ export default function SearchPage() { ? "border-red-400" : originId ? "border-primary bg-primary/5" - : "border-gray-200" + : "border-gray-200 dark:border-gray-700" }`} + style={{ backgroundColor: originId ? undefined : undefined }} > {originStation?.name ?? "Select departure"} @@ -969,12 +986,13 @@ export default function SearchPage() { ? "border-red-400" : destId ? "border-primary bg-primary/5" - : "border-gray-200" + : "border-gray-200 dark:border-gray-700" }`} > {destStation?.name ?? "Select destination"} @@ -1042,11 +1060,11 @@ export default function SearchPage() { - {isOpen &&
{item.answer}
} -
+ toggle(key)} + /> ); })}
- )) - )} + + ))} - + )} + -
-
-
- -
-

{t('help.help')}

-

{t('help.contact')}

- Contact Support + {/* Contact CTA */} +
+
+
+
-
- - +

Still need help?

+

+ Our support team is available to assist you. +

+ + Contact Support + +
+
+ + ); +} + +function FAQRow({ + question, + answer, + badge, + isOpen, + onToggle, +}: { + question: string; + answer: string; + badge?: string; + isOpen: boolean; + onToggle: () => void; +}) { + return ( +
+ + {isOpen && ( +
+ {answer} +
+ )} +
); } diff --git a/apps/edr-passenger-web/portal/src/components/Footer.tsx b/apps/edr-passenger-web/portal/src/components/Footer.tsx index 8d1aa1394..8d4e36a3f 100644 --- a/apps/edr-passenger-web/portal/src/components/Footer.tsx +++ b/apps/edr-passenger-web/portal/src/components/Footer.tsx @@ -1,7 +1,6 @@ 'use client'; import { Mail, Phone, MapPin } from 'lucide-react'; -import Link from 'next/link'; import { useLanguage, getTranslation, Language } from '@/lib/i18n'; import { useEffect, useState } from 'react'; @@ -21,83 +20,27 @@ export function Footer() {