basic booking load behavior

This commit is contained in:
Roland Schneider
2025-12-19 16:23:53 +01:00
parent 72c213eaea
commit 4e77578abf
18 changed files with 335 additions and 75 deletions

View File

@@ -10,7 +10,6 @@ import {
ParseIntPipe,
UseGuards,
ValidationPipe,
ParseDatePipe,
} from '@nestjs/common';
import { CalendarService } from './calendar.service';
import { GetCalendarDto } from './dto/get-calendar.dto';
@@ -22,9 +21,11 @@ import { Roles } from '../auth/roles.decorator';
import { Role } from '../auth/role.enum';
import { CalendarCreateBookingDto } from './dto/create-booking.dto';
import { CancelBookingDto } from './dto/cancel-booking.dto';
import { ApiBody } from '@nestjs/swagger';
import { ApiBody, ApiCreatedResponse } from '@nestjs/swagger';
import { User } from '../auth/user.decorator';
import * as types from '../types';
import { CalendarGetBookingDto } from './dto/calendar.get.booking.dto';
import { CalenderControllerGetBookingResponse } from './dto/booking.response.dto';
@Controller('calendar')
@UseGuards(JwtAuthGuard, RolesGuard)
@@ -101,16 +102,20 @@ export class CalendarController {
return this.calendarService.cancelBooking(bookingId, cancelBookingDto);
}
@Get('bookings/:eventId/:startTime')
getBookings(
@Get('bookings/:eventId')
@ApiCreatedResponse({ type: CalenderControllerGetBookingResponse }) // Removed <Booking> generic for cleaner syntax unless defined elsewhere
// Ensure ValidationPipe is enabled with transform: true (Global or Method scoped)
// @UsePipes(new ValidationPipe({ transform: true }))
async getBookings(
@User() user: types.AppUser,
@Param('eventId', ParseIntPipe) eventId: number,
@Param('startTime', new ParseDatePipe()) startTime: Date,
// Change @Query('dto') to @Query() to flatten the params
@Query() calendarGetBookingDto: CalendarGetBookingDto,
) {
return this.calendarService.getBookings(
user.user!.userId,
eventId,
startTime,
calendarGetBookingDto,
);
}
}

View File

