Compare commits

..

4 Commits

30 changed files with 16 additions and 1663 deletions

View File

@ -1,4 +1,4 @@
name: 🤖 AI PR Description Commenter (100% Safe with jq)
name: 🤖 AI PR Description Generator (with Template)
on:
pull_request:
@ -12,10 +12,8 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Install GitHub CLI and jq
run: |
sudo apt-get update
sudo apt-get install gh jq -y
- name: Install GitHub CLI
uses: cli/cli-action@v2
- name: Fetch PR Commits
id: fetch_commits
@ -25,31 +23,23 @@ jobs:
echo "$COMMITS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate PR Description with OpenAI (Safe JSON with jq)
- name: Generate PR Description with OpenAI
id: generate_description
run: |
REQUEST_BODY=$(jq -n \
--arg model "gpt-4o" \
--arg content "Given the following commit messages:\n\n${commits}\n\nGenerate a clear and professional pull request description." \
'{
model: $model,
messages: [{ role: "user", content: $content }]
}'
)
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d "$REQUEST_BODY")
-d '{
"model": "gpt-4o",
"messages": [{
"role": "user",
"content": "Given the following commit messages:\n\n'"${commits}"'\n\nFill the following pull request template. Only fill the \"## Description\" section:\n\n<!--\n Thanks for contributing!\n\n Provide a description of your changes below and a general summary in the title.\n-->\n\n## Jira Ticket\n\n[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)\n\n## Description\n\n<!--- Describe your changes in detail -->\n\n## How to Test\n\n<!--- Describe the created APIs / Logic -->"
}]
}')
DESCRIPTION=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
echo "---------- OpenAI Raw Response ----------"
echo "$RESPONSE"
echo "---------- Extracted Description ----------"
echo "$DESCRIPTION"
echo "description<<EOF" >> $GITHUB_ENV
echo "$DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@ -57,8 +47,8 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
commits: ${{ env.commits }}
- name: Post AI Generated Description as Comment
- name: Update PR Body with AI Description
run: |
gh pr comment ${{ github.event.pull_request.number }} --body "${{ env.description }}"
gh pr edit ${{ github.event.pull_request.number }} --body "${{ env.description }}"
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { AqiService } from './services';
import { AqiController } from './controllers';
@Module({
imports: [ConfigModule],
controllers: [AqiController],
providers: [AqiService, SqlLoaderService],
})
export class AqiModule {}

View File

@ -1,64 +0,0 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { AqiService } from '../services/aqi.service';
import {
GetAqiDailyBySpaceDto,
GetAqiPollutantBySpaceDto,
} from '../dto/get-aqi.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceParamsDto } from '../dto/aqi-params.dto';
@ApiTags('AQI Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.AQI.ROUTE,
})
export class AqiController {
constructor(private readonly aqiService: AqiService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('range/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_SUMMARY,
description: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getAQIRangeDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetAqiDailyBySpaceDto,
): Promise<BaseResponseDto> {
return await this.aqiService.getAQIRangeDataBySpace(params, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('distribution/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_SUMMARY,
description:
ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getAQIDistributionDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetAqiPollutantBySpaceDto,
): Promise<BaseResponseDto> {
return await this.aqiService.getAQIDistributionDataBySpace(params, query);
}
}

View File

@ -1 +0,0 @@
export * from './aqi.controller';

View File

@ -1,7 +0,0 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
export class SpaceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
@IsNotEmpty()
spaceUuid: string;
}

View File

@ -1,37 +0,0 @@
import { PollutantType } from '@app/common/constants/pollutants.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Matches, IsNotEmpty, IsString } from 'class-validator';
export class GetAqiDailyBySpaceDto {
@ApiProperty({
description: 'Month and year in format YYYY-MM',
example: '2025-03',
required: true,
})
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
message: 'monthDate must be in YYYY-MM format',
})
@IsNotEmpty()
monthDate: string;
}
export class GetAqiPollutantBySpaceDto {
@ApiProperty({
description: 'Pollutant Type',
enum: PollutantType,
example: PollutantType.AQI,
required: true,
})
@IsString()
@IsNotEmpty()
public pollutantType: string;
@ApiProperty({
description: 'Month and year in format YYYY-MM',
example: '2025-03',
required: true,
})
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
message: 'monthDate must be in YYYY-MM format',
})
@IsNotEmpty()
monthDate: string;
}

