mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-11 15:48:09 +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.';
|
'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 {
|
static COMMUNITY = class {
|
||||||
public static readonly ROUTE = '/projects/:projectUuid/communities';
|
public static readonly ROUTE = '/projects/:projectUuid/communities';
|
||||||
static ACTIONS = class {
|
static ACTIONS = class {
|
||||||
|
@ -14,6 +14,8 @@ import { createLogger } from 'winston';
|
|||||||
import { winstonLoggerOptions } from '../logger/services/winston.logger';
|
import { winstonLoggerOptions } from '../logger/services/winston.logger';
|
||||||
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
|
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
|
||||||
import { AutomationEntity } from '../modules/automation/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 { ClientEntity } from '../modules/client/entities';
|
||||||
import { CommunityEntity } from '../modules/community/entities';
|
import { CommunityEntity } from '../modules/community/entities';
|
||||||
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
|
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
|
||||||
@ -58,7 +60,6 @@ import {
|
|||||||
UserSpaceEntity,
|
UserSpaceEntity,
|
||||||
} from '../modules/user/entities';
|
} from '../modules/user/entities';
|
||||||
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
||||||
import { BookableSpaceEntity } from '../modules/booking/entities';
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
@ -119,6 +120,7 @@ import { BookableSpaceEntity } from '../modules/booking/entities';
|
|||||||
AqiSpaceDailyPollutantStatsEntity,
|
AqiSpaceDailyPollutantStatsEntity,
|
||||||
SpaceDailyOccupancyDurationEntity,
|
SpaceDailyOccupancyDurationEntity,
|
||||||
BookableSpaceEntity,
|
BookableSpaceEntity,
|
||||||
|
BookingEntity,
|
||||||
],
|
],
|
||||||
namingStrategy: new SnakeNamingStrategy(),
|
namingStrategy: new SnakeNamingStrategy(),
|
||||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { BookableSpaceEntity } from './entities/bookable-space.entity';
|
import { BookableSpaceEntity } from './entities/bookable-space.entity';
|
||||||
|
import { BookingEntity } from './entities/booking.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
controllers: [],
|
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 { Injectable } from '@nestjs/common';
|
||||||
import { BookableSpaceEntity } from '../entities/bookable-space.entity';
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { BookingEntity } from '../entities/booking.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BookableSpaceEntityRepository extends Repository<BookableSpaceEntity> {
|
export class BookingEntityRepository extends Repository<BookingEntity> {
|
||||||
constructor(private dataSource: DataSource) {
|
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';
|
} from 'typeorm';
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
||||||
import { BookableSpaceEntity } from '../../booking/entities';
|
import { BookableSpaceEntity } from '../../booking/entities/bookable-space.entity';
|
||||||
import { CommunityEntity } from '../../community/entities';
|
import { CommunityEntity } from '../../community/entities';
|
||||||
import { DeviceEntity } from '../../device/entities';
|
import { DeviceEntity } from '../../device/entities';
|
||||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||||
@ -20,6 +20,7 @@ import { UserSpaceEntity } from '../../user/entities';
|
|||||||
import { SpaceDto } from '../dtos';
|
import { SpaceDto } from '../dtos';
|
||||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||||
|
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||||
|
|
||||||
@Entity({ name: 'space' })
|
@Entity({ name: 'space' })
|
||||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||||
@ -132,6 +133,9 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
|||||||
@OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space)
|
@OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space)
|
||||||
bookableConfig: BookableSpaceEntity;
|
bookableConfig: BookableSpaceEntity;
|
||||||
|
|
||||||
|
@OneToMany(() => BookingEntity, (booking) => booking.space)
|
||||||
|
bookings: BookingEntity[];
|
||||||
|
|
||||||
constructor(partial: Partial<SpaceEntity>) {
|
constructor(partial: Partial<SpaceEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
UserOtpDto,
|
UserOtpDto,
|
||||||
UserSpaceDto,
|
UserSpaceDto,
|
||||||
} from '../dtos';
|
} from '../dtos';
|
||||||
|
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||||
|
|
||||||
@Entity({ name: 'user' })
|
@Entity({ name: 'user' })
|
||||||
export class UserEntity extends AbstractEntity<UserDto> {
|
export class UserEntity extends AbstractEntity<UserDto> {
|
||||||
@ -121,6 +122,9 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
|||||||
)
|
)
|
||||||
deviceUserNotification: DeviceNotificationEntity[];
|
deviceUserNotification: DeviceNotificationEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => BookingEntity, (booking) => booking.user)
|
||||||
|
bookings: BookingEntity[];
|
||||||
|
|
||||||
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
|
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
|
||||||
region: RegionEntity;
|
region: RegionEntity;
|
||||||
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
|
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { BookingRepositoryModule } from '@app/common/modules/booking/booking.repository.module';
|
||||||
import { BookableSpaceController } from './controllers';
|
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository';
|
||||||
import { BookableSpaceService } from './services';
|
import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository';
|
||||||
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
|
|
||||||
import { SpaceRepository } from '@app/common/modules/space';
|
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()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [BookableSpaceController],
|
imports: [BookingRepositoryModule],
|
||||||
|
controllers: [BookableSpaceController, BookingController],
|
||||||
providers: [
|
providers: [
|
||||||
BookableSpaceService,
|
BookableSpaceService,
|
||||||
|
BookingService,
|
||||||
|
|
||||||
BookableSpaceEntityRepository,
|
BookableSpaceEntityRepository,
|
||||||
|
BookingEntityRepository,
|
||||||
|
|
||||||
SpaceRepository,
|
SpaceRepository,
|
||||||
|
UserRepository,
|
||||||
],
|
],
|
||||||
exports: [BookableSpaceService],
|
exports: [BookableSpaceService, BookingService],
|
||||||
})
|
})
|
||||||
export class BookingModule {}
|
export class BookingModule {}
|
||||||
|
@ -19,11 +19,11 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|||||||
import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { CreateBookableSpaceDto } from '../dtos';
|
|
||||||
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
||||||
import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.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 { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
|
||||||
import { BookableSpaceService } from '../services';
|
import { BookableSpaceService } from '../services/bookable-space.service';
|
||||||
|
|
||||||
@ApiTags('Booking Module')
|
@ApiTags('Booking Module')
|
||||||
@Controller({
|
@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 { PageResponseDto } from '@app/common/dto/pagination.response.dto';
|
||||||
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
|
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
|
||||||
import { TypeORMCustomModel } from '@app/common/models/typeOrmCustom.model';
|
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 { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||||
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
|
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
|
||||||
import {
|
import {
|
||||||
@ -12,8 +12,8 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { CreateBookableSpaceDto } from '../dtos';
|
|
||||||
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
||||||
|
import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto';
|
||||||
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
|
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
|
||||||
|
|
||||||
@Injectable()
|
@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