mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-11 07:38:49 +00:00
Compare commits
4 Commits
dev
...
feat/imple
Author | SHA1 | Date | |
---|---|---|---|
a585eea329 | |||
b026fa593b | |||
48a6d15c96 | |||
a8d33bbc52 |
@ -91,6 +91,25 @@ export class ControllerRoute {
|
||||
'This endpoint allows you to update existing bookable spaces by providing the required details.';
|
||||
};
|
||||
};
|
||||
static BOOKING = class {
|
||||
public static readonly ROUTE = 'bookings';
|
||||
static ACTIONS = class {
|
||||
public static readonly ADD_BOOKING_SUMMARY = 'Add new booking';
|
||||
|
||||
public static readonly ADD_BOOKING_DESCRIPTION =
|
||||
'This endpoint allows you to add new booking by providing the required details.';
|
||||
|
||||
public static readonly GET_ALL_BOOKINGS_SUMMARY = 'Get all bookings';
|
||||
|
||||
public static readonly GET_ALL_BOOKINGS_DESCRIPTION =
|
||||
'This endpoint retrieves all bookings.';
|
||||
|
||||
public static readonly GET_MY_BOOKINGS_SUMMARY = 'Get my bookings';
|
||||
|
||||
public static readonly GET_MY_BOOKINGS_DESCRIPTION =
|
||||
'This endpoint retrieves all bookings for the authenticated user.';
|
||||
};
|
||||
};
|
||||
static COMMUNITY = class {
|
||||
public static readonly ROUTE = '/projects/:projectUuid/communities';
|
||||
static ACTIONS = class {
|
||||
|
@ -14,6 +14,8 @@ import { createLogger } from 'winston';
|
||||
import { winstonLoggerOptions } from '../logger/services/winston.logger';
|
||||
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
|
||||
import { AutomationEntity } from '../modules/automation/entities';
|
||||
import { BookableSpaceEntity } from '../modules/booking/entities/bookable-space.entity';
|
||||
import { BookingEntity } from '../modules/booking/entities/booking.entity';
|
||||
import { ClientEntity } from '../modules/client/entities';
|
||||
import { CommunityEntity } from '../modules/community/entities';
|
||||
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
|
||||
@ -58,7 +60,6 @@ import {
|
||||
UserSpaceEntity,
|
||||
} from '../modules/user/entities';
|
||||
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
||||
import { BookableSpaceEntity } from '../modules/booking/entities';
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
@ -119,6 +120,7 @@ import { BookableSpaceEntity } from '../modules/booking/entities';
|
||||
AqiSpaceDailyPollutantStatsEntity,
|
||||
SpaceDailyOccupancyDurationEntity,
|
||||
BookableSpaceEntity,
|
||||
BookingEntity,
|
||||
],
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BookableSpaceEntity } from './entities/bookable-space.entity';
|
||||
import { BookingEntity } from './entities/booking.entity';
|
||||
|
||||
@Module({
|
||||
providers: [],
|
||||
exports: [],
|
||||
controllers: [],
|
||||
imports: [TypeOrmModule.forFeature([BookableSpaceEntity])],
|
||||
imports: [TypeOrmModule.forFeature([BookableSpaceEntity, BookingEntity])],
|
||||
})
|
||||
export class BookableRepositoryModule {}
|
||||
export class BookingRepositoryModule {}
|
||||
|
44
libs/common/src/modules/booking/entities/booking.entity.ts
Normal file
44
libs/common/src/modules/booking/entities/booking.entity.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||
import { UserEntity } from '../../user/entities';
|
||||
|
||||
@Entity('booking')
|
||||
export class BookingEntity extends AbstractEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
default: () => 'gen_random_uuid()',
|
||||
nullable: false,
|
||||
})
|
||||
public uuid: string;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.bookableConfig)
|
||||
space: SpaceEntity;
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.bookings)
|
||||
user: UserEntity;
|
||||
|
||||
@Column({ type: Date, nullable: false })
|
||||
date: Date;
|
||||
|
||||
@Column({ type: 'time' })
|
||||
startTime: string;
|
||||
|
||||
@Column({ type: 'time' })
|
||||
endTime: string;
|
||||
|
||||
@Column({ type: 'int', default: null })
|
||||
cost?: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './bookable-space.entity';
|
@ -0,0 +1,10 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BookableSpaceEntity } from '../entities/bookable-space.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BookableSpaceEntityRepository extends Repository<BookableSpaceEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(BookableSpaceEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BookableSpaceEntity } from '../entities/bookable-space.entity';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { BookingEntity } from '../entities/booking.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BookableSpaceEntityRepository extends Repository<BookableSpaceEntity> {
|
||||
export class BookingEntityRepository extends Repository<BookingEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(BookableSpaceEntity, dataSource.createEntityManager());
|
||||
super(BookingEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './booking.repository';
|
@ -8,7 +8,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
||||
import { BookableSpaceEntity } from '../../booking/entities';
|
||||
import { BookableSpaceEntity } from '../../booking/entities/bookable-space.entity';
|
||||
import { CommunityEntity } from '../../community/entities';
|
||||
import { DeviceEntity } from '../../device/entities';
|
||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||
@ -20,6 +20,7 @@ import { UserSpaceEntity } from '../../user/entities';
|
||||
import { SpaceDto } from '../dtos';
|
||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
|
||||
@Entity({ name: 'space' })
|
||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@ -132,6 +133,9 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space)
|
||||
bookableConfig: BookableSpaceEntity;
|
||||
|
||||
@OneToMany(() => BookingEntity, (booking) => booking.space)
|
||||
bookings: BookingEntity[];
|
||||
|
||||
constructor(partial: Partial<SpaceEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
UserOtpDto,
|
||||
UserSpaceDto,
|
||||
} from '../dtos';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
|
||||
@Entity({ name: 'user' })
|
||||
export class UserEntity extends AbstractEntity<UserDto> {
|
||||
@ -121,6 +122,9 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
||||
)
|
||||
deviceUserNotification: DeviceNotificationEntity[];
|
||||
|
||||
@OneToMany(() => BookingEntity, (booking) => booking.user)
|
||||
bookings: BookingEntity[];
|
||||
|
||||
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
|
||||
region: RegionEntity;
|
||||
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
|
||||
|
@ -1,17 +1,28 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { BookableSpaceController } from './controllers';
|
||||
import { BookableSpaceService } from './services';
|
||||
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
|
||||
import { BookingRepositoryModule } from '@app/common/modules/booking/booking.repository.module';
|
||||
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository';
|
||||
import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository';
|
||||
import { SpaceRepository } from '@app/common/modules/space';
|
||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { BookableSpaceController } from './controllers/bookable-space.controller';
|
||||
import { BookingController } from './controllers/booking.controller';
|
||||
import { BookableSpaceService } from './services/bookable-space.service';
|
||||
import { BookingService } from './services/booking.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
controllers: [BookableSpaceController],
|
||||
imports: [BookingRepositoryModule],
|
||||
controllers: [BookableSpaceController, BookingController],
|
||||
providers: [
|
||||
BookableSpaceService,
|
||||
BookingService,
|
||||
|
||||
BookableSpaceEntityRepository,
|
||||
BookingEntityRepository,
|
||||
|
||||
SpaceRepository,
|
||||
UserRepository,
|
||||
],
|
||||
exports: [BookableSpaceService],
|
||||
exports: [BookableSpaceService, BookingService],
|
||||
})
|
||||
export class BookingModule {}
|
||||
|
@ -19,11 +19,11 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { CreateBookableSpaceDto } from '../dtos';
|
||||
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
||||
import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.dto';
|
||||
import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto';
|
||||
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
|
||||
import { BookableSpaceService } from '../services';
|
||||
import { BookableSpaceService } from '../services/bookable-space.service';
|
||||
|
||||
@ApiTags('Booking Module')
|
||||
@Controller({
|
||||
|
107
src/booking/controllers/booking.controller.ts
Normal file
107
src/booking/controllers/booking.controller.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
|
||||
import { BookingRequestDto } from '../dtos/booking-request.dto';
|
||||
import { BookingResponseDto } from '../dtos/booking-response.dto';
|
||||
import { CreateBookingDto } from '../dtos/create-booking.dto';
|
||||
import { MyBookingRequestDto } from '../dtos/my-booking-request.dto';
|
||||
import { BookingService } from '../services/booking.service';
|
||||
|
||||
@ApiTags('Booking Module')
|
||||
@Controller({
|
||||
version: EnableDisableStatusEnum.ENABLED,
|
||||
path: ControllerRoute.BOOKING.ROUTE,
|
||||
})
|
||||
export class BookingController {
|
||||
constructor(private readonly bookingService: BookingService) {}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.BOOKING.ACTIONS.ADD_BOOKING_SUMMARY,
|
||||
description: ControllerRoute.BOOKING.ACTIONS.ADD_BOOKING_DESCRIPTION,
|
||||
})
|
||||
async create(
|
||||
@Body() dto: CreateBookingDto,
|
||||
@Req() req: Request,
|
||||
): Promise<BaseResponseDto> {
|
||||
const userUuid = req['user']?.uuid;
|
||||
if (!userUuid) {
|
||||
throw new Error('User UUID is required in the request');
|
||||
}
|
||||
const result = await this.bookingService.create(userUuid, dto);
|
||||
return new SuccessResponseDto({
|
||||
data: result,
|
||||
message: 'Successfully created booking',
|
||||
});
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AdminRoleGuard)
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.BOOKING.ACTIONS.GET_ALL_BOOKINGS_SUMMARY,
|
||||
description: ControllerRoute.BOOKING.ACTIONS.GET_ALL_BOOKINGS_DESCRIPTION,
|
||||
})
|
||||
async findAll(
|
||||
@Query() query: BookingRequestDto,
|
||||
@Req() req: Request,
|
||||
): Promise<BaseResponseDto> {
|
||||
const project = req['user']?.project?.uuid;
|
||||
if (!project) {
|
||||
throw new Error('Project UUID is required in the request');
|
||||
}
|
||||
const result = await this.bookingService.findAll(query, project);
|
||||
return new SuccessResponseDto({
|
||||
data: plainToInstance(BookingResponseDto, result, {
|
||||
excludeExtraneousValues: true,
|
||||
}),
|
||||
message: 'Successfully fetched all bookings',
|
||||
});
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('my-bookings')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.BOOKING.ACTIONS.GET_MY_BOOKINGS_SUMMARY,
|
||||
description: ControllerRoute.BOOKING.ACTIONS.GET_MY_BOOKINGS_DESCRIPTION,
|
||||
})
|
||||
async findMyBookings(
|
||||
@Query() query: MyBookingRequestDto,
|
||||
@Req() req: Request,
|
||||
): Promise<BaseResponseDto> {
|
||||
const userUuid = req['user']?.uuid;
|
||||
const project = req['user']?.project?.uuid;
|
||||
if (!project) {
|
||||
throw new Error('Project UUID is required in the request');
|
||||
}
|
||||
const result = await this.bookingService.findMyBookings(
|
||||
query,
|
||||
userUuid,
|
||||
project,
|
||||
);
|
||||
return new SuccessResponseDto({
|
||||
data: plainToInstance(BookingResponseDto, result, {
|
||||
excludeExtraneousValues: true,
|
||||
}),
|
||||
message: 'Successfully fetched all bookings',
|
||||
});
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './bookable-space.controller';
|
14
src/booking/dtos/booking-request.dto.ts
Normal file
14
src/booking/dtos/booking-request.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, Matches } from 'class-validator';
|
||||
|
||||
export class BookingRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'Month in MM/YYYY format',
|
||||
example: '07/2025',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@Matches(/^(0[1-9]|1[0-2])\/\d{4}$/, {
|
||||
message: 'Date must be in MM/YYYY format',
|
||||
})
|
||||
month: string;
|
||||
}
|
90
src/booking/dtos/booking-response.dto.ts
Normal file
90
src/booking/dtos/booking-response.dto.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose, Transform, Type } from 'class-transformer';
|
||||
|
||||
export class BookingUserResponseDto {
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
uuid: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
nullable: true,
|
||||
})
|
||||
@Expose()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
nullable: true,
|
||||
})
|
||||
@Expose()
|
||||
@Transform(({ obj }) => {
|
||||
return {
|
||||
companyName: obj.inviteUser?.companyName || null,
|
||||
};
|
||||
})
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
nullable: true,
|
||||
})
|
||||
@Expose()
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export class BookingSpaceResponseDto {
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
uuid: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
spaceName: string;
|
||||
}
|
||||
|
||||
export class BookingResponseDto {
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
uuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
})
|
||||
@Expose()
|
||||
date: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
startTime: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
endTime: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
})
|
||||
@Expose()
|
||||
cost: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: BookingUserResponseDto,
|
||||
})
|
||||
@Type(() => BookingUserResponseDto)
|
||||
@Expose()
|
||||
user: BookingUserResponseDto;
|
||||
|
||||
@ApiProperty({
|
||||
type: BookingSpaceResponseDto,
|
||||
})
|
||||
@Type(() => BookingSpaceResponseDto)
|
||||
@Expose()
|
||||
space: BookingSpaceResponseDto;
|
||||
}
|
35
src/booking/dtos/create-booking.dto.ts
Normal file
35
src/booking/dtos/create-booking.dto.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDate, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator';
|
||||
|
||||
export class CreateBookingDto {
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
example: '4fa85f64-5717-4562-b3fc-2c963f66afa7',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsUUID('4', { message: 'Invalid space UUID provided' })
|
||||
spaceUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsDate()
|
||||
date: Date;
|
||||
|
||||
@ApiProperty({ example: '09:00' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Start time cannot be empty' })
|
||||
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
|
||||
message: 'Start time must be in HH:mm format (24-hour)',
|
||||
})
|
||||
startTime: string;
|
||||
|
||||
@ApiProperty({ example: '17:00' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'End time cannot be empty' })
|
||||
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
|
||||
message: 'End time must be in HH:mm format (24-hour)',
|
||||
})
|
||||
endTime: string;
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './create-bookable-space.dto';
|
14
src/booking/dtos/my-booking-request.dto.ts
Normal file
14
src/booking/dtos/my-booking-request.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsIn, IsOptional } from 'class-validator';
|
||||
|
||||
export class MyBookingRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'Filter bookings by time period',
|
||||
example: 'past',
|
||||
enum: ['past', 'future'],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(['past', 'future'])
|
||||
when?: 'past' | 'future';
|
||||
}
|
@ -2,7 +2,7 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { PageResponseDto } from '@app/common/dto/pagination.response.dto';
|
||||
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
|
||||
import { TypeORMCustomModel } from '@app/common/models/typeOrmCustom.model';
|
||||
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
|
||||
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
|
||||
import {
|
||||
@ -12,8 +12,8 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { CreateBookableSpaceDto } from '../dtos';
|
||||
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
||||
import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto';
|
||||
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
|
||||
|
||||
@Injectable()
|
||||
|
192
src/booking/services/booking.service.ts
Normal file
192
src/booking/services/booking.service.ts
Normal file
@ -0,0 +1,192 @@
|
||||
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 { 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 }: 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 } } },
|
||||
date: Between(fromDate, toDate),
|
||||
},
|
||||
relations: ['space', 'user', 'user.inviteUser'],
|
||||
order: { date: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findMyBookings(
|
||||
{ when }: MyBookingRequestDto,
|
||||
userUuid: string,
|
||||
project: string,
|
||||
) {
|
||||
return this.bookingEntityRepository.find({
|
||||
where: {
|
||||
user: { uuid: userUuid },
|
||||
space: { community: { project: { uuid: project } } },
|
||||
// date: when
|
||||
// ? when === 'past'
|
||||
// ? LessThanOrEqual(new Date())
|
||||
// : MoreThanOrEqual(new Date())
|
||||
// : undefined,
|
||||
},
|
||||
relations: ['space', 'user'],
|
||||
order: { date: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './bookable-space.service';
|
Reference in New Issue
Block a user