SP-1753 Feat/booking system (#454)

* task: add get all bookable spaces API (#453)

* task: add non bookable space API
This commit is contained in:
ZaydSkaff
2025-07-08 09:02:24 +03:00
committed by GitHub
parent 66391bafd8
commit 18b21d697c
8 changed files with 1818 additions and 2347 deletions

View File

@ -77,6 +77,12 @@ export class ControllerRoute {
public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION = public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to add new bookable spaces by providing the required details.'; 'This endpoint allows you to add new bookable spaces by providing the required details.';
public static readonly GET_ALL_BOOKABLE_SPACES_SUMMARY =
'Get all bookable spaces';
public static readonly GET_ALL_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint retrieves all bookable spaces.';
}; };
}; };
static COMMUNITY = class { static COMMUNITY = class {

View File

@ -1,14 +1,14 @@
import { DaysEnum } from '@app/common/constants/days.enum';
import { import {
Entity,
Column, Column,
CreateDateColumn, CreateDateColumn,
UpdateDateColumn, Entity,
OneToOne,
JoinColumn, JoinColumn,
OneToOne,
UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { SpaceEntity } from '../../space/entities/space.entity';
import { DaysEnum } from '@app/common/constants/days.enum';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity('bookable-space') @Entity('bookable-space')
export class BookableSpaceEntity extends AbstractEntity { export class BookableSpaceEntity extends AbstractEntity {
@ -37,6 +37,9 @@ export class BookableSpaceEntity extends AbstractEntity {
@Column({ type: 'time' }) @Column({ type: 'time' })
endTime: string; endTime: string;
@Column({ type: Boolean, default: true })
active: boolean;
@Column({ type: 'int' }) @Column({ type: 'int' })
points: number; points: number;

3922
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,7 @@
"onesignal-node": "^3.4.0", "onesignal-node": "^3.4.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"winston": "^3.17.0", "winston": "^3.17.0",

View File

@ -1,11 +1,24 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { ControllerRoute } from '@app/common/constants/controller-route';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { ControllerRoute } from '@app/common/constants/controller-route'; 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 { 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 { CreateBookableSpaceDto } from '../dtos';
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.dto';
import { BookableSpaceService } from '../services'; import { BookableSpaceService } from '../services';
@ApiTags('Booking Module') @ApiTags('Booking Module')
@ -25,6 +38,45 @@ export class BookableSpaceController {
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION, ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION,
}) })
async create(@Body() dto: CreateBookableSpaceDto): Promise<BaseResponseDto> { async create(@Body() dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
return this.bookableSpaceService.create(dto); const result = await this.bookableSpaceService.create(dto);
return new SuccessResponseDto({
data: result,
message: 'Successfully created bookable spaces',
});
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.GET_ALL_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS
.GET_ALL_BOOKABLE_SPACES_DESCRIPTION,
})
async findAll(
@Query() query: BookableSpaceRequestDto,
@Req() req: Request,
): Promise<PageResponse<BookableSpaceResponseDto>> {
const project = req['user']?.project?.uuid;
if (!project) {
throw new Error('Project UUID is required in the request');
}
const { data, pagination } = await this.bookableSpaceService.findAll(
query,
project,
);
return new PageResponse<BookableSpaceResponseDto>(
{
data: data.map((space) =>
plainToInstance(BookableSpaceResponseDto, space, {
excludeExtraneousValues: true,
}),
),
message: 'Successfully fetched all bookable spaces',
},
pagination,
);
} }
} }

View File

@ -0,0 +1,31 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
export class BookableSpaceRequestDto extends OmitType(
PaginationRequestGetListDto,
['includeSpaces'],
) {
@ApiProperty({
type: Boolean,
required: false,
})
@IsBoolean()
@IsOptional()
@Transform(({ obj }) => {
return obj.active === BooleanValues.TRUE;
})
active?: boolean;
@ApiProperty({
type: Boolean,
})
@IsBoolean()
@IsNotEmpty()
@Transform(({ obj }) => {
return obj.configured === BooleanValues.TRUE;
})
configured: boolean;
}

View File

@ -0,0 +1,58 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';
export class BookableSpaceConfigResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty({
type: [String],
})
@Expose()
daysAvailable: string[];
@ApiProperty()
@Expose()
startTime: string;
@ApiProperty()
@Expose()
endTime: string;
@ApiProperty({
type: Boolean,
})
@Expose()
active: boolean;
@ApiProperty({
type: Number,
})
@Expose()
points: number;
}
export class BookableSpaceResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty()
@Expose()
spaceUuid: string;
@ApiProperty()
@Expose()
spaceName: string;
@ApiProperty()
@Expose()
virtualLocation: string;
@ApiProperty({
type: BookableSpaceConfigResponseDto,
})
@Expose()
@Type(() => BookableSpaceConfigResponseDto)
bookableConfig: BookableSpaceConfigResponseDto;
}

