From 2bfce8beb1b776776265fc6a6d8178d6a2051a44 Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Wed, 17 Jun 2026 16:49:05 +0300 Subject: [PATCH] Update booking summary page layout and update notification service --- .../modules/notifications/dtos/email.dto.ts | 55 +++- .../src/modules/notifications/dtos/sms.dto.ts | 19 ++ .../notifications/notifications.controller.ts | 11 +- .../notifications/notifications.module.ts | 52 ++-- .../notifications/notifications.service.ts | 4 +- .../notifications/sms-client.service.ts | 17 +- .../portal/src/app/booking/payment/page.tsx | 267 ++++++++++-------- .../portal/src/app/globals.css | 24 +- .../portal/tailwind.config.js | 1 + 9 files changed, 287 insertions(+), 163 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts b/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts index 63bbc4a5a..a435fa4ce 100644 --- a/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts +++ b/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts @@ -1,8 +1,57 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class SendEmail { + @ApiProperty() + @IsEmail() + @IsNotEmpty() to: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + sourceId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + sourceName?: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() subject: string; - body: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() html?: string; - templateKey?: string; - context?: Record; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + text?: string; + + @ApiPropertyOptional() + @IsOptional() + body?: string; + + @ApiPropertyOptional() + @IsOptional() + context?: Record; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + templateName?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsEmail() + from?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsEmail() + replyTo?: string; } diff --git a/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts b/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts index 1897f7794..2841597fc 100644 --- a/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts +++ b/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts @@ -1,9 +1,28 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + export class SendMessage { + @ApiProperty() + @IsNotEmpty() + @IsString() to: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() message: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() from?: string; } export class BulkMessagesDto { + @ApiProperty({ type: [SendMessage] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SendMessage) messages: SendMessage[]; } diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts index 9fd69830c..0b7798a04 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts @@ -7,7 +7,7 @@ import { TestNotificationDto } from './notifications.dto'; import { EmailClientService } from './email-client.service'; import { SmsClientService } from './sms-client.service'; import { SendEmail } from './dtos/email.dto'; -import { SendMessage } from './dtos/sms.dto'; +import { BulkMessagesDto, SendMessage } from './dtos/sms.dto'; @ApiTags('Notifications') @Controller('notifications') @@ -56,6 +56,15 @@ export class NotificationsController { return this.smsClient.sendSms(dto); } + @Post('send/sms/bulk') + @UseGuards(IamGuard) + @IamRoles('ADMIN', 'STAFF') + @ApiOperation({ summary: 'Send bulk SMS messages via the SMS microservice' }) + @ApiBody({ type: BulkMessagesDto }) + sendBulkSms(@Body() dto: BulkMessagesDto) { + return this.smsClient.sendBulkMessages(dto); + } + @Post('test') @UseGuards(IamGuard) @IamRoles('ADMIN', 'STAFF') diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts index f209ccf17..f3436ee38 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { NotificationsController } from './notifications.controller'; import { NotificationsService } from './notifications.service'; @@ -11,34 +10,33 @@ import { SmsClientService } from './sms-client.service'; @Module({ imports: [ HttpModule.register({ timeout: 10_000 }), - ClientsModule.registerAsync([ - { - name: 'EMAIL_SERVICE', - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - transport: Transport.RMQ, - options: { - urls: [config.get('RABBITMQ_URL') ?? 'amqp://localhost:5672'], - queue: config.get('EMAIL_QUEUE') ?? 'email_queue', - queueOptions: { durable: true }, - noAck: true, - }, - }), - }, + ClientsModule.register([ { name: 'SMS_SERVICE', - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - transport: Transport.RMQ, - options: { - urls: [config.get('RABBITMQ_URL') ?? 'amqp://localhost:5672'], - queue: config.get('SMS_QUEUE') ?? 'sms_queue', - queueOptions: { durable: true }, - noAck: true, - }, - }), + transport: Transport.RMQ, + options: { + urls: [process.env.RABBITMQ_URL as string], + queue: 'sms_queue', + queueOptions: { durable: true }, + }, + }, + { + name: 'EMAIL_SERVICE', + transport: Transport.RMQ, + options: { + urls: [process.env.RABBITMQ_URL as string], + queue: 'email_queue', + queueOptions: { durable: true }, + }, + }, + { + name: 'NOTIFICATION_SERVICE', + transport: Transport.RMQ, + options: { + urls: [process.env.RABBITMQ_URL as string], + queue: 'notification_queue', + queueOptions: { durable: true }, + }, }, ]), ], diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts index 68d76572c..595633e93 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts @@ -20,7 +20,7 @@ export class NotificationsService { private pushAdapter: PushAdapter, ) { this.channels = new Map([ - ['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, body }).then(() => true) }], + ['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, text: body }).then(() => true) }], ['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }], ['PUSH', this.pushAdapter as NotificationChannel], ]); @@ -107,7 +107,7 @@ export class NotificationsService { await this.emailClient.sendEmail({ to: passenger.user.email, subject: this.sanitize(dto.title), - body: this.sanitize(dto.body), + text: this.sanitize(dto.body), }); } diff --git a/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts index 0108e0758..148a8a42b 100644 --- a/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts @@ -1,4 +1,9 @@ -import { Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { + Inject, + Injectable, + Logger, + OnApplicationBootstrap, +} from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { BulkMessagesDto, SendMessage } from './dtos/sms.dto'; @@ -8,14 +13,18 @@ export class SmsClientService implements OnApplicationBootstrap { constructor( @Inject('SMS_SERVICE') - private readonly smsClient: ClientProxy, + private smsClient: ClientProxy, ) {} async onApplicationBootstrap() { this.smsClient .connect() - .then(() => this.logger.log('Connected to SMS service')) - .catch((err) => this.logger.error('Error connecting to SMS service', err)); + .then(() => { + this.logger.log('connected to SMS service'); + }) + .catch((err) => { + console.error('Error happened at SMS service', err); + }); } async sendSms(dto: SendMessage) { diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx index 389123510..def68279c 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx @@ -238,52 +238,61 @@ export default function PaymentPage() { {/* Flight-style timeline */} -
-
+
+ {/* Left column: Timeline with dots and line */} +
+ {/* Origin dot */} +
+ {/* Vertical line */} +
+ {/* Destination dot */} +
+
- {/* Origin */} -
-
-
- {outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'} -
-
- {outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'} -
-
- {outboundSchedule?.origin} -
-
- - {/* Journey Info */} -
-
-
- - - - {outboundSchedule?.duration} + {/* Right column: Content */} +
+ {/* Origin */} +
+
+ {outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'}
-
- - - - Train {outboundSchedule?.trainNumber} +
+ {outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'} +
+
+ {outboundSchedule?.origin}
-
- {/* Destination */} -
-
-
- {outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'} + {/* Journey Info */} +
+
+
+ + + + {outboundSchedule?.duration} +
+
+ + + + Train {outboundSchedule?.trainNumber} +
+
-
- {outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} -
-
- {outboundSchedule?.destination} + + {/* Destination */} +
+
+ {outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'} +
+
+ {outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} +
+
+ {outboundSchedule?.destination} +
@@ -307,52 +316,61 @@ export default function PaymentPage() {
{/* Flight-style timeline */} -
-
+
+ {/* Left column: Timeline with dots and line */} +
+ {/* Origin dot */} +
+ {/* Vertical line */} +
+ {/* Destination dot */} +
+
- {/* Origin */} -
-
-
- {inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'} -
-
- {inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'} -
-
- {inboundSchedule?.origin} -
-
- - {/* Journey Info */} -
-
-
- - - - {inboundSchedule?.duration} + {/* Right column: Content */} +
+ {/* Origin */} +
+
+ {inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'}
-
- - - - Train {inboundSchedule?.trainNumber} +
+ {inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'} +
+
+ {inboundSchedule?.origin}
-
- {/* Destination */} -
-
-
- {inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'} + {/* Journey Info */} +
+
+
+ + + + {inboundSchedule?.duration} +
+
+ + + + Train {inboundSchedule?.trainNumber} +
+
-
- {inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} -
-
- {inboundSchedule?.destination} + + {/* Destination */} +
+
+ {inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'} +
+
+ {inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} +
+
+ {inboundSchedule?.destination} +
@@ -378,52 +396,61 @@ export default function PaymentPage() {
{/* Flight-style timeline */} -
-
+
+ {/* Left column: Timeline with dots and line */} +
+ {/* Origin dot */} +
+ {/* Vertical line */} +
+ {/* Destination dot */} +
+
- {/* Origin */} -
-
-
- {selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'} -
-
- {selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'} -
-
- {selectedSchedule?.origin} -
-
- - {/* Journey Info */} -
-
-
- - - - {selectedSchedule?.duration} + {/* Right column: Content */} +
+ {/* Origin */} +
+
+ {selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'}
-
- - - - Train {selectedSchedule?.trainNumber} +
+ {selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'} +
+
+ {selectedSchedule?.origin}
-
- {/* Destination */} -
-
-
- {selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'} + {/* Journey Info */} +
+
+
+ + + + {selectedSchedule?.duration} +
+
+ + + + Train {selectedSchedule?.trainNumber} +
+
-
- {selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} -
-
- {selectedSchedule?.destination} + + {/* Destination */} +
+
+ {selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'} +
+
+ {selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} +
+
+ {selectedSchedule?.destination} +
diff --git a/apps/edr-passenger-web/portal/src/app/globals.css b/apps/edr-passenger-web/portal/src/app/globals.css index 1da5e5f88..9714d54bc 100644 --- a/apps/edr-passenger-web/portal/src/app/globals.css +++ b/apps/edr-passenger-web/portal/src/app/globals.css @@ -30,7 +30,13 @@ } .btn-ghost:hover { - @apply bg-[rgb(20_113_76)] bg-opacity-10 dark:bg-[rgb(20_113_76)] dark:bg-opacity-20; + background-color: rgba(20, 113, 76, 0.1); + } + + @media (prefers-color-scheme: dark) { + .btn-ghost:hover { + background-color: rgba(20, 113, 76, 0.2); + } } .input-field { @@ -146,6 +152,17 @@ } } + @keyframes slide-up { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + .animate-bounce-in { animation: bounce-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } @@ -167,11 +184,6 @@ animation: slide-in-right 0.5s ease-out; } - @keyframes slide-up { - from { transform: translateY(100%); opacity: 0; } - to { transform: translateY(0); opacity: 1; } - } - .animate-slide-up { animation: slide-up 0.25s cubic-bezier(0.32, 0.72, 0, 1); } diff --git a/apps/edr-passenger-web/portal/tailwind.config.js b/apps/edr-passenger-web/portal/tailwind.config.js index 57df24cf5..f9ae9f63d 100644 --- a/apps/edr-passenger-web/portal/tailwind.config.js +++ b/apps/edr-passenger-web/portal/tailwind.config.js @@ -85,3 +85,4 @@ export default { }, }, plugins: [], +};