View File

@ -1,138 +0,0 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
GetAqiDailyBySpaceDto,
GetAqiPollutantBySpaceDto,
} from '../dto/get-aqi.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceParamsDto } from '../dto/aqi-params.dto';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { PollutantType } from '@app/common/constants/pollutants.enum';
@Injectable()
export class AqiService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async getAQIDistributionDataBySpace(
params: SpaceParamsDto,
query: GetAqiPollutantBySpaceDto,
): Promise<BaseResponseDto> {
const { monthDate, pollutantType } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'fact_daily_space_aqi',
'proceduce_select_daily_space_aqi',
[spaceUuid, monthDate],
);
const categories = [
'good',
'moderate',
'unhealthy_sensitive',
'unhealthy',
'very_unhealthy',
'hazardous',
];
const transformedData = data.map((item) => {
const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD
const categoryData = categories.map((category) => {
const key = `${category}_${pollutantType.toLowerCase()}_percentage`;
return {
type: category,
percentage: item[key] ?? 0,
};
});
return { date, data: categoryData };
});
const response = this.buildResponse(
`AQI distribution data fetched successfully for ${spaceUuid} space and pollutant ${pollutantType}`,
transformedData,
);
return response;
} catch (error) {
console.error('Failed to fetch AQI distribution data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch AQI distribution data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getAQIRangeDataBySpace(
params: SpaceParamsDto,
query: GetAqiDailyBySpaceDto,
): Promise<BaseResponseDto> {
const { monthDate } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'fact_daily_space_aqi',
'proceduce_select_daily_space_aqi',
[spaceUuid, monthDate],
);
// Define pollutants dynamically
const pollutants = Object.values(PollutantType);
const transformedData = data.map((item) => {
const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD
const dailyData = pollutants.map((type) => ({
type,
min: item[`daily_min_${type}`],
max: item[`daily_max_${type}`],
average: item[`daily_avg_${type}`],
}));
return { date, data: dailyData };
});
const response = this.buildResponse(
`AQI data fetched successfully for ${spaceUuid} space`,
transformedData,
);
return convertKeysToCamelCase(response);
} catch (error) {
console.error('Failed to fetch AQI data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch AQI data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private buildResponse(message: string, data: any[]) {
return new SuccessResponseDto({
message,
data,
statusCode: HttpStatus.OK,
});
}
private async executeProcedure(
procedureFolderName: string,
procedureFileName: string,
params: (string | number | null)[],
): Promise<any[]> {
const query = this.loadQuery(procedureFolderName, procedureFileName);
return await this.dataSource.query(query, params);
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -1 +0,0 @@
export * from './aqi.service';

View File

@ -1,29 +0,0 @@
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 { EmailService } from '@app/common/util/email/email.service';
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({
imports: [BookingRepositoryModule],
controllers: [BookableSpaceController, BookingController],
providers: [
BookableSpaceService,
BookingService,
EmailService,
BookableSpaceEntityRepository,
BookingEntityRepository,
SpaceRepository,
UserRepository,
],
exports: [BookableSpaceService, BookingService],
})
export class BookingModule {}

View File

@ -1,107 +0,0 @@
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,
Param,
ParseUUIDPipe,
Post,
Put,
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 { 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/bookable-space.service';
@ApiTags('Booking Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.BOOKABLE_SPACES.ROUTE,
})
export class BookableSpaceController {
constructor(private readonly bookableSpaceService: BookableSpaceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION,
})
async create(@Body() dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
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,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put(':spaceUuid')
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.UPDATE_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS
.UPDATE_BOOKABLE_SPACES_DESCRIPTION,
})
async update(
@Param('spaceUuid', ParseUUIDPipe) spaceUuid: string,
@Body() dto: UpdateBookableSpaceDto,
): Promise<BaseResponseDto> {
const result = await this.bookableSpaceService.update(spaceUuid, dto);
return new SuccessResponseDto({
data: result,
message: 'Successfully updated bookable spaces',
});
}
}

