mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-17 03:05:13 +00:00
SP-1757, SP-1758, SP-1809, SP-1810: Feat/implement booking (#469)
* fix: commission device API * task: add create booking API * add get All api for dashboard & mobile * add Find APIs for bookings * implement sending email updates on update bookable space * move email interfaces to separate files
This commit is contained in:
@ -1,12 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { ErrorMessageService } from 'src/error-message/error-message.service';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { CommonService } from './common.service';
|
import { CommonService } from './common.service';
|
||||||
|
import config from './config';
|
||||||
import { DatabaseModule } from './database/database.module';
|
import { DatabaseModule } from './database/database.module';
|
||||||
import { HelperModule } from './helper/helper.module';
|
import { HelperModule } from './helper/helper.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import config from './config';
|
|
||||||
import { EmailService } from './util/email.service';
|
|
||||||
import { ErrorMessageService } from 'src/error-message/error-message.service';
|
|
||||||
import { TuyaService } from './integrations/tuya/services/tuya.service';
|
import { TuyaService } from './integrations/tuya/services/tuya.service';
|
||||||
import { SceneDeviceRepository } from './modules/scene-device/repositories';
|
import { SceneDeviceRepository } from './modules/scene-device/repositories';
|
||||||
import { SpaceRepository } from './modules/space';
|
import { SpaceRepository } from './modules/space';
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
SubspaceModelRepository,
|
SubspaceModelRepository,
|
||||||
} from './modules/space-model';
|
} from './modules/space-model';
|
||||||
import { SubspaceRepository } from './modules/space/repositories/subspace.repository';
|
import { SubspaceRepository } from './modules/space/repositories/subspace.repository';
|
||||||
|
import { EmailService } from './util/email/email.service';
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
CommonService,
|
CommonService,
|
||||||
|
@ -10,6 +10,8 @@ export default registerAs(
|
|||||||
SMTP_USER: process.env.SMTP_USER,
|
SMTP_USER: process.env.SMTP_USER,
|
||||||
SMTP_SENDER: process.env.SMTP_SENDER,
|
SMTP_SENDER: process.env.SMTP_SENDER,
|
||||||
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
||||||
|
BATCH_EMAIL_API_URL: process.env.BATCH_EMAIL_API_URL,
|
||||||
|
SEND_EMAIL_API_URL: process.env.SEND_EMAIL_API_URL,
|
||||||
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN,
|
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN,
|
||||||
MAILTRAP_INVITATION_TEMPLATE_UUID:
|
MAILTRAP_INVITATION_TEMPLATE_UUID:
|
||||||
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID,
|
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID,
|
||||||
@ -21,5 +23,9 @@ export default registerAs(
|
|||||||
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
|
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
|
||||||
MAILTRAP_SEND_OTP_TEMPLATE_UUID:
|
MAILTRAP_SEND_OTP_TEMPLATE_UUID:
|
||||||
process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID,
|
process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID,
|
||||||
|
MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID:
|
||||||
|
process.env.MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID,
|
||||||
|
MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID:
|
||||||
|
process.env.MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export const SEND_EMAIL_API_URL_PROD = 'https://send.api.mailtrap.io/api/send/';
|
|
||||||
export const SEND_EMAIL_API_URL_DEV =
|
|
||||||
'https://sandbox.api.mailtrap.io/api/send/2634012';
|
|
@ -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, {
|
||||||
|
8
libs/common/src/util/email/batch-email.interface.ts
Normal file
8
libs/common/src/util/email/batch-email.interface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface BatchEmailData {
|
||||||
|
base: { from: { email: string }; template_uuid: string };
|
||||||
|
requests: Array<{
|
||||||
|
to: { email: string }[];
|
||||||
|
template_variables: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
isBatch: true;
|
||||||
|
}
|
@ -1,15 +1,17 @@
|
|||||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import {
|
import Mail from 'nodemailer/lib/mailer';
|
||||||
SEND_EMAIL_API_URL_DEV,
|
import { BatchEmailData } from './batch-email.interface';
|
||||||
SEND_EMAIL_API_URL_PROD,
|
import { SingleEmailData } from './single-email.interface';
|
||||||
} from '../constants/mail-trap';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
private smtpConfig: any;
|
private smtpConfig: any;
|
||||||
|
private API_TOKEN: string;
|
||||||
|
private SEND_EMAIL_API_URL: string;
|
||||||
|
private BATCH_EMAIL_API_URL: string;
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(private readonly configService: ConfigService) {
|
||||||
this.smtpConfig = {
|
this.smtpConfig = {
|
||||||
@ -22,6 +24,15 @@ export class EmailService {
|
|||||||
pass: this.configService.get<string>('email-config.SMTP_PASSWORD'),
|
pass: this.configService.get<string>('email-config.SMTP_PASSWORD'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
this.API_TOKEN = this.configService.get<string>(
|
||||||
|
'email-config.MAILTRAP_API_TOKEN',
|
||||||
|
);
|
||||||
|
this.SEND_EMAIL_API_URL = this.configService.get<string>(
|
||||||
|
'email-config.SEND_EMAIL_API_URL',
|
||||||
|
);
|
||||||
|
this.BATCH_EMAIL_API_URL = this.configService.get<string>(
|
||||||
|
'email-config.BATCH_EMAIL_API_URL',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEmail(
|
async sendEmail(
|
||||||
@ -31,7 +42,7 @@ export class EmailService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const transporter = nodemailer.createTransport(this.smtpConfig);
|
const transporter = nodemailer.createTransport(this.smtpConfig);
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions: Mail.Options = {
|
||||||
from: this.smtpConfig.sender,
|
from: this.smtpConfig.sender,
|
||||||
to: email,
|
to: email,
|
||||||
subject,
|
subject,
|
||||||
@ -44,13 +55,6 @@ export class EmailService {
|
|||||||
email: string,
|
email: string,
|
||||||
emailInvitationData: any,
|
emailInvitationData: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const API_TOKEN = this.configService.get<string>(
|
|
||||||
'email-config.MAILTRAP_API_TOKEN',
|
|
||||||
);
|
|
||||||
const API_URL = isProduction
|
|
||||||
? SEND_EMAIL_API_URL_PROD
|
|
||||||
: SEND_EMAIL_API_URL_DEV;
|
|
||||||
const TEMPLATE_UUID = this.configService.get<string>(
|
const TEMPLATE_UUID = this.configService.get<string>(
|
||||||
'email-config.MAILTRAP_INVITATION_TEMPLATE_UUID',
|
'email-config.MAILTRAP_INVITATION_TEMPLATE_UUID',
|
||||||
);
|
);
|
||||||
@ -68,21 +72,12 @@ export class EmailService {
|
|||||||
template_variables: emailInvitationData,
|
template_variables: emailInvitationData,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
return this.sendEmailWithTemplateV2({
|
||||||
await axios.post(API_URL, emailData, {
|
...emailData,
|
||||||
headers: {
|
isBatch: false,
|
||||||
Authorization: `Bearer ${API_TOKEN}`,
|
});
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.response?.data?.message ||
|
|
||||||
'Error sending email using Mailtrap template',
|
|
||||||
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEmailWithTemplate({
|
async sendEmailWithTemplate({
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
@ -94,14 +89,6 @@ export class EmailService {
|
|||||||
isEnable: boolean;
|
isEnable: boolean;
|
||||||
isDelete: boolean;
|
isDelete: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const API_TOKEN = this.configService.get<string>(
|
|
||||||
'email-config.MAILTRAP_API_TOKEN',
|
|
||||||
);
|
|
||||||
const API_URL = isProduction
|
|
||||||
? SEND_EMAIL_API_URL_PROD
|
|
||||||
: SEND_EMAIL_API_URL_DEV;
|
|
||||||
|
|
||||||
// Determine the template UUID based on the arguments
|
// Determine the template UUID based on the arguments
|
||||||
const templateUuid = isDelete
|
const templateUuid = isDelete
|
||||||
? this.configService.get<string>(
|
? this.configService.get<string>(
|
||||||
@ -128,32 +115,16 @@ export class EmailService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
return this.sendEmailWithTemplateV2({
|
||||||
await axios.post(API_URL, emailData, {
|
...emailData,
|
||||||
headers: {
|
isBatch: false,
|
||||||
Authorization: `Bearer ${API_TOKEN}`,
|
});
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.response?.data?.message ||
|
|
||||||
'Error sending email using Mailtrap template',
|
|
||||||
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEditUserEmailWithTemplate(
|
async sendEditUserEmailWithTemplate(
|
||||||
email: string,
|
email: string,
|
||||||
emailEditData: any,
|
emailEditData: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const API_TOKEN = this.configService.get<string>(
|
|
||||||
'email-config.MAILTRAP_API_TOKEN',
|
|
||||||
);
|
|
||||||
const API_URL = isProduction
|
|
||||||
? SEND_EMAIL_API_URL_PROD
|
|
||||||
: SEND_EMAIL_API_URL_DEV;
|
|
||||||
const TEMPLATE_UUID = this.configService.get<string>(
|
const TEMPLATE_UUID = this.configService.get<string>(
|
||||||
'email-config.MAILTRAP_EDIT_USER_TEMPLATE_UUID',
|
'email-config.MAILTRAP_EDIT_USER_TEMPLATE_UUID',
|
||||||
);
|
);
|
||||||
@ -171,32 +142,15 @@ export class EmailService {
|
|||||||
template_variables: emailEditData,
|
template_variables: emailEditData,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
return this.sendEmailWithTemplateV2({
|
||||||
await axios.post(API_URL, emailData, {
|
...emailData,
|
||||||
headers: {
|
isBatch: false,
|
||||||
Authorization: `Bearer ${API_TOKEN}`,
|
});
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.response?.data?.message ||
|
|
||||||
'Error sending email using Mailtrap template',
|
|
||||||
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async sendOtpEmailWithTemplate(
|
async sendOtpEmailWithTemplate(
|
||||||
email: string,
|
email: string,
|
||||||
emailEditData: any,
|
emailEditData: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const API_TOKEN = this.configService.get<string>(
|
|
||||||
'email-config.MAILTRAP_API_TOKEN',
|
|
||||||
);
|
|
||||||
const API_URL = isProduction
|
|
||||||
? SEND_EMAIL_API_URL_PROD
|
|
||||||
: SEND_EMAIL_API_URL_DEV;
|
|
||||||
const TEMPLATE_UUID = this.configService.get<string>(
|
const TEMPLATE_UUID = this.configService.get<string>(
|
||||||
'email-config.MAILTRAP_SEND_OTP_TEMPLATE_UUID',
|
'email-config.MAILTRAP_SEND_OTP_TEMPLATE_UUID',
|
||||||
);
|
);
|
||||||
@ -214,20 +168,84 @@ export class EmailService {
|
|||||||
template_variables: emailEditData,
|
template_variables: emailEditData,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
return this.sendEmailWithTemplateV2({
|
||||||
await axios.post(API_URL, emailData, {
|
...emailData,
|
||||||
headers: {
|
isBatch: false,
|
||||||
Authorization: `Bearer ${API_TOKEN}`,
|
});
|
||||||
'Content-Type': 'application/json',
|
}
|
||||||
|
|
||||||
|
async sendUpdateBookingTimingEmailWithTemplate(
|
||||||
|
emails: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
bookings: {
|
||||||
|
date: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
}[];
|
||||||
|
}[],
|
||||||
|
emailVariables: {
|
||||||
|
space_name: string;
|
||||||
|
days: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const TEMPLATE_UUID = this.configService.get<string>(
|
||||||
|
'email-config.MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID',
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailData = {
|
||||||
|
base: {
|
||||||
|
from: {
|
||||||
|
email: this.smtpConfig.sender,
|
||||||
},
|
},
|
||||||
});
|
template_uuid: TEMPLATE_UUID,
|
||||||
} catch (error) {
|
},
|
||||||
throw new HttpException(
|
requests: emails.map(({ email, name, bookings }) => ({
|
||||||
error.response?.data?.message ||
|
to: [{ email }],
|
||||||
'Error sending email using Mailtrap template',
|
template_variables: { ...emailVariables, name, bookings },
|
||||||
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
})),
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
return this.sendEmailWithTemplateV2({
|
||||||
|
...emailData,
|
||||||
|
isBatch: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendUpdateBookingAvailabilityEmailWithTemplate(
|
||||||
|
emails: { email: string; name: string }[],
|
||||||
|
emailVariables: {
|
||||||
|
space_name: string;
|
||||||
|
availability: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const TEMPLATE_UUID = this.configService.get<string>(
|
||||||
|
'email-config.MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID',
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailData = {
|
||||||
|
base: {
|
||||||
|
from: {
|
||||||
|
email: this.smtpConfig.sender,
|
||||||
|
},
|
||||||
|
template_uuid: TEMPLATE_UUID,
|
||||||
|
},
|
||||||
|
requests: emails.map(({ email, name }) => ({
|
||||||
|
to: [{ email }],
|
||||||
|
template_variables: {
|
||||||
|
...emailVariables,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.sendEmailWithTemplateV2({
|
||||||
|
...emailData,
|
||||||
|
isBatch: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
generateUserChangesEmailBody(
|
generateUserChangesEmailBody(
|
||||||
addedSpaceNames: string[],
|
addedSpaceNames: string[],
|
||||||
@ -264,4 +282,30 @@ export class EmailService {
|
|||||||
nameChanged,
|
nameChanged,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendEmailWithTemplateV2({
|
||||||
|
isBatch,
|
||||||
|
...emailData
|
||||||
|
}: BatchEmailData | SingleEmailData): Promise<void> {
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
isBatch ? this.BATCH_EMAIL_API_URL : this.SEND_EMAIL_API_URL,
|
||||||
|
{
|
||||||
|
...emailData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.data?.message ||
|
||||||
|
'Error sending email using Mailtrap template',
|
||||||
|
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
7
libs/common/src/util/email/single-email.interface.ts
Normal file
7
libs/common/src/util/email/single-email.interface.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface SingleEmailData {
|
||||||
|
from: { email: string };
|
||||||
|
to: { email: string }[];
|
||||||
|
template_uuid: string;
|
||||||
|
template_variables?: Record<string, any>;
|
||||||
|
isBatch: false;
|
||||||
|
}
|
7
libs/common/src/util/time-to-12-hours-convetion.ts
Normal file
7
libs/common/src/util/time-to-12-hours-convetion.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { format, parse } from 'date-fns';
|
||||||
|
|
||||||
|
export function to12HourFormat(timeString: string): string {
|
||||||
|
timeString = timeString.padEnd(8, ':00');
|
||||||
|
const parsedTime = parse(timeString, 'HH:mm:ss', new Date());
|
||||||
|
return format(parsedTime, 'hh:mm a');
|
||||||
|
}
|
52
package-lock.json
generated
52
package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
"firebase": "^10.12.5",
|
"firebase": "^10.12.5",
|
||||||
"google-auth-library": "^9.14.1",
|
"google-auth-library": "^9.14.1",
|
||||||
@ -40,7 +41,7 @@
|
|||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nest-winston": "^1.10.2",
|
"nest-winston": "^1.10.2",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.10",
|
"nodemailer": "^7.0.5",
|
||||||
"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",
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
@ -3366,6 +3368,15 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
|
||||||
|
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
@ -5115,6 +5126,22 @@
|
|||||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/concurrently/node_modules/date-fns": {
|
||||||
|
"version": "2.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
|
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.11"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/date-fns"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concurrently/node_modules/supports-color": {
|
"node_modules/concurrently/node_modules/supports-color": {
|
||||||
"version": "8.1.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
@ -5360,19 +5387,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "2.30.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.21.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.11"
|
|
||||||
},
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "github",
|
||||||
"url": "https://opencollective.com/date-fns"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
@ -9784,9 +9804,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "6.10.1",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||||
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
"firebase": "^10.12.5",
|
"firebase": "^10.12.5",
|
||||||
"google-auth-library": "^9.14.1",
|
"google-auth-library": "^9.14.1",
|
||||||
@ -52,7 +53,7 @@
|
|||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nest-winston": "^1.10.2",
|
"nest-winston": "^1.10.2",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.10",
|
"nodemailer": "^7.0.5",
|
||||||
"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",
|
||||||
@ -72,6 +73,7 @@
|
|||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
|
import { AuthService } from '@app/common/auth/services/auth.service';
|
||||||
|
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
||||||
|
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
|
||||||
|
import {
|
||||||
|
UserOtpRepository,
|
||||||
|
UserRepository,
|
||||||
|
} from '@app/common/modules/user/repositories';
|
||||||
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
|
|
||||||
import { UserOtpRepository } from '@app/common/modules/user/repositories';
|
|
||||||
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
|
||||||
import { RoleService } from 'src/role/services';
|
import { RoleService } from 'src/role/services';
|
||||||
import { UserAuthController } from './controllers';
|
import { UserAuthController } from './controllers';
|
||||||
import { UserAuthService } from './services';
|
import { UserAuthService } from './services';
|
||||||
import { AuthService } from '@app/common/auth/services/auth.service';
|
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
@ -16,7 +16,7 @@ import { UserSessionRepository } from '../../../libs/common/src/modules/session/
|
|||||||
import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity';
|
import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity';
|
||||||
import { UserRepository } from '../../../libs/common/src/modules/user/repositories';
|
import { UserRepository } from '../../../libs/common/src/modules/user/repositories';
|
||||||
import { UserOtpRepository } from '../../../libs/common/src/modules/user/repositories/user.repository';
|
import { UserOtpRepository } from '../../../libs/common/src/modules/user/repositories/user.repository';
|
||||||
import { EmailService } from '../../../libs/common/src/util/email.service';
|
import { EmailService } from '../../../libs/common/src/util/email/email.service';
|
||||||
import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos';
|
import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos';
|
||||||
import { UserSignUpDto } from '../dtos/user-auth.dto';
|
import { UserSignUpDto } from '../dtos/user-auth.dto';
|
||||||
import { UserLoginDto } from '../dtos/user-login.dto';
|
import { UserLoginDto } from '../dtos/user-login.dto';
|
||||||
|
@ -1,17 +1,29 @@
|
|||||||
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 { 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()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [BookableSpaceController],
|
imports: [BookingRepositoryModule],
|
||||||
|
controllers: [BookableSpaceController, BookingController],
|
||||||
providers: [
|
providers: [
|
||||||
BookableSpaceService,
|
BookableSpaceService,
|
||||||
|
BookingService,
|
||||||
|
EmailService,
|
||||||
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;
|
||||||
|
}
|
88
src/booking/dtos/booking-response.dto.ts
Normal file
88
src/booking/dtos/booking-response.dto.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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;
|
||||||
|
}
|
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,24 +2,30 @@ 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 { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.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 { EmailService } from '@app/common/util/email/email.service';
|
||||||
|
import { to12HourFormat } from '@app/common/util/time-to-12-hours-convetion';
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { format } from 'date-fns';
|
||||||
import { CreateBookableSpaceDto } from '../dtos';
|
import { Brackets, In } from 'typeorm';
|
||||||
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()
|
||||||
export class BookableSpaceService {
|
export class BookableSpaceService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly emailService: EmailService,
|
||||||
private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository,
|
private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository,
|
||||||
|
private readonly bookingEntityRepository: BookingEntityRepository,
|
||||||
private readonly spaceRepository: SpaceRepository,
|
private readonly spaceRepository: SpaceRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -84,8 +90,7 @@ export class BookableSpaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* todo: if updating availability, send to the ones who have access to this space
|
* Update bookable space configuration
|
||||||
* todo: if updating other fields, just send emails to all users who's bookings might be affected
|
|
||||||
*/
|
*/
|
||||||
async update(spaceUuid: string, dto: UpdateBookableSpaceDto) {
|
async update(spaceUuid: string, dto: UpdateBookableSpaceDto) {
|
||||||
// fetch spaces exist
|
// fetch spaces exist
|
||||||
@ -102,11 +107,179 @@ export class BookableSpaceService {
|
|||||||
dto.startTime || space.bookableConfig.startTime,
|
dto.startTime || space.bookableConfig.startTime,
|
||||||
dto.endTime || space.bookableConfig.endTime,
|
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);
|
Object.assign(space.bookableConfig, dto);
|
||||||
return this.bookableSpaceEntityRepository.save(space.bookableConfig);
|
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
|
* Fetch spaces by UUIDs and throw an error if any are missing
|
||||||
*/
|
*/
|
||||||
|
215
src/booking/services/booking.service.ts
Normal file
215
src/booking/services/booking.service.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
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 }: 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,
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
export * from './bookable-space.service';
|
|
@ -9,6 +9,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
|
|||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||||
@ -27,6 +28,7 @@ import {
|
|||||||
PowerClampHourlyRepository,
|
PowerClampHourlyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
|
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||||
import { ProductRepository } from '@app/common/modules/product/repositories';
|
import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
import { RegionRepository } from '@app/common/modules/region/repositories';
|
import { RegionRepository } from '@app/common/modules/region/repositories';
|
||||||
@ -57,7 +59,7 @@ import {
|
|||||||
UserRepository,
|
UserRepository,
|
||||||
UserSpaceRepository,
|
UserSpaceRepository,
|
||||||
} from '@app/common/modules/user/repositories';
|
} from '@app/common/modules/user/repositories';
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
import { CommunityModule } from 'src/community/community.module';
|
import { CommunityModule } from 'src/community/community.module';
|
||||||
import { CommunityService } from 'src/community/services';
|
import { CommunityService } from 'src/community/services';
|
||||||
import { DeviceService } from 'src/device/services';
|
import { DeviceService } from 'src/device/services';
|
||||||
@ -81,8 +83,6 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su
|
|||||||
import { TagService as NewTagService } from 'src/tags/services';
|
import { TagService as NewTagService } from 'src/tags/services';
|
||||||
import { UserDevicePermissionService } from 'src/user-device-permission/services';
|
import { UserDevicePermissionService } from 'src/user-device-permission/services';
|
||||||
import { UserService, UserSpaceService } from 'src/users/services';
|
import { UserService, UserSpaceService } from 'src/users/services';
|
||||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
|
||||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
||||||
|
@ -15,7 +15,7 @@ import { SpaceRepository } from '@app/common/modules/space';
|
|||||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||||
import { UserEntity } from '@app/common/modules/user/entities';
|
import { UserEntity } from '@app/common/modules/user/entities';
|
||||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
HttpException,
|
HttpException,
|
||||||
|
@ -28,7 +28,7 @@ import { PasswordType } from '@app/common/constants/password-type.enum';
|
|||||||
import { VisitorPasswordEnum } from '@app/common/constants/visitor-password.enum';
|
import { VisitorPasswordEnum } from '@app/common/constants/visitor-password.enum';
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
import { DeviceService } from 'src/device/services';
|
import { DeviceService } from 'src/device/services';
|
||||||
import { DoorLockService } from 'src/door-lock/services';
|
import { DoorLockService } from 'src/door-lock/services';
|
||||||
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
|
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
|
||||||
|
@ -1,39 +1,39 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { VisitorPasswordService } from './services/visitor-password.service';
|
|
||||||
import { VisitorPasswordController } from './controllers/visitor-password.controller';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { DeviceRepositoryModule } from '@app/common/modules/device';
|
|
||||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
|
||||||
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
|
|
||||||
import { DoorLockModule } from 'src/door-lock/door.lock.module';
|
|
||||||
import { DeviceService } from 'src/device/services';
|
|
||||||
import { ProductRepository } from '@app/common/modules/product/repositories';
|
|
||||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories';
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
import { SceneService } from 'src/scene/services';
|
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||||
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceRepositoryModule } from '@app/common/modules/device';
|
||||||
|
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||||
|
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||||
|
import {
|
||||||
|
PowerClampDailyRepository,
|
||||||
|
PowerClampHourlyRepository,
|
||||||
|
PowerClampMonthlyRepository,
|
||||||
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
|
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||||
|
import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||||
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
import {
|
import {
|
||||||
SceneIconRepository,
|
SceneIconRepository,
|
||||||
SceneRepository,
|
SceneRepository,
|
||||||
} from '@app/common/modules/scene/repositories';
|
} from '@app/common/modules/scene/repositories';
|
||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
import { Module } from '@nestjs/common';
|
||||||
import {
|
import { ConfigModule } from '@nestjs/config';
|
||||||
PowerClampHourlyRepository,
|
import { DeviceService } from 'src/device/services';
|
||||||
PowerClampDailyRepository,
|
import { DoorLockModule } from 'src/door-lock/door.lock.module';
|
||||||
PowerClampMonthlyRepository,
|
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
import { SceneService } from 'src/scene/services';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { VisitorPasswordController } from './controllers/visitor-password.controller';
|
||||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
import { VisitorPasswordService } from './services/visitor-password.service';
|
||||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
|
||||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
|
||||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
|
||||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
|
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
|
||||||
controllers: [VisitorPasswordController],
|
controllers: [VisitorPasswordController],
|
||||||
|
Reference in New Issue
Block a user