mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-15 18:27:05 +00:00
Compare commits
1 Commits
dev
...
fix/remove
Author | SHA1 | Date | |
---|---|---|---|
2c3b985594 |
@ -1,11 +1,12 @@
|
|||||||
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';
|
||||||
@ -14,7 +15,6 @@ 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,8 +10,6 @@ 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,
|
||||||
@ -23,9 +21,5 @@ 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,25 +91,6 @@ 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 {
|
||||||
|
3
libs/common/src/constants/mail-trap.ts
Normal file
3
libs/common/src/constants/mail-trap.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
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,8 +14,6 @@ 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';
|
||||||
@ -60,6 +58,7 @@ 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({
|
||||||
@ -120,7 +119,6 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/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,12 +1,11 @@
|
|||||||
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, BookingEntity])],
|
imports: [TypeOrmModule.forFeature([BookableSpaceEntity])],
|
||||||
})
|
})
|
||||||
export class BookingRepositoryModule {}
|
export class BookableRepositoryModule {}
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
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
libs/common/src/modules/booking/entities/index.ts
Normal file
1
libs/common/src/modules/booking/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './bookable-space.entity';
|
@ -1,10 +0,0 @@
|
|||||||
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 { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
import { DataSource, Repository } from 'typeorm';
|
||||||
import { BookingEntity } from '../entities/booking.entity';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { BookableSpaceEntity } from '../entities/bookable-space.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BookingEntityRepository extends Repository<BookingEntity> {
|
export class BookableSpaceEntityRepository extends Repository<BookableSpaceEntity> {
|
||||||
constructor(private dataSource: DataSource) {
|
constructor(private dataSource: DataSource) {
|
||||||
super(BookingEntity, dataSource.createEntityManager());
|
super(BookableSpaceEntity, dataSource.createEntityManager());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
libs/common/src/modules/booking/repositories/index.ts
Normal file
1
libs/common/src/modules/booking/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
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/bookable-space.entity';
|
import { BookableSpaceEntity } from '../../booking/entities';
|
||||||
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,7 +20,6 @@ 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> {
|
||||||
@ -76,13 +75,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
|||||||
})
|
})
|
||||||
subspaces?: SubspaceEntity[];
|
subspaces?: SubspaceEntity[];
|
||||||
|
|
||||||
// Position columns
|
|
||||||
@Column({ type: 'float', nullable: false, default: 0 })
|
|
||||||
public x: number; // X coordinate for position
|
|
||||||
|
|
||||||
@Column({ type: 'float', nullable: false, default: 0 })
|
|
||||||
public y: number; // Y coordinate for position
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => DeviceEntity,
|
() => DeviceEntity,
|
||||||
(devicesSpaceEntity) => devicesSpaceEntity.spaceDevice,
|
(devicesSpaceEntity) => devicesSpaceEntity.spaceDevice,
|
||||||
@ -133,9 +125,6 @@ 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,7 +29,6 @@ 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> {
|
||||||
@ -122,9 +121,6 @@ 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, {
|
||||||
|
@ -2,16 +2,14 @@ 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 * as nodemailer from 'nodemailer';
|
||||||
import Mail from 'nodemailer/lib/mailer';
|
import {
|
||||||
import { BatchEmailData } from './batch-email.interface';
|
SEND_EMAIL_API_URL_DEV,
|
||||||
import { SingleEmailData } from './single-email.interface';
|
SEND_EMAIL_API_URL_PROD,
|
||||||
|
} 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 = {
|
||||||
@ -24,15 +22,6 @@ 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(
|
||||||
@ -42,7 +31,7 @@ export class EmailService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const transporter = nodemailer.createTransport(this.smtpConfig);
|
const transporter = nodemailer.createTransport(this.smtpConfig);
|
||||||
|
|
||||||
const mailOptions: Mail.Options = {
|
const mailOptions = {
|
||||||
from: this.smtpConfig.sender,
|
from: this.smtpConfig.sender,
|
||||||
to: email,
|
to: email,
|
||||||
subject,
|
subject,
|
||||||
@ -55,6 +44,13 @@ 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',
|
||||||
);
|
);
|
||||||
@ -72,12 +68,21 @@ export class EmailService {
|
|||||||
template_variables: emailInvitationData,
|
template_variables: emailInvitationData,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sendEmailWithTemplateV2({
|
try {
|
||||||
...emailData,
|
await axios.post(API_URL, emailData, {
|
||||||
isBatch: false,
|
headers: {
|
||||||
|
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,
|
||||||
@ -89,6 +94,14 @@ 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>(
|
||||||
@ -115,16 +128,32 @@ export class EmailService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sendEmailWithTemplateV2({
|
try {
|
||||||
...emailData,
|
await axios.post(API_URL, emailData, {
|
||||||
isBatch: false,
|
headers: {
|
||||||
|
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',
|
||||||
);
|
);
|
||||||
@ -142,15 +171,32 @@ export class EmailService {
|
|||||||
template_variables: emailEditData,
|
template_variables: emailEditData,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sendEmailWithTemplateV2({
|
try {
|
||||||
...emailData,
|
await axios.post(API_URL, emailData, {
|
||||||
isBatch: false,
|
headers: {
|
||||||
|
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',
|
||||||
);
|
);
|
||||||
@ -168,84 +214,20 @@ export class EmailService {
|
|||||||
template_variables: emailEditData,
|
template_variables: emailEditData,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sendEmailWithTemplateV2({
|
try {
|
||||||
...emailData,
|
await axios.post(API_URL, emailData, {
|
||||||
isBatch: false,
|
headers: {
|
||||||
});
|
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>(
|
} catch (error) {
|
||||||
'email-config.MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID',
|
throw new HttpException(
|
||||||
|
error.response?.data?.message ||
|
||||||
|
'Error sending email using Mailtrap template',
|
||||||
|
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
|
|
||||||
const emailData = {
|
|
||||||
base: {
|
|
||||||
from: {
|
|
||||||
email: this.smtpConfig.sender,
|
|
||||||
},
|
|
||||||
template_uuid: TEMPLATE_UUID,
|
|
||||||
},
|
|
||||||
requests: emails.map(({ email, name, bookings }) => ({
|
|
||||||
to: [{ email }],
|
|
||||||
template_variables: { ...emailVariables, name, bookings },
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
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[],
|
||||||
@ -282,30 +264,4 @@ 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,8 +0,0 @@
|
|||||||
export interface BatchEmailData {
|
|
||||||
base: { from: { email: string }; template_uuid: string };
|
|
||||||
requests: Array<{
|
|
||||||
to: { email: string }[];
|
|
||||||
template_variables: Record<string, any>;
|
|
||||||
}>;
|
|
||||||
isBatch: true;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface SingleEmailData {
|
|
||||||
from: { email: string };
|
|
||||||
to: { email: string }[];
|
|
||||||
template_uuid: string;
|
|
||||||
template_variables?: Record<string, any>;
|
|
||||||
isBatch: false;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
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,7 +32,6 @@
|
|||||||
"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",
|
||||||
@ -41,7 +40,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": "^7.0.5",
|
"nodemailer": "^6.9.10",
|
||||||
"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",
|
||||||
@ -61,7 +60,6 @@
|
|||||||
"@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",
|
||||||
@ -3368,15 +3366,6 @@
|
|||||||
"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",
|
||||||
@ -5126,22 +5115,6 @@
|
|||||||
"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",
|
||||||
@ -5387,12 +5360,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "4.1.0",
|
"version": "2.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.11"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "opencollective",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://opencollective.com/date-fns"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
@ -9804,9 +9784,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "7.0.5",
|
"version": "6.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||||
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@
|
|||||||
"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",
|
||||||
@ -53,7 +52,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": "^7.0.5",
|
"nodemailer": "^6.9.10",
|
||||||
"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",
|
||||||
@ -73,7 +72,6 @@
|
|||||||
"@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,17 +1,15 @@
|
|||||||
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 { JwtService } from '@nestjs/jwt';
|
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||||
|
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/email.service';
|
import { EmailService } from '../../../libs/common/src/util/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,29 +1,17 @@
|
|||||||
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 { Global, Module } from '@nestjs/common';
|
||||||
import { BookableSpaceController } from './controllers/bookable-space.controller';
|
import { BookableSpaceController } from './controllers';
|
||||||
import { BookingController } from './controllers/booking.controller';
|
import { BookableSpaceService } from './services';
|
||||||
import { BookableSpaceService } from './services/bookable-space.service';
|
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
|
||||||
import { BookingService } from './services/booking.service';
|
import { SpaceRepository } from '@app/common/modules/space';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BookingRepositoryModule],
|
controllers: [BookableSpaceController],
|
||||||
controllers: [BookableSpaceController, BookingController],
|
|
||||||
providers: [
|
providers: [
|
||||||
BookableSpaceService,
|
BookableSpaceService,
|
||||||
BookingService,
|
|
||||||
EmailService,
|
|
||||||
BookableSpaceEntityRepository,
|
BookableSpaceEntityRepository,
|
||||||
BookingEntityRepository,
|
|
||||||
|
|
||||||
SpaceRepository,
|
SpaceRepository,
|
||||||
UserRepository,
|
|
||||||
],
|
],
|
||||||
exports: [BookableSpaceService, BookingService],
|
exports: [BookableSpaceService],
|
||||||
})
|
})
|
||||||
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/bookable-space.service';
|
import { BookableSpaceService } from '../services';
|
||||||
|
|
||||||
@ApiTags('Booking Module')
|
@ApiTags('Booking Module')
|
||||||
@Controller({
|
@Controller({
|
||||||
|
@ -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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
1
src/booking/controllers/index.ts
Normal file
1
src/booking/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './bookable-space.controller';
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
1
src/booking/dtos/index.ts
Normal file
1
src/booking/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './create-bookable-space.dto';
|
@ -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';
|
|
||||||
}
|
|
@ -2,30 +2,24 @@ 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/bookable-space.repository';
|
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
|
||||||
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 { format } from 'date-fns';
|
import { In } from 'typeorm';
|
||||||
import { Brackets, In } from 'typeorm';
|
import { CreateBookableSpaceDto } from '../dtos';
|
||||||
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,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -90,7 +84,8 @@ export class BookableSpaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update bookable space configuration
|
* todo: if updating availability, send to the ones who have access to this space
|
||||||
|
* 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
|
||||||
@ -107,179 +102,11 @@ 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
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
1
src/booking/services/index.ts
Normal file
1
src/booking/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './bookable-space.service';
|
@ -9,7 +9,6 @@ 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';
|
||||||
@ -28,7 +27,6 @@ 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';
|
||||||
@ -59,7 +57,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/email.service';
|
import { EmailService } from '@app/common/util/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';
|
||||||
@ -83,6 +81,8 @@ 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/email.service';
|
import { EmailService } from '@app/common/util/email.service';
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
HttpException,
|
HttpException,
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
IsArray,
|
IsArray,
|
||||||
IsMongoId,
|
IsMongoId,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
@ -48,14 +47,6 @@ export class AddSpaceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
public icon?: string;
|
public icon?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'X position on canvas', example: 120 })
|
|
||||||
@IsNumber()
|
|
||||||
x: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Y position on canvas', example: 200 })
|
|
||||||
@IsNumber()
|
|
||||||
y: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'UUID of the Space Model',
|
description: 'UUID of the Space Model',
|
||||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||||
|
@ -4,7 +4,6 @@ import { Type } from 'class-transformer';
|
|||||||
import {
|
import {
|
||||||
ArrayUnique,
|
ArrayUnique,
|
||||||
IsArray,
|
IsArray,
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
NotEquals,
|
NotEquals,
|
||||||
@ -36,16 +35,6 @@ export class UpdateSpaceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
public icon?: string;
|
public icon?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'X position on canvas', example: 120 })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
x?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Y position on canvas', example: 200 })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
y?: number;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'List of subspace modifications',
|
description: 'List of subspace modifications',
|
||||||
type: [UpdateSubspaceDto],
|
type: [UpdateSubspaceDto],
|
||||||
|
@ -93,6 +93,9 @@ export class SpaceService {
|
|||||||
parentUuid && !isRecursiveCall
|
parentUuid && !isRecursiveCall
|
||||||
? await this.validationService.validateSpace(parentUuid)
|
? await this.validationService.validateSpace(parentUuid)
|
||||||
: null;
|
: null;
|
||||||
|
if (parent) {
|
||||||
|
await this.validateNamingConflict(addSpaceDto.spaceName, parent);
|
||||||
|
}
|
||||||
|
|
||||||
const spaceModel = spaceModelUuid
|
const spaceModel = spaceModelUuid
|
||||||
? await this.validationService.validateSpaceModel(spaceModelUuid)
|
? await this.validationService.validateSpaceModel(spaceModelUuid)
|
||||||
@ -102,8 +105,6 @@ export class SpaceService {
|
|||||||
// todo: find a better way to handle this instead of naming every key
|
// todo: find a better way to handle this instead of naming every key
|
||||||
spaceName: addSpaceDto.spaceName,
|
spaceName: addSpaceDto.spaceName,
|
||||||
icon: addSpaceDto.icon,
|
icon: addSpaceDto.icon,
|
||||||
x: addSpaceDto.x,
|
|
||||||
y: addSpaceDto.y,
|
|
||||||
spaceModel,
|
spaceModel,
|
||||||
parent: isRecursiveCall
|
parent: isRecursiveCall
|
||||||
? recursiveCallParentEntity
|
? recursiveCallParentEntity
|
||||||
@ -505,6 +506,8 @@ export class SpaceService {
|
|||||||
spaceUuid,
|
spaceUuid,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.validateNamingConflict(updateSpaceDto.spaceName, space, true);
|
||||||
|
|
||||||
if (space.spaceModel && !updateSpaceDto.spaceModelUuid) {
|
if (space.spaceModel && !updateSpaceDto.spaceModelUuid) {
|
||||||
await queryRunner.manager.update(SpaceEntity, space.uuid, {
|
await queryRunner.manager.update(SpaceEntity, space.uuid, {
|
||||||
spaceModel: null,
|
spaceModel: null,
|
||||||
@ -655,13 +658,11 @@ export class SpaceService {
|
|||||||
updateSpaceDto: UpdateSpaceDto,
|
updateSpaceDto: UpdateSpaceDto,
|
||||||
queryRunner: QueryRunner,
|
queryRunner: QueryRunner,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { spaceName, x, y, icon } = updateSpaceDto;
|
const { spaceName, icon } = updateSpaceDto;
|
||||||
|
|
||||||
const updateFields: Partial<SpaceEntity> = {};
|
const updateFields: Partial<SpaceEntity> = {};
|
||||||
|
|
||||||
if (spaceName) updateFields.spaceName = spaceName;
|
if (spaceName) updateFields.spaceName = spaceName;
|
||||||
if (x !== undefined) updateFields.x = x;
|
|
||||||
if (y !== undefined) updateFields.y = y;
|
|
||||||
if (icon) updateFields.icon = icon;
|
if (icon) updateFields.icon = icon;
|
||||||
|
|
||||||
if (Object.keys(updateFields).length > 0) {
|
if (Object.keys(updateFields).length > 0) {
|
||||||
@ -828,4 +829,34 @@ export class SpaceService {
|
|||||||
queryRunner,
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateNamingConflict(
|
||||||
|
newSpaceName: string,
|
||||||
|
parent: SpaceEntity,
|
||||||
|
isUpdate: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!isUpdate && parent.spaceName === newSpaceName) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Space can't be created with the same name as its parent space`,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (parent.children?.some((child) => child.spaceName === newSpaceName)) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Space name cannot be the same as one of its siblings/children`,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isUpdate) {
|
||||||
|
const sibling = await this.spaceRepository.exists({
|
||||||
|
where: { spaceName: newSpaceName, parent: { uuid: parent.uuid } },
|
||||||
|
});
|
||||||
|
if (sibling) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Space name cannot be the same as one of its siblings/children`,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/email.service';
|
import { EmailService } from '@app/common/util/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 { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
import { Module } from '@nestjs/common';
|
||||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
import { VisitorPasswordService } from './services/visitor-password.service';
|
||||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
import { VisitorPasswordController } from './controllers/visitor-password.controller';
|
||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.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 { CommunityRepository } from '@app/common/modules/community/repositories';
|
|
||||||
import { DeviceRepositoryModule } from '@app/common/modules/device';
|
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 { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||||
import {
|
import { EmailService } from '@app/common/util/email.service';
|
||||||
PowerClampDailyRepository,
|
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
|
||||||
PowerClampHourlyRepository,
|
import { DoorLockModule } from 'src/door-lock/door.lock.module';
|
||||||
PowerClampMonthlyRepository,
|
import { DeviceService } from 'src/device/services';
|
||||||
} 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 { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||||
|
import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories';
|
||||||
|
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { SceneService } from 'src/scene/services';
|
||||||
import {
|
import {
|
||||||
SceneIconRepository,
|
SceneIconRepository,
|
||||||
SceneRepository,
|
SceneRepository,
|
||||||
} from '@app/common/modules/scene/repositories';
|
} from '@app/common/modules/scene/repositories';
|
||||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories';
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
import { EmailService } from '@app/common/util/email/email.service';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
import { Module } from '@nestjs/common';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import {
|
||||||
import { DeviceService } from 'src/device/services';
|
PowerClampHourlyRepository,
|
||||||
import { DoorLockModule } from 'src/door-lock/door.lock.module';
|
PowerClampDailyRepository,
|
||||||
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
|
PowerClampMonthlyRepository,
|
||||||
import { SceneService } from 'src/scene/services';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { VisitorPasswordController } from './controllers/visitor-password.controller';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
import { VisitorPasswordService } from './services/visitor-password.service';
|
import { OccupancyService } from '@app/common/helper/services/occupancy.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