View File

@ -1,107 +0,0 @@
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',
});
}
}

View File

@ -1,31 +0,0 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.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(
PaginationRequestWithSearchGetListDto,
['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

@ -1,59 +0,0 @@
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,
nullable: true,
})
@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,23 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsUUID, 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;
@ApiProperty({
description: 'Space UUID',
example: '550e8400-e29b-41d4-a716-446655440000',
required: false,
})
@IsOptional()
@IsUUID('4')
space?: string;
}

View File

@ -1,88 +0,0 @@
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 }) => obj.inviteUser?.companyName || null)
companyName: string;
@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;
}

View File

@ -1,63 +0,0 @@
import { DaysEnum } from '@app/common/constants/days.enum';
import { ApiProperty } from '@nestjs/swagger';
import {
ArrayMinSize,
IsArray,
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
Matches,
Max,
Min,
} from 'class-validator';
export class CreateBookableSpaceDto {
@ApiProperty({
type: 'string',
isArray: true,
example: [
'3fa85f64-5717-4562-b3fc-2c963f66afa6',
'4fa85f64-5717-4562-b3fc-2c963f66afa7',
],
})
@IsArray()
@ArrayMinSize(1, { message: 'At least one space must be selected' })
@IsUUID('all', { each: true, message: 'Invalid space UUID provided' })
spaceUuids: string[];
@ApiProperty({
enum: DaysEnum,
isArray: true,
example: [DaysEnum.MON, DaysEnum.WED, DaysEnum.FRI],
})
@IsArray()
@ArrayMinSize(1, { message: 'At least one day must be selected' })
@IsEnum(DaysEnum, { each: true, message: 'Invalid day provided' })
daysAvailable: DaysEnum[];
@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;
@ApiProperty({ example: 10, required: false })
@IsOptional()
@IsInt()
@Min(0, { message: 'Points cannot be negative' })
@Max(1000, { message: 'Points cannot exceed 1000' })
points?: number;
}

View File

@ -1,35 +0,0 @@
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;
}

View File

@ -1,14 +0,0 @@
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';
}

View File

@ -1,12 +0,0 @@
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator';
import { CreateBookableSpaceDto } from './create-bookable-space.dto';
export class UpdateBookableSpaceDto extends PartialType(
OmitType(CreateBookableSpaceDto, ['spaceUuids']),
) {
@ApiProperty({ type: Boolean })
@IsOptional()
@IsBoolean()
active?: boolean;
}

View File

@ -1 +0,0 @@
export * from './booking.module';

View File

