mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-08-25 16:19:38 +00:00
219 lines
6.9 KiB
TypeScript
219 lines
6.9 KiB
TypeScript
import { DaysEnum } from '@app/common/constants/days.enum';
|
|
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
|
|
import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository';
|
|
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
|
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
|
|
import { UserRepository } from '@app/common/modules/user/repositories/user.repository';
|
|
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
ForbiddenException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { format } from 'date-fns';
|
|
import { Between } from 'typeorm/find-options/operator/Between';
|
|
import { BookingRequestDto } from '../dtos/booking-request.dto';
|
|
import { CreateBookingDto } from '../dtos/create-booking.dto';
|
|
import { MyBookingRequestDto } from '../dtos/my-booking-request.dto';
|
|
|
|
@Injectable()
|
|
export class BookingService {
|
|
constructor(
|
|
private readonly bookingEntityRepository: BookingEntityRepository,
|
|
private readonly spaceRepository: SpaceRepository,
|
|
private readonly userRepository: UserRepository,
|
|
) {}
|
|
|
|
async create(userUuid: string, dto: CreateBookingDto) {
|
|
console.log(userUuid);
|
|
const user = await this.userRepository.findOne({
|
|
where: { uuid: userUuid },
|
|
relations: ['userSpaces', 'userSpaces.space'],
|
|
});
|
|
console.log(user.userSpaces);
|
|
if (!user.userSpaces.some(({ space }) => space.uuid === dto.spaceUuid)) {
|
|
throw new ForbiddenException(
|
|
`User does not have permission to book this space: ${dto.spaceUuid}`,
|
|
);
|
|
}
|
|
// Validate time slots first
|
|
this.validateTimeSlot(dto.startTime, dto.endTime);
|
|
|
|
// fetch spaces exist
|
|
const space = await this.getSpaceConfigurationAndBookings(dto.spaceUuid);
|
|
|
|
// Validate booking availability
|
|
this.validateBookingAvailability(space, dto);
|
|
|
|
// Create and save booking
|
|
return this.createBookings(space, userUuid, dto);
|
|
}
|
|
|
|
async findAll({ month, space }: BookingRequestDto, project: string) {
|
|
const [monthNumber, year] = month.split('-').map(Number);
|
|
const fromDate = new Date(year, monthNumber - 1, 1);
|
|
const toDate = new Date(year, monthNumber, 0, 23, 59, 59);
|
|
return this.bookingEntityRepository.find({
|
|
where: {
|
|
space: {
|
|
community: { project: { uuid: project } },
|
|
uuid: space ? space : undefined,
|
|
},
|
|
date: Between(fromDate, toDate),
|
|
},
|
|
relations: ['space', 'user', 'user.inviteUser'],
|
|
order: { date: 'DESC' },
|
|
});
|
|
}
|
|
|
|
async findMyBookings(
|
|
{ when }: MyBookingRequestDto,
|
|
userUuid: string,
|
|
project: string,
|
|
) {
|
|
const now = new Date();
|
|
const nowTime = format(now, 'HH:mm');
|
|
|
|
const query = this.bookingEntityRepository
|
|
.createQueryBuilder('booking')
|
|
.leftJoinAndSelect('booking.space', 'space')
|
|
.innerJoin(
|
|
'space.community',
|
|
'community',
|
|
'community.project = :project',
|
|
{ project },
|
|
)
|
|
.leftJoinAndSelect('booking.user', 'user')
|
|
.where('user.uuid = :userUuid', { userUuid });
|
|
|
|
if (when === 'past') {
|
|
query.andWhere(
|
|
`(DATE(booking.date) < :today OR (DATE(booking.date) = :today AND booking.startTime < :nowTime))`,
|
|
{ today: now, nowTime },
|
|
);
|
|
} else if (when === 'future') {
|
|
query.andWhere(
|
|
`(DATE(booking.date) > :today OR (DATE(booking.date) = :today AND booking.startTime >= :nowTime))`,
|
|
{ today: now, nowTime },
|
|
);
|
|
}
|
|
|
|
query.orderBy({
|
|
'DATE(booking.date)': 'DESC',
|
|
'booking.startTime': 'DESC',
|
|
});
|
|
|
|
return query.getMany();
|
|
}
|
|
|
|
/**
|
|
* Fetch space by UUID and throw an error if not found or if not configured for booking
|
|
*/
|
|
private async getSpaceConfigurationAndBookings(
|
|
spaceUuid: string,
|
|
): Promise<SpaceEntity> {
|
|
const space = await this.spaceRepository.findOne({
|
|
where: { uuid: spaceUuid },
|
|
relations: ['bookableConfig', 'bookings'],
|
|
});
|
|
|
|
if (!space) {
|
|
throw new NotFoundException(`Space not found: ${spaceUuid}`);
|
|
}
|
|
if (!space.bookableConfig) {
|
|
throw new NotFoundException(
|
|
`This space is not configured for booking: ${spaceUuid}`,
|
|
);
|
|
}
|
|
|
|
return space;
|
|
}
|
|
|
|
/**
|
|
* Ensure the slot start time is before the end time
|
|
*/
|
|
private validateTimeSlot(startTime: string, endTime: string): void {
|
|
const start = timeToMinutes(startTime);
|
|
const end = timeToMinutes(endTime);
|
|
|
|
if (start >= end) {
|
|
throw new BadRequestException(
|
|
`End time must be after start time for slot: ${startTime}-${endTime}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* check if the space is available for booking on the requested day
|
|
* and if the requested time slot is within the available hours
|
|
*/
|
|
private validateBookingAvailability(
|
|
space: SpaceEntity,
|
|
dto: CreateBookingDto,
|
|
): void {
|
|
// Check if the space is available for booking on the requested day
|
|
const availableDays = space.bookableConfig?.daysAvailable || [];
|
|
const requestedDay = new Date(dto.date).toLocaleDateString('en-US', {
|
|
weekday: 'short',
|
|
}) as DaysEnum;
|
|
|
|
if (!availableDays.includes(requestedDay)) {
|
|
const dayFullName = new Date(dto.date).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
});
|
|
throw new BadRequestException(
|
|
`Space is not available for booking on ${dayFullName}s`,
|
|
);
|
|
}
|
|
|
|
const dtoStartTimeInMinutes = timeToMinutes(dto.startTime);
|
|
const dtoEndTimeInMinutes = timeToMinutes(dto.endTime);
|
|
|
|
if (
|
|
dtoStartTimeInMinutes < timeToMinutes(space.bookableConfig.startTime) ||
|
|
dtoEndTimeInMinutes > timeToMinutes(space.bookableConfig.endTime)
|
|
) {
|
|
throw new BadRequestException(
|
|
`Booking time must be within the available hours for space: ${space.spaceName}`,
|
|
);
|
|
}
|
|
|
|
const previousBookings = space.bookings.filter(
|
|
(booking) =>
|
|
timeToMinutes(booking.startTime) < dtoEndTimeInMinutes &&
|
|
timeToMinutes(booking.endTime) > dtoStartTimeInMinutes &&
|
|
format(new Date(booking.date), 'yyyy-MM-dd') ===
|
|
format(new Date(dto.date), 'yyyy-MM-dd'),
|
|
);
|
|
|
|
if (previousBookings.length > 0) {
|
|
// tell the user what time is unavailable
|
|
const unavailableTimes = previousBookings.map((booking) => {
|
|
return `${booking.startTime}-${booking.endTime}`;
|
|
});
|
|
throw new ConflictException(
|
|
`Space is already booked during this times: ${unavailableTimes.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
/**
|
|
* Create bookable space entries after all validations pass
|
|
*/
|
|
private async createBookings(
|
|
space: SpaceEntity,
|
|
user: string,
|
|
{ spaceUuid, date, ...dto }: CreateBookingDto,
|
|
) {
|
|
const entry = this.bookingEntityRepository.create({
|
|
space: { uuid: spaceUuid },
|
|
user: { uuid: user },
|
|
...dto,
|
|
date: new Date(date),
|
|
cost: space.bookableConfig?.points || null,
|
|
});
|
|
|
|
return this.bookingEntityRepository.save(entry);
|
|
}
|
|
}
|