mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 07:07:21 +00:00
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:
@ -77,6 +77,12 @@ export class ControllerRoute {
|
||||
|
||||
public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION =
|
||||
'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 {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { DaysEnum } from '@app/common/constants/days.enum';
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
UpdateDateColumn,
|
||||
} 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 { SpaceEntity } from '../../space/entities/space.entity';
|
||||
|
||||
@Entity('bookable-space')
|
||||
export class BookableSpaceEntity extends AbstractEntity {
|
||||
@ -37,6 +37,9 @@ export class BookableSpaceEntity extends AbstractEntity {
|
||||
@Column({ type: 'time' })
|
||||
endTime: string;
|
||||
|
||||
@Column({ type: Boolean, default: true })
|
||||
active: boolean;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
points: number;
|
||||
|
||||
|
3916
package-lock.json
generated
3916
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -56,7 +56,7 @@
|
||||
"onesignal-node": "^3.4.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"winston": "^3.17.0",
|
||||
|
@ -1,11 +1,24 @@
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
|
||||
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 { 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 { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
||||
import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.dto';
|
||||
import { BookableSpaceService } from '../services';
|
||||
|
||||
@ApiTags('Booking Module')
|
||||
@ -25,6 +38,45 @@ export class BookableSpaceController {
|
||||
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION,
|
||||
})
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
31
src/booking/dtos/bookable-space-request.dto.ts
Normal file
31
src/booking/dtos/bookable-space-request.dto.ts
Normal 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;
|
||||
}
|
58
src/booking/dtos/bookable-space-response.dto.ts
Normal file
58
src/booking/dtos/bookable-space-response.dto.ts
Normal 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;
|
||||
}
|
@ -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 {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
BadRequestException,
|
||||
} 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 { timeToMinutes } from '@app/common/helper/timeToMinutes';
|
||||
import { CreateBookableSpaceDto } from '../dtos';
|
||||
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BookableSpaceService {
|
||||
@ -20,7 +22,7 @@ export class BookableSpaceService {
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
|
||||
async create(dto: CreateBookableSpaceDto) {
|
||||
// Validate time slots first
|
||||
this.validateTimeSlot(dto.startTime, dto.endTime);
|
||||
|
||||
@ -34,6 +36,47 @@ export class BookableSpaceService {
|
||||
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
|
||||
*/
|
||||
@ -98,7 +141,7 @@ export class BookableSpaceService {
|
||||
private async createBookableSpaces(
|
||||
spaces: SpaceEntity[],
|
||||
dto: CreateBookableSpaceDto,
|
||||
): Promise<BaseResponseDto> {
|
||||
) {
|
||||
try {
|
||||
const entries = spaces.map((space) =>
|
||||
this.bookableSpaceEntityRepository.create({
|
||||
@ -110,11 +153,7 @@ export class BookableSpaceService {
|
||||
}),
|
||||
);
|
||||
|
||||
const data = await this.bookableSpaceEntityRepository.save(entries);
|
||||
return new SuccessResponseDto({
|
||||
data,
|
||||
message: 'Successfully added new bookable spaces',
|
||||
});
|
||||
return this.bookableSpaceEntityRepository.save(entries);
|
||||
} catch (error) {
|
||||
if (error.code === '23505') {
|
||||
throw new ConflictException(
|
||||
|
Reference in New Issue
Block a user