@ -1,370 +0,0 @@
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/bookable-space.repository';
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 { EmailService } from '@app/common/util/email/email.service';
import { to12HourFormat } from '@app/common/util/time-to-12-hours-convetion';
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { format } from 'date-fns';
import { Brackets, In } from 'typeorm';
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()
export class BookableSpaceService {
constructor(
private readonly emailService: EmailService,
private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository,
private readonly bookingEntityRepository: BookingEntityRepository,
private readonly spaceRepository: SpaceRepository,
) {}
async create(dto: CreateBookableSpaceDto) {
// Validate time slots first
this.validateTimeSlot(dto.startTime, dto.endTime);
// fetch spaces exist
const spaces = await this.getSpacesOrFindMissing(dto.spaceUuids);
// Validate no duplicate bookable configurations
await this.validateNoDuplicateBookableConfigs(dto.spaceUuids);
// Create and save bookable spaces
return this.createBookableSpaces(spaces, dto);
}
async findAll(
{ active, page, size, configured, search }: 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 (search) {
qb = qb.andWhere(
'(space.spaceName ILIKE :search OR community.name ILIKE :search OR parentSpace.spaceName ILIKE :search)',
{ search: `%${search}%` },
);
}
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,
};
}
/**
* Update bookable space configuration
*/
async update(spaceUuid: string, dto: UpdateBookableSpaceDto) {
// fetch spaces exist
const space = (await this.getSpacesOrFindMissing([spaceUuid]))[0];
if (!space.bookableConfig) {
throw new NotFoundException(
`Bookable configuration not found for space: ${spaceUuid}`,
);
}
if (dto.startTime || dto.endTime) {
// Validate time slots first
this.validateTimeSlot(
dto.startTime || space.bookableConfig.startTime,
dto.endTime || space.bookableConfig.endTime,
);
if (
dto.startTime != space.bookableConfig.startTime ||
dto.endTime != space.bookableConfig.endTime ||
dto.daysAvailable != space.bookableConfig.daysAvailable
) {
this.handleTimingUpdate(
{
daysAvailable:
dto.daysAvailable || space.bookableConfig.daysAvailable,
startTime: dto.startTime || space.bookableConfig.startTime,
endTime: dto.endTime || space.bookableConfig.endTime,
},
space,
);
}
}
if (
dto.active !== undefined &&
dto.active !== space.bookableConfig.active
) {
this.handleAvailabilityUpdate(dto.active, space);
}
Object.assign(space.bookableConfig, dto);
return this.bookableSpaceEntityRepository.save(space.bookableConfig);
}
private async handleTimingUpdate(
dto: UpdateBookableSpaceDto,
space: SpaceEntity,
): Promise<void> {
const affectedUsers = await this.getAffectedBookings(space.uuid);
if (!affectedUsers.length) return;
const groupedParams = this.groupBookingsByUser(affectedUsers);
return this.emailService.sendUpdateBookingTimingEmailWithTemplate(
groupedParams,
{
space_name: space.spaceName,
start_time: to12HourFormat(dto.startTime),
end_time: to12HourFormat(dto.endTime),
days: dto.daysAvailable.join(', '),
},
);
}
private async getAffectedBookings(spaceUuid: string) {
const today = new Date();
const nowTime = format(today, 'HH:mm');
const bookingWithDayCte = this.bookingEntityRepository
.createQueryBuilder('b')
.select('b.*')
.addSelect(
`
CASE EXTRACT(DOW FROM b.date)
WHEN 0 THEN 'Sun'
WHEN 1 THEN 'Mon'
WHEN 2 THEN 'Tue'
WHEN 3 THEN 'Wed'
WHEN 4 THEN 'Thu'
WHEN 5 THEN 'Fri'
WHEN 6 THEN 'Sat'
END::"bookable-space_days_available_enum"
`,
'booking_day',
)
.where(
`(DATE(b.date) > :today OR (DATE(b.date) = :today AND b.startTime >= :nowTime))`,
{ today, nowTime },
)
.andWhere('b.space_uuid = :spaceUuid', { spaceUuid });
const query = this.bookableSpaceEntityRepository
.createQueryBuilder('bs')
.distinct(true)
.addCommonTableExpression(bookingWithDayCte, 'booking_with_day')
.select('u.first_name', 'name')
.addSelect('u.email', 'email')
.addSelect('DATE(bwd.date)', 'date')
.addSelect('bwd.start_time', 'start_time')
.addSelect('bwd.end_time', 'end_time')
.from('booking_with_day', 'bwd')
.innerJoin('user', 'u', 'u.uuid = bwd.user_uuid')
.where('bs.space_uuid = :spaceUuid', { spaceUuid })
.andWhere(
new Brackets((qb) => {
qb.where('NOT (bwd.booking_day = ANY(bs.days_available))')
.orWhere('bwd.start_time < bs.start_time')
.orWhere('bwd.end_time > bs.end_time');
}),
);
return query.getRawMany<{
name: string;
email: string;
date: string;
start_time: string;
end_time: string;
}>();
}
private groupBookingsByUser(
bookings: {
name: string;
email: string;
date: string;
start_time: string;
end_time: string;
}[],
): {
name: string;
email: string;
bookings: { date: string; start_time: string; end_time: string }[];
}[] {
const grouped: Record<
string,
{
name: string;
email: string;
bookings: { date: string; start_time: string; end_time: string }[];
}
> = {};
for (const { name, email, date, start_time, end_time } of bookings) {
const formattedDate = format(new Date(date), 'yyyy-MM-dd');
const formattedStartTime = to12HourFormat(start_time);
const formattedEndTime = to12HourFormat(end_time);
if (!grouped[email]) {
grouped[email] = {
name,
email,
bookings: [],
};
}
grouped[email].bookings.push({
date: formattedDate,
start_time: formattedStartTime,
end_time: formattedEndTime,
});
}
return Object.values(grouped);
}
private async handleAvailabilityUpdate(
active: boolean,
space: SpaceEntity,
): Promise<void> {
space = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['userSpaces', 'userSpaces.user'],
});
const emails = space.userSpaces.map((userSpace) => ({
email: userSpace.user.email,
name: userSpace.user.firstName,
}));
if (!emails.length) return Promise.resolve();
return this.emailService.sendUpdateBookingAvailabilityEmailWithTemplate(
emails,
{
availability: active ? 'Available' : 'Unavailable',
space_name: space.spaceName,
isAvailable: active,
},
);
}
/**
* Fetch spaces by UUIDs and throw an error if any are missing
*/
private async getSpacesOrFindMissing(
spaceUuids: string[],
): Promise<SpaceEntity[]> {
const spaces = await this.spaceRepository.find({
where: { uuid: In(spaceUuids) },
relations: ['bookableConfig'],
});
if (spaces.length !== spaceUuids.length) {
const foundUuids = spaces.map((s) => s.uuid);
const missingUuids = spaceUuids.filter(
(uuid) => !foundUuids.includes(uuid),
);
throw new NotFoundException(
`Spaces not found: ${missingUuids.join(', ')}`,
);
}
return spaces;
}
/**
* Validate there are no existing bookable configurations for these spaces
*/
private async validateNoDuplicateBookableConfigs(
spaceUuids: string[],
): Promise<void> {
const existingBookables = await this.bookableSpaceEntityRepository.find({
where: { space: { uuid: In(spaceUuids) } },
relations: ['space'],
});
if (existingBookables.length > 0) {
const existingUuids = [
...new Set(existingBookables.map((b) => b.space.uuid)),
];
throw new ConflictException(
`Bookable configuration already exists for spaces: ${existingUuids.join(', ')}`,
);
}
}
/**
* 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}`,
);
}
}
/**
* Create bookable space entries after all validations pass
*/
private async createBookableSpaces(
spaces: SpaceEntity[],
dto: CreateBookableSpaceDto,
) {
try {
const entries = spaces.map((space) =>
this.bookableSpaceEntityRepository.create({
space,
daysAvailable: dto.daysAvailable,
startTime: dto.startTime,
endTime: dto.endTime,
points: dto.points,
}),
);
return this.bookableSpaceEntityRepository.save(entries);
} catch (error) {
if (error.code === '23505') {
throw new ConflictException(
'Duplicate bookable space configuration detected',
);
}
throw error;
}
}
}