View File

@ -1,17 +1,19 @@
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 { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
import { import {
BadRequestException,
ConflictException,
Injectable, Injectable,
NotFoundException, NotFoundException,
ConflictException,
BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { CreateBookableSpaceDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { timeToMinutes } from '@app/common/helper/timeToMinutes'; import { CreateBookableSpaceDto } from '../dtos';
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
@Injectable() @Injectable()
export class BookableSpaceService { export class BookableSpaceService {
@ -20,7 +22,7 @@ export class BookableSpaceService {
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
) {} ) {}
async create(dto: CreateBookableSpaceDto): Promise<BaseResponseDto> { async create(dto: CreateBookableSpaceDto) {
// Validate time slots first // Validate time slots first
this.validateTimeSlot(dto.startTime, dto.endTime); this.validateTimeSlot(dto.startTime, dto.endTime);
@ -34,6 +36,47 @@ export class BookableSpaceService {
return this.createBookableSpaces(spaces, dto); return this.createBookableSpaces(spaces, dto);
} }
async findAll(
{ active, page, size, configured }: BookableSpaceRequestDto,
project: string,
): Promise<{
data: BaseResponseDto['data'];
pagination: PageResponseDto;
}> {
let qb = this.spaceRepository
.createQueryBuilder('space')
.leftJoinAndSelect('space.parent', 'parentSpace')
.leftJoinAndSelect('space.community', 'community')
.where('community.project = :project', { project });
if (configured) {
qb = qb
.leftJoinAndSelect('space.bookableConfig', 'bookableConfig')
.andWhere('bookableConfig.uuid IS NOT NULL');
if (active !== undefined) {
qb = qb.andWhere('bookableConfig.active = :active', { active });
}
} else {
qb = qb
.leftJoinAndSelect('space.bookableConfig', 'bookableConfig')
.andWhere('bookableConfig.uuid IS NULL');
}
const customModel = TypeORMCustomModel(this.spaceRepository);
const { baseResponseDto, paginationResponseDto } =
await customModel.findAll({ page, size, modelName: 'space' }, qb);
return {
data: baseResponseDto.data.map((space) => {
return {
...space,
virtualLocation: `${space.community?.name} - ${space.parent ? space.parent?.spaceName + ' - ' : ''}${space.spaceName}`,
};
}),
pagination: paginationResponseDto,
};
}
/** /**
* Fetch spaces by UUIDs and throw an error if any are missing * Fetch spaces by UUIDs and throw an error if any are missing
*/ */
@ -98,7 +141,7 @@ export class BookableSpaceService {
private async createBookableSpaces( private async createBookableSpaces(
spaces: SpaceEntity[], spaces: SpaceEntity[],
dto: CreateBookableSpaceDto, dto: CreateBookableSpaceDto,
): Promise<BaseResponseDto> { ) {
try { try {
const entries = spaces.map((space) => const entries = spaces.map((space) =>
this.bookableSpaceEntityRepository.create({ this.bookableSpaceEntityRepository.create({
@ -110,11 +153,7 @@ export class BookableSpaceService {
}), }),
); );
const data = await this.bookableSpaceEntityRepository.save(entries); return this.bookableSpaceEntityRepository.save(entries);
return new SuccessResponseDto({
data,
message: 'Successfully added new bookable spaces',
});
} catch (error) { } catch (error) {
if (error.code === '23505') { if (error.code === '23505') {
throw new ConflictException( throw new ConflictException(