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 =
|
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 {
|
||||||
|
@ -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
3922
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
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(
|
||||||
|
Reference in New Issue
Block a user