View File

@ -1,218 +0,0 @@
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 { format } from 'date-fns';
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, space }: 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 } },
uuid: space ? space : undefined,
},
date: Between(fromDate, toDate),
},
relations: ['space', 'user', 'user.inviteUser'],
order: { date: 'DESC' },
});
}
async findMyBookings(
{ when }: MyBookingRequestDto,
userUuid: string,
project: string,
) {
const now = new Date();
const nowTime = format(now, 'HH:mm');
const query = this.bookingEntityRepository
.createQueryBuilder('booking')
.leftJoinAndSelect('booking.space', 'space')
.innerJoin(
'space.community',
'community',
'community.project = :project',
{ project },
)
.leftJoinAndSelect('booking.user', 'user')
.where('user.uuid = :userUuid', { userUuid });
if (when === 'past') {
query.andWhere(
`(DATE(booking.date) < :today OR (DATE(booking.date) = :today AND booking.startTime < :nowTime))`,
{ today: now, nowTime },
);
} else if (when === 'future') {
query.andWhere(
`(DATE(booking.date) > :today OR (DATE(booking.date) = :today AND booking.startTime >= :nowTime))`,
{ today: now, nowTime },
);
}
query.orderBy({
'DATE(booking.date)': 'DESC',
'booking.startTime': 'DESC',
});
return query.getMany();
}
/**
* 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 &&
format(new Date(booking.date), 'yyyy-MM-dd') ===
format(new Date(dto.date), 'yyyy-MM-dd'),
);
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);
}
}

View File

@ -1 +0,0 @@
export * from './occupancy.controller';

View File

@ -1,71 +0,0 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { OccupancyService } from '../services/occupancy.service';
import {
GetOccupancyDurationBySpaceDto,
GetOccupancyHeatMapBySpaceDto,
} from '../dto/get-occupancy.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceParamsDto } from '../dto/occupancy-params.dto';
@ApiTags('Occupancy Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.Occupancy.ROUTE,
})
export class OccupancyController {
constructor(private readonly occupancyService: OccupancyService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('heat-map/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY,
description:
ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getOccupancyHeatMapDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetOccupancyHeatMapBySpaceDto,
): Promise<BaseResponseDto> {
return await this.occupancyService.getOccupancyHeatMapDataBySpace(
params,
query,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('duration/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY,
description:
ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getOccupancyDurationDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetOccupancyDurationBySpaceDto,
): Promise<BaseResponseDto> {
return await this.occupancyService.getOccupancyDurationDataBySpace(
params,
query,
);
}
}

View File

@ -1,27 +0,0 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Matches, IsNotEmpty } from 'class-validator';
export class GetOccupancyHeatMapBySpaceDto {
@ApiPropertyOptional({
description: 'Input year in YYYY format to filter the data',
example: '2025',
required: false,
})
@IsNotEmpty()
@Matches(/^\d{4}$/, {
message: 'Year must be in YYYY format',
})
year: string;
}
export class GetOccupancyDurationBySpaceDto {
@ApiPropertyOptional({
description: 'Month and year in format YYYY-MM',
example: '2025-03',
required: true,
})
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
message: 'monthDate must be in YYYY-MM format',
})
@IsNotEmpty()
monthDate: string;
}

View File

@ -1,7 +0,0 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
export class SpaceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
@IsNotEmpty()
spaceUuid: string;
}

View File

@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { OccupancyController } from './controllers';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from './services';
@Module({
imports: [ConfigModule],
controllers: [OccupancyController],
providers: [OccupancyService, SqlLoaderService],
})
export class OccupancyModule {}

View File

@ -1 +0,0 @@
export * from './occupancy.service';

View File

@ -1,103 +0,0 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
GetOccupancyDurationBySpaceDto,
GetOccupancyHeatMapBySpaceDto,
} from '../dto/get-occupancy.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceParamsDto } from '../dto/occupancy-params.dto';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@Injectable()
export class OccupancyService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async getOccupancyDurationDataBySpace(
params: SpaceParamsDto,
query: GetOccupancyDurationBySpaceDto,
): Promise<BaseResponseDto> {
const { monthDate } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'fact_daily_space_occupancy_duration',
'procedure_select_daily_space_occupancy_duration',
[spaceUuid, monthDate],
);
const formattedData = data.map((item) => ({
...item,
event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD
}));
return this.buildResponse(
`Occupancy duration data fetched successfully for ${spaceUuid} space`,
formattedData,
);
} catch (error) {
console.error('Failed to fetch occupancy duration data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch occupancy duration data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getOccupancyHeatMapDataBySpace(
params: SpaceParamsDto,
query: GetOccupancyHeatMapBySpaceDto,
): Promise<BaseResponseDto> {
const { year } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'fact_space_occupancy_count',
'proceduce_select_fact_space_occupancy',
[spaceUuid, year],
);
const formattedData = data.map((item) => ({
...item,
event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD
}));
return this.buildResponse(
`Occupancy heat map data fetched successfully for ${spaceUuid} space`,
formattedData,
);
} catch (error) {
console.error('Failed to fetch occupancy heat map data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch occupancy heat map data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private buildResponse(message: string, data: any[]) {
return new SuccessResponseDto({
message,
data,
statusCode: HttpStatus.OK,
});
}
private async executeProcedure(
procedureFolderName: string,
procedureFileName: string,
params: (string | number | null)[],
): Promise<any[]> {
const query = this.loadQuery(procedureFolderName, procedureFileName);
return await this.dataSource.query(query, params);
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

0
test-ai.txt Normal file
View File