678 lines
21 KiB
TypeScript
678 lines
21 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Param,
|
|
Body,
|
|
Query,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Logger,
|
|
UsePipes,
|
|
ValidationPipe,
|
|
NotFoundException,
|
|
ParseUUIDPipe,
|
|
ParseIntPipe,
|
|
DefaultValuePipe,
|
|
UseGuards,
|
|
Res,
|
|
StreamableFile,
|
|
Inject,
|
|
} from '@nestjs/common';
|
|
import {
|
|
ApiTags,
|
|
ApiOperation,
|
|
ApiResponse,
|
|
ApiBadRequestResponse,
|
|
ApiNotFoundResponse,
|
|
ApiInternalServerErrorResponse,
|
|
ApiQuery,
|
|
ApiParam,
|
|
ApiBearerAuth,
|
|
ApiProduces,
|
|
} from '@nestjs/swagger';
|
|
import { Response } from 'express';
|
|
import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto } from '../dto';
|
|
import { BookingFilterDto } from '../dto/booking-filter.dto';
|
|
import { BookingExportDto, ExportFormat } from '../dto/booking-export.dto';
|
|
import { BookingMapper } from '../mappers';
|
|
import { BookingService } from '@domain/services/booking.service';
|
|
import { BookingRepository, BOOKING_REPOSITORY } from '@domain/ports/out/booking.repository';
|
|
import {
|
|
RateQuoteRepository,
|
|
RATE_QUOTE_REPOSITORY,
|
|
} from '@domain/ports/out/rate-quote.repository';
|
|
import { BookingNumber } from '@domain/value-objects/booking-number.vo';
|
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
|
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
|
import { ExportService } from '../services/export.service';
|
|
import { FuzzySearchService } from '../services/fuzzy-search.service';
|
|
import { AuditService } from '../services/audit.service';
|
|
import { AuditAction, AuditStatus } from '@domain/entities/audit-log.entity';
|
|
import { NotificationService } from '../services/notification.service';
|
|
import { NotificationsGateway } from '../gateways/notifications.gateway';
|
|
import { WebhookService } from '../services/webhook.service';
|
|
import { WebhookEvent } from '@domain/entities/webhook.entity';
|
|
|
|
@ApiTags('Bookings')
|
|
@Controller('bookings')
|
|
@UseGuards(JwtAuthGuard)
|
|
@ApiBearerAuth()
|
|
export class BookingsController {
|
|
private readonly logger = new Logger(BookingsController.name);
|
|
|
|
constructor(
|
|
private readonly bookingService: BookingService,
|
|
@Inject(BOOKING_REPOSITORY) private readonly bookingRepository: BookingRepository,
|
|
@Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository,
|
|
private readonly exportService: ExportService,
|
|
private readonly fuzzySearchService: FuzzySearchService,
|
|
private readonly auditService: AuditService,
|
|
private readonly notificationService: NotificationService,
|
|
private readonly notificationsGateway: NotificationsGateway,
|
|
private readonly webhookService: WebhookService
|
|
) {}
|
|
|
|
@Post()
|
|
@HttpCode(HttpStatus.CREATED)
|
|
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
|
@ApiOperation({
|
|
summary: 'Create a new booking',
|
|
description:
|
|
'Create a new booking based on a rate quote. The booking will be in "draft" status initially. Requires authentication.',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.CREATED,
|
|
description: 'Booking created successfully',
|
|
type: BookingResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiBadRequestResponse({
|
|
description: 'Invalid request parameters',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'Rate quote not found',
|
|
})
|
|
@ApiInternalServerErrorResponse({
|
|
description: 'Internal server error',
|
|
})
|
|
async createBooking(
|
|
@Body() dto: CreateBookingRequestDto,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<BookingResponseDto> {
|
|
this.logger.log(`[User: ${user.email}] Creating booking for rate quote: ${dto.rateQuoteId}`);
|
|
|
|
try {
|
|
// Convert DTO to domain input, using authenticated user's data
|
|
const input = {
|
|
...BookingMapper.toCreateBookingInput(dto),
|
|
userId: user.id,
|
|
organizationId: user.organizationId,
|
|
};
|
|
|
|
// Create booking via domain service
|
|
const booking = await this.bookingService.createBooking(input);
|
|
|
|
// Fetch rate quote for response
|
|
const rateQuote = await this.rateQuoteRepository.findById(dto.rateQuoteId);
|
|
if (!rateQuote) {
|
|
throw new NotFoundException(`Rate quote ${dto.rateQuoteId} not found`);
|
|
}
|
|
|
|
// Convert to DTO
|
|
const response = BookingMapper.toDto(booking, rateQuote);
|
|
|
|
this.logger.log(
|
|
`Booking created successfully: ${booking.bookingNumber.value} (${booking.id})`
|
|
);
|
|
|
|
// Audit log: Booking created
|
|
await this.auditService.logSuccess(
|
|
AuditAction.BOOKING_CREATED,
|
|
user.id,
|
|
user.email,
|
|
user.organizationId,
|
|
{
|
|
resourceType: 'booking',
|
|
resourceId: booking.id,
|
|
resourceName: booking.bookingNumber.value,
|
|
metadata: {
|
|
rateQuoteId: dto.rateQuoteId,
|
|
status: booking.status.value,
|
|
carrier: rateQuote.carrierName,
|
|
},
|
|
}
|
|
);
|
|
|
|
// Send real-time notification
|
|
try {
|
|
const notification = await this.notificationService.notifyBookingCreated(
|
|
user.id,
|
|
user.organizationId,
|
|
booking.bookingNumber.value,
|
|
booking.id
|
|
);
|
|
await this.notificationsGateway.sendNotificationToUser(user.id, notification);
|
|
} catch (error: any) {
|
|
// Don't fail the booking creation if notification fails
|
|
this.logger.error(`Failed to send notification: ${error?.message}`);
|
|
}
|
|
|
|
// Trigger webhooks
|
|
try {
|
|
await this.webhookService.triggerWebhooks(
|
|
WebhookEvent.BOOKING_CREATED,
|
|
user.organizationId,
|
|
{
|
|
bookingId: booking.id,
|
|
bookingNumber: booking.bookingNumber.value,
|
|
status: booking.status.value,
|
|
shipper: booking.shipper,
|
|
consignee: booking.consignee,
|
|
carrier: rateQuote.carrierName,
|
|
origin: rateQuote.origin,
|
|
destination: rateQuote.destination,
|
|
etd: rateQuote.etd?.toISOString(),
|
|
eta: rateQuote.eta?.toISOString(),
|
|
createdAt: booking.createdAt.toISOString(),
|
|
}
|
|
);
|
|
} catch (error: any) {
|
|
// Don't fail the booking creation if webhook fails
|
|
this.logger.error(`Failed to trigger webhooks: ${error?.message}`);
|
|
}
|
|
|
|
return response;
|
|
} catch (error: any) {
|
|
this.logger.error(
|
|
`Booking creation failed: ${error?.message || 'Unknown error'}`,
|
|
error?.stack
|
|
);
|
|
|
|
// Audit log: Booking creation failed
|
|
await this.auditService.logFailure(
|
|
AuditAction.BOOKING_CREATED,
|
|
user.id,
|
|
user.email,
|
|
user.organizationId,
|
|
error?.message || 'Unknown error',
|
|
{
|
|
resourceType: 'booking',
|
|
metadata: {
|
|
rateQuoteId: dto.rateQuoteId,
|
|
},
|
|
}
|
|
);
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
@Get(':id')
|
|
@ApiOperation({
|
|
summary: 'Get booking by ID',
|
|
description: 'Retrieve detailed information about a specific booking. Requires authentication.',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Booking ID (UUID)',
|
|
example: '550e8400-e29b-41d4-a716-446655440000',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Booking details retrieved successfully',
|
|
type: BookingResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'Booking not found',
|
|
})
|
|
async getBooking(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<BookingResponseDto> {
|
|
this.logger.log(`[User: ${user.email}] Fetching booking: ${id}`);
|
|
|
|
const booking = await this.bookingRepository.findById(id);
|
|
if (!booking) {
|
|
throw new NotFoundException(`Booking ${id} not found`);
|
|
}
|
|
|
|
// Verify booking belongs to user's organization
|
|
if (booking.organizationId !== user.organizationId) {
|
|
throw new NotFoundException(`Booking ${id} not found`);
|
|
}
|
|
|
|
// Fetch rate quote
|
|
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
|
|
if (!rateQuote) {
|
|
throw new NotFoundException(`Rate quote ${booking.rateQuoteId} not found`);
|
|
}
|
|
|
|
return BookingMapper.toDto(booking, rateQuote);
|
|
}
|
|
|
|
@Get('number/:bookingNumber')
|
|
@ApiOperation({
|
|
summary: 'Get booking by booking number',
|
|
description:
|
|
'Retrieve detailed information about a specific booking using its booking number. Requires authentication.',
|
|
})
|
|
@ApiParam({
|
|
name: 'bookingNumber',
|
|
description: 'Booking number',
|
|
example: 'WCM-2025-ABC123',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Booking details retrieved successfully',
|
|
type: BookingResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'Booking not found',
|
|
})
|
|
async getBookingByNumber(
|
|
@Param('bookingNumber') bookingNumber: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<BookingResponseDto> {
|
|
this.logger.log(`[User: ${user.email}] Fetching booking by number: ${bookingNumber}`);
|
|
|
|
const bookingNumberVo = BookingNumber.fromString(bookingNumber);
|
|
const booking = await this.bookingRepository.findByBookingNumber(bookingNumberVo);
|
|
|
|
if (!booking) {
|
|
throw new NotFoundException(`Booking ${bookingNumber} not found`);
|
|
}
|
|
|
|
// Verify booking belongs to user's organization
|
|
if (booking.organizationId !== user.organizationId) {
|
|
throw new NotFoundException(`Booking ${bookingNumber} not found`);
|
|
}
|
|
|
|
// Fetch rate quote
|
|
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
|
|
if (!rateQuote) {
|
|
throw new NotFoundException(`Rate quote ${booking.rateQuoteId} not found`);
|
|
}
|
|
|
|
return BookingMapper.toDto(booking, rateQuote);
|
|
}
|
|
|
|
@Get()
|
|
@ApiOperation({
|
|
summary: 'List bookings',
|
|
description:
|
|
"Retrieve a paginated list of bookings for the authenticated user's organization. Requires authentication.",
|
|
})
|
|
@ApiQuery({
|
|
name: 'page',
|
|
required: false,
|
|
description: 'Page number (1-based)',
|
|
example: 1,
|
|
})
|
|
@ApiQuery({
|
|
name: 'pageSize',
|
|
required: false,
|
|
description: 'Number of items per page',
|
|
example: 20,
|
|
})
|
|
@ApiQuery({
|
|
name: 'status',
|
|
required: false,
|
|
description: 'Filter by booking status',
|
|
enum: ['draft', 'pending_confirmation', 'confirmed', 'in_transit', 'delivered', 'cancelled'],
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Bookings list retrieved successfully',
|
|
type: BookingListResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
async listBookings(
|
|
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
|
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
|
|
@Query('status') status: string | undefined,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<BookingListResponseDto> {
|
|
this.logger.log(
|
|
`[User: ${user.email}] Listing bookings: page=${page}, pageSize=${pageSize}, status=${status}`
|
|
);
|
|
|
|
// ADMIN: Fetch ALL bookings from database
|
|
// Others: Fetch only bookings from their organization
|
|
let bookings: any[];
|
|
if (user.role === 'admin') {
|
|
this.logger.log(`[ADMIN] Fetching ALL bookings from database`);
|
|
bookings = await this.bookingRepository.findAll();
|
|
} else {
|
|
this.logger.log(`[User] Fetching bookings from organization: ${user.organizationId}`);
|
|
bookings = await this.bookingRepository.findByOrganization(user.organizationId);
|
|
}
|
|
|
|
// Filter by status if provided
|
|
const filteredBookings = status
|
|
? bookings.filter((b: any) => b.status.value === status)
|
|
: bookings;
|
|
|
|
// Paginate
|
|
const startIndex = (page - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const paginatedBookings = filteredBookings.slice(startIndex, endIndex);
|
|
|
|
// Fetch rate quotes for all bookings
|
|
const bookingsWithQuotes = await Promise.all(
|
|
paginatedBookings.map(async (booking: any) => {
|
|
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
|
|
return { booking, rateQuote: rateQuote! };
|
|
})
|
|
);
|
|
|
|
// Convert to DTOs
|
|
const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes);
|
|
|
|
const totalPages = Math.ceil(filteredBookings.length / pageSize);
|
|
|
|
return {
|
|
bookings: bookingDtos,
|
|
total: filteredBookings.length,
|
|
page,
|
|
pageSize,
|
|
totalPages,
|
|
};
|
|
}
|
|
|
|
@Get('search/fuzzy')
|
|
@ApiOperation({
|
|
summary: 'Fuzzy search bookings',
|
|
description:
|
|
'Search bookings using fuzzy matching. Tolerant to typos and partial matches. Searches across booking number, shipper, and consignee names.',
|
|
})
|
|
@ApiQuery({
|
|
name: 'q',
|
|
required: true,
|
|
description: 'Search query (minimum 2 characters)',
|
|
example: 'WCM-2025',
|
|
})
|
|
@ApiQuery({
|
|
name: 'limit',
|
|
required: false,
|
|
description: 'Maximum number of results',
|
|
example: 20,
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Search results retrieved successfully',
|
|
type: [BookingResponseDto],
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
async fuzzySearch(
|
|
@Query('q') searchTerm: string,
|
|
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<BookingResponseDto[]> {
|
|
this.logger.log(`[User: ${user.email}] Fuzzy search: "${searchTerm}"`);
|
|
|
|
if (!searchTerm || searchTerm.length < 2) {
|
|
return [];
|
|
}
|
|
|
|
// Perform fuzzy search
|
|
const bookingOrms = await this.fuzzySearchService.search(
|
|
searchTerm,
|
|
user.organizationId,
|
|
limit
|
|
);
|
|
|
|
// Map ORM entities to domain and fetch rate quotes
|
|
const bookingsWithQuotes = await Promise.all(
|
|
bookingOrms.map(async bookingOrm => {
|
|
const booking = await this.bookingRepository.findById(bookingOrm.id);
|
|
const rateQuote = await this.rateQuoteRepository.findById(bookingOrm.rateQuoteId);
|
|
return { booking: booking!, rateQuote: rateQuote! };
|
|
})
|
|
);
|
|
|
|
// Convert to DTOs
|
|
const bookingDtos = bookingsWithQuotes.map(({ booking, rateQuote }) =>
|
|
BookingMapper.toDto(booking, rateQuote)
|
|
);
|
|
|
|
this.logger.log(`Fuzzy search returned ${bookingDtos.length} results`);
|
|
|
|
return bookingDtos;
|
|
}
|
|
|
|
@Get('advanced/search')
|
|
@ApiOperation({
|
|
summary: 'Advanced booking search with filtering',
|
|
description:
|
|
'Search bookings with advanced filtering options including status, date ranges, carrier, ports, shipper/consignee. Supports sorting and pagination.',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Filtered bookings retrieved successfully',
|
|
type: BookingListResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
async advancedSearch(
|
|
@Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<BookingListResponseDto> {
|
|
this.logger.log(
|
|
`[User: ${user.email}] Advanced search with filters: ${JSON.stringify(filter)}`
|
|
);
|
|
|
|
// Fetch all bookings for organization
|
|
let bookings = await this.bookingRepository.findByOrganization(user.organizationId);
|
|
|
|
// Apply filters
|
|
bookings = this.applyFilters(bookings, filter);
|
|
|
|
// Sort bookings
|
|
bookings = this.sortBookings(bookings, filter.sortBy!, filter.sortOrder!);
|
|
|
|
// Total count before pagination
|
|
const total = bookings.length;
|
|
|
|
// Paginate
|
|
const startIndex = ((filter.page || 1) - 1) * (filter.pageSize || 20);
|
|
const endIndex = startIndex + (filter.pageSize || 20);
|
|
const paginatedBookings = bookings.slice(startIndex, endIndex);
|
|
|
|
// Fetch rate quotes
|
|
const bookingsWithQuotes = await Promise.all(
|
|
paginatedBookings.map(async booking => {
|
|
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
|
|
return { booking, rateQuote: rateQuote! };
|
|
})
|
|
);
|
|
|
|
// Convert to DTOs
|
|
const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes);
|
|
|
|
const totalPages = Math.ceil(total / (filter.pageSize || 20));
|
|
|
|
return {
|
|
bookings: bookingDtos,
|
|
total,
|
|
page: filter.page || 1,
|
|
pageSize: filter.pageSize || 20,
|
|
totalPages,
|
|
};
|
|
}
|
|
|
|
@Post('export')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({
|
|
summary: 'Export bookings to CSV/Excel/JSON',
|
|
description:
|
|
'Export bookings with optional filtering. Supports CSV, Excel (xlsx), and JSON formats.',
|
|
})
|
|
@ApiProduces(
|
|
'text/csv',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'application/json'
|
|
)
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Export file generated successfully',
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
async exportBookings(
|
|
@Body(new ValidationPipe({ transform: true })) exportDto: BookingExportDto,
|
|
@Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto,
|
|
@CurrentUser() user: UserPayload,
|
|
@Res({ passthrough: true }) res: Response
|
|
): Promise<StreamableFile> {
|
|
this.logger.log(`[User: ${user.email}] Exporting bookings to ${exportDto.format}`);
|
|
|
|
let bookings: any[];
|
|
|
|
// If specific booking IDs provided, use those
|
|
if (exportDto.bookingIds && exportDto.bookingIds.length > 0) {
|
|
bookings = await Promise.all(
|
|
exportDto.bookingIds.map(id => this.bookingRepository.findById(id))
|
|
);
|
|
bookings = bookings.filter(b => b !== null && b.organizationId === user.organizationId);
|
|
} else {
|
|
// Otherwise, use filter criteria
|
|
bookings = await this.bookingRepository.findByOrganization(user.organizationId);
|
|
bookings = this.applyFilters(bookings, filter);
|
|
}
|
|
|
|
// Fetch rate quotes
|
|
const bookingsWithQuotes = await Promise.all(
|
|
bookings.map(async booking => {
|
|
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
|
|
return { booking, rateQuote: rateQuote! };
|
|
})
|
|
);
|
|
|
|
// Generate export file
|
|
const exportResult = await this.exportService.exportBookings(
|
|
bookingsWithQuotes,
|
|
exportDto.format,
|
|
exportDto.fields
|
|
);
|
|
|
|
// Set response headers
|
|
res.set({
|
|
'Content-Type': exportResult.contentType,
|
|
'Content-Disposition': `attachment; filename="${exportResult.filename}"`,
|
|
});
|
|
|
|
// Audit log: Data exported
|
|
await this.auditService.logSuccess(
|
|
AuditAction.DATA_EXPORTED,
|
|
user.id,
|
|
user.email,
|
|
user.organizationId,
|
|
{
|
|
resourceType: 'booking',
|
|
metadata: {
|
|
format: exportDto.format,
|
|
bookingCount: bookings.length,
|
|
fields: exportDto.fields?.join(', ') || 'all',
|
|
filename: exportResult.filename,
|
|
},
|
|
}
|
|
);
|
|
|
|
return new StreamableFile(exportResult.buffer);
|
|
}
|
|
|
|
/**
|
|
* Apply filters to bookings array
|
|
*/
|
|
private applyFilters(bookings: any[], filter: BookingFilterDto): any[] {
|
|
let filtered = bookings;
|
|
|
|
// Filter by status
|
|
if (filter.status && filter.status.length > 0) {
|
|
filtered = filtered.filter(b => filter.status!.includes(b.status.value));
|
|
}
|
|
|
|
// Filter by search (booking number partial match)
|
|
if (filter.search) {
|
|
const searchLower = filter.search.toLowerCase();
|
|
filtered = filtered.filter(b => b.bookingNumber.value.toLowerCase().includes(searchLower));
|
|
}
|
|
|
|
// Filter by shipper
|
|
if (filter.shipper) {
|
|
const shipperLower = filter.shipper.toLowerCase();
|
|
filtered = filtered.filter(b => b.shipper.name.toLowerCase().includes(shipperLower));
|
|
}
|
|
|
|
// Filter by consignee
|
|
if (filter.consignee) {
|
|
const consigneeLower = filter.consignee.toLowerCase();
|
|
filtered = filtered.filter(b => b.consignee.name.toLowerCase().includes(consigneeLower));
|
|
}
|
|
|
|
// Filter by creation date range
|
|
if (filter.createdFrom) {
|
|
const fromDate = new Date(filter.createdFrom);
|
|
filtered = filtered.filter(b => b.createdAt >= fromDate);
|
|
}
|
|
if (filter.createdTo) {
|
|
const toDate = new Date(filter.createdTo);
|
|
filtered = filtered.filter(b => b.createdAt <= toDate);
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Sort bookings array
|
|
*/
|
|
private sortBookings(bookings: any[], sortBy: string, sortOrder: string): any[] {
|
|
return [...bookings].sort((a, b) => {
|
|
let aValue: any;
|
|
let bValue: any;
|
|
|
|
switch (sortBy) {
|
|
case 'bookingNumber':
|
|
aValue = a.bookingNumber.value;
|
|
bValue = b.bookingNumber.value;
|
|
break;
|
|
case 'status':
|
|
aValue = a.status.value;
|
|
bValue = b.status.value;
|
|
break;
|
|
case 'createdAt':
|
|
default:
|
|
aValue = a.createdAt;
|
|
bValue = b.createdAt;
|
|
break;
|
|
}
|
|
|
|
if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
|
|
if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}
|
|
}
|