@@ -24,6 +24,10 @@ import { CancelBookingDto } from './dto/cancel-booking.dto';
import { CalendarCreateBookingDto } from './dto/create-booking.dto';
import { EventType } from '../entity/event-type.entity';
import { CalendarGetBookingDto } from './dto/calendar.get.booking.dto';
import {
BookingResponseDto,
CalenderControllerGetBookingResponse,
} from './dto/booking.response.dto';
// --- Type-Safe Maps ---
const frequencyMap: Record<string, RRule.Frequency> = {
@@ -602,26 +606,37 @@ export class CalendarService {
userId: number,
eventId: number,
queryParams: CalendarGetBookingDto,
): Promise<Booking[]> {
): Promise<CalenderControllerGetBookingResponse> {
console.info('getBookings', userId, eventId);
await Promise.resolve();
const { page = 1, limit = 0, sortBy, order } = queryParams;
const findOptions: FindManyOptions<Booking> = {};
const { page = 1, limit = 0 } = queryParams;
// const { page = 1, limit = 0, sortBy, order } = queryParams;
const findOptions: FindManyOptions<Booking> = {
where: {
eventId: eventId,
occurrenceStartTime: queryParams.startTime,
},
};
const paginated = limit > 0;
if (paginated) {
findOptions.skip = (page - 1) * limit;
findOptions.take = limit;
}
if (sortBy && order) {
findOptions.order = { [sortBy]: order };
}
const [data, totalItems] =
await this.bookingRepository.findAndCount(findOptions);
const dtoData = data.map((booking) => new BookingResponseDto(booking));
if (!paginated) {
return { data, total: data.length };
return {
data: dtoData,
meta: {
totalItems,
totalPages: 1,
currentPage: 1,
itemCount: data.length,
itemsPerPage: Number.MAX_SAFE_INTEGER,
},
};
}
return {
data,
data: dtoData,
meta: {
totalItems,
itemCount: data.length,
@@ -630,22 +645,6 @@ export class CalendarService {
currentPage: page,
},
};
// const booking = await this.bookingRepository.findOneBy({ id: bookingId });
// if (!booking) {
// throw new NotFoundException(`Booking with ID ${bookingId} not found.`);
// }
// if (booking.canceledAt) {
// throw new BadRequestException('This booking has already been cancelled.');
// }
//
// // Update the booking with cancellation details
// booking.canceledAt = new Date();
// booking.canceledReason = cancelBookingDto.canceledReason || null;
// booking.canceledByUserId = cancelBookingDto.canceledByUserId;
//
// return this.bookingRepository.save(booking);
return [];
}
private isValidOccurrence(event: Event, occurrenceTime: Date): boolean {

View File

@@ -0,0 +1,53 @@
import { ApiProperty } from '@nestjs/swagger';
import { Booking } from '../../entity/booking.entity';
import { UserResponseDto } from './user.response.dto';
import { PaginationResponseMetaDto } from './pagination.response.meta.dto';
export class BookingResponseDto {
@ApiProperty()
id: number;
@ApiProperty()
eventId: number;
@ApiProperty()
occurrenceStartTime: Date; // ISO String for the client
@ApiProperty({ required: false })
notes?: string;
@ApiProperty()
reservedSeatsCount: number;
@ApiProperty({ nullable: true })
canceledAt?: string;
// Flattened User Info (Avoid sending the whole User object)
@ApiProperty({
nullable: true,
type: UserResponseDto, // <--- Good practice to be explicit here too
})
user?: UserResponseDto;
constructor(booking: Booking) {
this.id = Number(booking.id); // Handle BigInt conversion
this.eventId = Number(booking.eventId);
this.occurrenceStartTime = booking.occurrenceStartTime;
this.notes = booking.notes;
this.reservedSeatsCount = booking.reservedSeatsCount;
this.canceledAt = booking.canceledAt?.toISOString() || undefined;
// Safety check: Only map user if relation is loaded
if (booking.user) {
// Assuming User entity has firstName/lastName
this.user = new UserResponseDto(booking.user);
}
}
}
export class CalenderControllerGetBookingResponse {
@ApiProperty({ type: [BookingResponseDto] })
data: BookingResponseDto[] = [];
@ApiProperty()
meta: PaginationResponseMetaDto;
}

View File

@@ -1,16 +1,62 @@
import {
IsOptional,
IsString,
IsNumber,
IsInt,
IsIn,
IsBoolean,
IsDate,
Min,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CalendarGetBookingDto {
@IsOptional() @Type(() => Number) @IsNumber() page?: number;
@IsOptional() @Type(() => Number) @IsNumber() limit?: number;
@IsOptional() @IsString() sortBy?: string;
@IsOptional() @IsIn(['ASC', 'DESC']) order?: 'ASC' | 'DESC';
@IsString() startTime: string;
@ApiPropertyOptional({
description: 'Page number for pagination',
default: 1,
minimum: 1,
})
@IsOptional()
@Type(() => Number) // Converts the query string '1' to number 1
@IsInt() // Ensures it is an integer (no decimals)
@Min(1) // Ensures it is at least 1
page?: number = 1; // Set default value in TS class
@ApiPropertyOptional({
description: 'Number of items per page',
default: 10,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 10;
@ApiPropertyOptional({
description: 'Field to sort by',
default: 'startTime',
})
@IsOptional()
@IsString()
sortBy?: string = 'startTime';
@ApiPropertyOptional({
description: 'Sort order',
enum: ['ASC', 'DESC'],
default: 'ASC',
})
@IsOptional()
@IsIn(['ASC', 'DESC'])
order?: 'ASC' | 'DESC' = 'ASC';
@ApiProperty({
description: 'The start time for the event occurrence (ISO 8601 string)',
example: '2023-10-27T10:00:00.000Z',
required: true, // Explicitly stating this is required
type: String,
})
// No @IsOptional() here implies it is REQUIRED
@Type(() => Date) // Converts ISO string from URL to JavaScript Date object
@IsDate() // Validates that the result is a valid Date instance
startTime: Date;
}

View File

@@ -0,0 +1,10 @@
import { PaginatedResponse } from '../../types';
import { ApiProperty } from '@nestjs/swagger';
import { PaginationResponseMetaDto } from './pagination.response.meta.dto';
export class PaginationResponseDto<T> implements PaginatedResponse<T> {
@ApiProperty()
data: T[];
@ApiProperty()
meta: PaginationResponseMetaDto;
}

View File

@@ -0,0 +1,15 @@
import { PaginatedResponseMeta } from '../../types';
import { ApiProperty } from '@nestjs/swagger';
export class PaginationResponseMetaDto implements PaginatedResponseMeta {
@ApiProperty()
totalItems: number;
@ApiProperty()
itemCount: number;
@ApiProperty()
itemsPerPage: number;
@ApiProperty()
totalPages: number;
@ApiProperty()
currentPage: number;
}

View File

@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '../../entity/user';
export class UserResponseDto {
@ApiProperty()
id: number;
@ApiProperty()
username: string;
constructor(user: User) {
this.id = Number(user.id); // Handle BigInt conversion
this.username = user.username;
}
}

View File

@@ -1,3 +1,6 @@
import { BookingResponseDto } from './calendar/dto/booking.response.dto';
import { PaginationResponseMetaDto } from './calendar/dto/pagination.response.meta.dto';
export interface LoginRequest {
username: string;
password: string;