mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-08-25 21:09:39 +00:00
Compare commits
16 Commits
212d0d1974
...
task/add-a
Author | SHA1 | Date | |
---|---|---|---|
dcdb10a1fb | |||
2aa6a40af7 | |||
7ae826eb71 | |||
beed6fcfb7 | |||
015470b5ea | |||
36640b104b | |||
d4af9eaccc | |||
f447cfa065 | |||
9d952d2fb0 | |||
c0849c2252 | |||
9254db08f9 | |||
b9c4308d1c | |||
db95cc0dab | |||
87c380ab6f | |||
a060f92208 | |||
2c3b985594 |
@ -223,6 +223,10 @@ export class ControllerRoute {
|
||||
public static readonly CREATE_SPACE_DESCRIPTION =
|
||||
'This endpoint allows you to create a space in a specified community. Optionally, you can specify a parent space to nest the new space under it.';
|
||||
|
||||
public static readonly DUPLICATE_SPACE_SUMMARY = 'Duplicate a space';
|
||||
public static readonly DUPLICATE_SPACE_DESCRIPTION =
|
||||
'This endpoint allows you to create a copy of an existing space in a specified community.';
|
||||
|
||||
public static readonly LIST_SPACE_SUMMARY = 'List spaces in community';
|
||||
public static readonly LIST_SPACE_DESCRIPTION =
|
||||
'List spaces in specified community by community id';
|
||||
|
3
libs/common/src/constants/timer-job-type.enum.ts
Normal file
3
libs/common/src/constants/timer-job-type.enum.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum TimerJobTypeEnum {
|
||||
INVITE_USER_EMAIL = 'INVITE_USER_EMAIL',
|
||||
}
|
@ -54,6 +54,7 @@ import { SpaceEntity } from '../modules/space/entities/space.entity';
|
||||
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
||||
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
|
||||
import { TimerEntity } from '../modules/timer/entities/timer.entity';
|
||||
import { TimeZoneEntity } from '../modules/timezone/entities';
|
||||
import {
|
||||
UserNotificationEntity,
|
||||
@ -121,6 +122,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
||||
SpaceDailyOccupancyDurationEntity,
|
||||
BookableSpaceEntity,
|
||||
BookingEntity,
|
||||
TimerEntity,
|
||||
],
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||
|
@ -15,17 +15,16 @@ import { ProjectEntity } from '../../project/entities';
|
||||
import { RoleTypeEntity } from '../../role-type/entities';
|
||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||
import { UserEntity } from '../../user/entities';
|
||||
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
|
||||
|
||||
@Entity({ name: 'invite-user' })
|
||||
@Unique(['email', 'project'])
|
||||
export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
|
||||
export class InviteUserEntity extends AbstractEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
default: () => 'gen_random_uuid()',
|
||||
nullable: false,
|
||||
})
|
||||
public uuid: string;
|
||||
uuid: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
@ -49,50 +48,67 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
|
||||
status: string;
|
||||
|
||||
@Column()
|
||||
public firstName: string;
|
||||
firstName: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
public lastName: string;
|
||||
lastName: string;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
})
|
||||
public phoneNumber: string;
|
||||
phoneNumber: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: true,
|
||||
})
|
||||
public isActive: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: true,
|
||||
})
|
||||
public isEnabled: boolean;
|
||||
isEnabled: boolean;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
public invitationCode: string;
|
||||
invitationCode: string;
|
||||
|
||||
@Column({
|
||||
default: new Date(),
|
||||
type: 'date',
|
||||
})
|
||||
accessStartDate: Date;
|
||||
|
||||
@Column({
|
||||
type: 'date',
|
||||
nullable: true,
|
||||
})
|
||||
accessEndDate?: Date;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
enum: Object.values(RoleType),
|
||||
})
|
||||
public invitedBy: string;
|
||||
invitedBy: string;
|
||||
|
||||
@ManyToOne(() => RoleTypeEntity, (roleType) => roleType.invitedUsers, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
public roleType: RoleTypeEntity;
|
||||
roleType: RoleTypeEntity;
|
||||
|
||||
@OneToOne(() => UserEntity, (user) => user.inviteUser, {
|
||||
nullable: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'user_uuid' })
|
||||
user: UserEntity;
|
||||
|
||||
@OneToMany(
|
||||
() => InviteUserSpaceEntity,
|
||||
(inviteUserSpace) => inviteUserSpace.inviteUser,
|
||||
@ -103,32 +119,34 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'project_uuid' })
|
||||
public project: ProjectEntity;
|
||||
project: ProjectEntity;
|
||||
|
||||
constructor(partial: Partial<InviteUserEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
|
||||
@Entity({ name: 'invite-user-space' })
|
||||
@Unique(['inviteUser', 'space'])
|
||||
export class InviteUserSpaceEntity extends AbstractEntity<InviteUserSpaceDto> {
|
||||
export class InviteUserSpaceEntity extends AbstractEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
default: () => 'gen_random_uuid()',
|
||||
nullable: false,
|
||||
})
|
||||
public uuid: string;
|
||||
uuid: string;
|
||||
|
||||
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'invite_user_uuid' })
|
||||
public inviteUser: InviteUserEntity;
|
||||
inviteUser: InviteUserEntity;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.invitedUsers)
|
||||
@JoinColumn({ name: 'space_uuid' })
|
||||
public space: SpaceEntity;
|
||||
space: SpaceEntity;
|
||||
|
||||
constructor(partial: Partial<InviteUserSpaceEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -22,6 +22,11 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
||||
})
|
||||
public space: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'space_uuid',
|
||||
})
|
||||
public spaceUuid: string;
|
||||
|
||||
@ManyToOne(() => SpaceModelProductAllocationEntity, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -31,9 +36,19 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@Column({
|
||||
name: 'product_uuid',
|
||||
})
|
||||
public productUuid: string;
|
||||
|
||||
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||
public tag: NewTagEntity;
|
||||
|
||||
@Column({
|
||||
name: 'tag_uuid',
|
||||
})
|
||||
public tagUuid: string;
|
||||
|
||||
constructor(partial: Partial<SpaceProductAllocationEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
||||
import { BookableSpaceEntity } from '../../booking/entities/bookable-space.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
import { CommunityEntity } from '../../community/entities';
|
||||
import { DeviceEntity } from '../../device/entities';
|
||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||
@ -20,7 +21,6 @@ import { UserSpaceEntity } from '../../user/entities';
|
||||
import { SpaceDto } from '../dtos';
|
||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
|
||||
@Entity({ name: 'space' })
|
||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@ -47,12 +47,26 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
community: CommunityEntity;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true })
|
||||
@Column({
|
||||
name: 'community_id',
|
||||
})
|
||||
communityId: string;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.children, {
|
||||
nullable: true,
|
||||
})
|
||||
parent: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'parent_uuid',
|
||||
nullable: true,
|
||||
})
|
||||
public parentUuid: string;
|
||||
|
||||
@OneToMany(() => SpaceEntity, (space) => space.parent, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
cascade: true,
|
||||
})
|
||||
children: SpaceEntity[];
|
||||
|
||||
@ -73,16 +87,10 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
|
||||
@OneToMany(() => SubspaceEntity, (subspace) => subspace.space, {
|
||||
nullable: true,
|
||||
cascade: true,
|
||||
})
|
||||
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(
|
||||
() => DeviceEntity,
|
||||
(devicesSpaceEntity) => devicesSpaceEntity.spaceDevice,
|
||||
|
@ -22,6 +22,11 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
||||
})
|
||||
public subspace: SubspaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'subspace_uuid',
|
||||
})
|
||||
public subspaceUuid: string;
|
||||
|
||||
@ManyToOne(() => SubspaceModelProductAllocationEntity, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -31,9 +36,19 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@Column({
|
||||
name: 'product_uuid',
|
||||
})
|
||||
public productUuid: string;
|
||||
|
||||
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||
public tag: NewTagEntity;
|
||||
|
||||
@Column({
|
||||
name: 'tag_uuid',
|
||||
})
|
||||
public tagUuid: string;
|
||||
|
||||
constructor(partial: Partial<SubspaceProductAllocationEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -26,6 +26,11 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
|
||||
@JoinColumn({ name: 'space_uuid' })
|
||||
space: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'space_uuid',
|
||||
})
|
||||
public spaceUuid: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: false,
|
||||
|
37
libs/common/src/modules/timer/entities/timer.entity.ts
Normal file
37
libs/common/src/modules/timer/entities/timer.entity.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
|
||||
@Entity({ name: 'timer' })
|
||||
export class TimerEntity extends AbstractEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
default: () => 'gen_random_uuid()',
|
||||
nullable: false,
|
||||
})
|
||||
uuid: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
enum: Object.values(TimerJobTypeEnum),
|
||||
type: String,
|
||||
})
|
||||
type: TimerJobTypeEnum;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'date',
|
||||
})
|
||||
triggerDate: Date;
|
||||
|
||||
@Column({
|
||||
type: 'jsonb',
|
||||
nullable: true,
|
||||
})
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
constructor(partial: Partial<TimerEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TimerEntity } from '../entities/timer.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TimerRepository extends Repository<TimerEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(TimerEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
12
libs/common/src/modules/timer/timer.repository.module.ts
Normal file
12
libs/common/src/modules/timer/timer.repository.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TimerEntity } from './entities/timer.entity';
|
||||
import { TimerRepository } from './repositories/timer.repository';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TimerEntity])],
|
||||
providers: [TimerRepository],
|
||||
exports: [TimerRepository],
|
||||
})
|
||||
export class TimerRepositoryModule {}
|
@ -11,6 +11,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { OtpType } from '../../../../src/constants/otp-type.enum';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
import { ClientEntity } from '../../client/entities';
|
||||
import {
|
||||
DeviceNotificationEntity,
|
||||
@ -29,7 +30,6 @@ import {
|
||||
UserOtpDto,
|
||||
UserSpaceDto,
|
||||
} from '../dtos';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
|
||||
@Entity({ name: 'user' })
|
||||
export class UserEntity extends AbstractEntity<UserDto> {
|
||||
@ -101,6 +101,9 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
appAgreementAcceptedAt: Date;
|
||||
|
||||
@Column({ type: Boolean, default: false })
|
||||
bookingEnabled: boolean;
|
||||
|
||||
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
31
package-lock.json
generated
31
package-lock.json
generated
@ -48,6 +48,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
@ -2365,6 +2366,18 @@
|
||||
"rxjs": "^7.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cqrs/node_modules/uuid": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/jwt": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz",
|
||||
@ -12749,18 +12762,6 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
@ -12896,9 +12897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
@ -60,6 +60,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
|
@ -35,16 +35,18 @@ import { UserNotificationModule } from './user-notification/user-notification.mo
|
||||
import { UserModule } from './users/user.module';
|
||||
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
|
||||
|
||||
import { TimerRepositoryModule } from '@app/common/modules/timer/timer.repository.module';
|
||||
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { ThrottlerModule } from '@nestjs/throttler/dist/throttler.module';
|
||||
import { isArray } from 'class-validator';
|
||||
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
||||
import { AqiModule } from './aqi/aqi.module';
|
||||
import { OccupancyModule } from './occupancy/occupancy.module';
|
||||
import { WeatherModule } from './weather/weather.module';
|
||||
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
|
||||
import { SchedulerModule } from './scheduler/scheduler.module';
|
||||
import { BookingModule } from './booking';
|
||||
import { OccupancyModule } from './occupancy/occupancy.module';
|
||||
import { SchedulerModule } from './scheduler/scheduler.module';
|
||||
import { TimerModule } from './timer/timer.module';
|
||||
import { WeatherModule } from './weather/weather.module';
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@ -63,6 +65,8 @@ import { BookingModule } from './booking';
|
||||
},
|
||||
}),
|
||||
WinstonModule.forRoot(winstonLoggerOptions),
|
||||
TimerModule,
|
||||
TimerRepositoryModule,
|
||||
ClientModule,
|
||||
AuthenticationModule,
|
||||
UserModule,
|
||||
|
@ -7,8 +7,8 @@ export class BookingRequestDto {
|
||||
example: '07-2025',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@Matches(/^(0[1-9]|1[0-2])\/\d{4}$/, {
|
||||
message: 'Date must be in MM-YYYY format',
|
||||
@Matches(/^(0[1-9]|1[0-2])\-\d{4}$/, {
|
||||
message: 'Date must be in MM/YYYY format',
|
||||
})
|
||||
month: string;
|
||||
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDate, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator';
|
||||
import {
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
MinDate,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateBookingDto {
|
||||
@ApiProperty({
|
||||
@ -15,6 +22,7 @@ export class CreateBookingDto {
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsDate()
|
||||
@MinDate(new Date())
|
||||
date: Date;
|
||||
|
||||
@ApiProperty({ example: '09:00' })
|
||||
|
@ -54,7 +54,8 @@ export class BookableSpaceService {
|
||||
.createQueryBuilder('space')
|
||||
.leftJoinAndSelect('space.parent', 'parentSpace')
|
||||
.leftJoinAndSelect('space.community', 'community')
|
||||
.where('community.project = :project', { project });
|
||||
.where('space.disabled = :disabled', { disabled: false })
|
||||
.andWhere('community.project = :project', { project });
|
||||
|
||||
if (search) {
|
||||
qb = qb.andWhere(
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { SpaceService } from 'src/space/services';
|
||||
import { QueryRunner, SelectQueryBuilder } from 'typeorm';
|
||||
import { Brackets, QueryRunner, SelectQueryBuilder } from 'typeorm';
|
||||
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
||||
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
||||
|
||||
@ -184,18 +184,46 @@ export class CommunityService {
|
||||
|
||||
let qb: undefined | SelectQueryBuilder<CommunityEntity> = undefined;
|
||||
|
||||
const matchingCommunityIdsQb = this.communityRepository
|
||||
.createQueryBuilder('c')
|
||||
.select('c.uuid')
|
||||
.where('c.project = :projectUuid', { projectUuid })
|
||||
.andWhere('c.name != :orphanCommunityName', {
|
||||
orphanCommunityName: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
|
||||
})
|
||||
.distinct(true);
|
||||
if (includeSpaces) {
|
||||
matchingCommunityIdsQb.leftJoin('c.spaces', 'space');
|
||||
}
|
||||
|
||||
if (search) {
|
||||
matchingCommunityIdsQb
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('c.name ILIKE :search');
|
||||
if (includeSpaces) qb.orWhere('space.spaceName ILIKE :search');
|
||||
}),
|
||||
)
|
||||
.setParameter('search', `%${search}%`);
|
||||
}
|
||||
|
||||
qb = this.communityRepository
|
||||
.createQueryBuilder('c')
|
||||
.where('c.project = :projectUuid', { projectUuid })
|
||||
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
|
||||
.distinct(true);
|
||||
|
||||
.andWhere('c.name != :orphanCommunityName', {
|
||||
orphanCommunityName: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
|
||||
})
|
||||
.andWhere(`c.uuid IN (${matchingCommunityIdsQb.getQuery()})`)
|
||||
.setParameters(matchingCommunityIdsQb.getParameters());
|
||||
if (includeSpaces) {
|
||||
qb.leftJoinAndSelect(
|
||||
'c.spaces',
|
||||
'space',
|
||||
'space.disabled = :disabled AND space.spaceName != :orphanSpaceName',
|
||||
{ disabled: false, orphanSpaceName: ORPHAN_SPACE_NAME },
|
||||
{
|
||||
disabled: false,
|
||||
orphanSpaceName: ORPHAN_SPACE_NAME,
|
||||
},
|
||||
)
|
||||
.leftJoinAndSelect('space.parent', 'parent')
|
||||
.leftJoinAndSelect(
|
||||
@ -204,16 +232,7 @@ export class CommunityService {
|
||||
'children.disabled = :disabled',
|
||||
{ disabled: false },
|
||||
);
|
||||
// .leftJoinAndSelect('space.spaceModel', 'spaceModel')
|
||||
}
|
||||
|
||||
if (search) {
|
||||
qb.andWhere(
|
||||
`c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`,
|
||||
{ search: `%${search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
const customModel = TypeORMCustomModel(this.communityRepository);
|
||||
|
||||
const { baseResponseDto, paginationResponseDto } =
|
||||
|
@ -2,10 +2,12 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MinDate,
|
||||
} from 'class-validator';
|
||||
|
||||
export class AddUserInvitationDto {
|
||||
@ -16,7 +18,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public firstName: string;
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The last name of the user',
|
||||
@ -25,7 +27,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public lastName: string;
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The email of the user',
|
||||
@ -34,7 +36,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public email: string;
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The job title of the user',
|
||||
@ -43,7 +45,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public jobTitle?: string;
|
||||
jobTitle?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The company name of the user',
|
||||
@ -52,7 +54,27 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public companyName?: string;
|
||||
companyName?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Access start date',
|
||||
example: new Date(),
|
||||
required: false,
|
||||
})
|
||||
@IsDate()
|
||||
@MinDate(new Date())
|
||||
@IsOptional()
|
||||
accessStartDate?: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Access start date',
|
||||
example: new Date(),
|
||||
required: false,
|
||||
})
|
||||
@IsDate()
|
||||
@MinDate(new Date())
|
||||
@IsOptional()
|
||||
accessEndDate?: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The phone number of the user',
|
||||
@ -61,7 +83,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public phoneNumber?: string;
|
||||
phoneNumber?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The role uuid of the user',
|
||||
@ -70,7 +92,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsUUID('4')
|
||||
@IsNotEmpty()
|
||||
public roleUuid: string;
|
||||
roleUuid: string;
|
||||
@ApiProperty({
|
||||
description: 'The project uuid of the user',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
@ -78,7 +100,7 @@ export class AddUserInvitationDto {
|
||||
})
|
||||
@IsUUID('4')
|
||||
@IsNotEmpty()
|
||||
public projectUuid: string;
|
||||
projectUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The array of space UUIDs (at least one required)',
|
||||
@ -88,7 +110,7 @@ export class AddUserInvitationDto {
|
||||
@IsArray()
|
||||
@IsUUID('4', { each: true })
|
||||
@ArrayMinSize(1)
|
||||
public spaceUuids: string[];
|
||||
spaceUuids: string[];
|
||||
constructor(dto: Partial<AddUserInvitationDto>) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { RoleType } from '@app/common/constants/role.type.enum';
|
||||
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
@ -23,6 +24,7 @@ import {
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { SpaceUserService } from 'src/space/services';
|
||||
import { TimerService } from 'src/timer/timer.service';
|
||||
import { UserSpaceService } from 'src/users/services';
|
||||
import {
|
||||
DataSource,
|
||||
@ -52,6 +54,7 @@ export class InviteUserService {
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
private readonly roleTypeRepository: RoleTypeRepository,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly timerService: TimerService,
|
||||
) {}
|
||||
|
||||
async createUserInvitation(
|
||||
@ -68,8 +71,14 @@ export class InviteUserService {
|
||||
roleUuid,
|
||||
spaceUuids,
|
||||
projectUuid,
|
||||
accessStartDate,
|
||||
accessEndDate,
|
||||
} = dto;
|
||||
|
||||
if (accessStartDate && accessEndDate && accessEndDate <= accessStartDate) {
|
||||
throw new BadRequestException(
|
||||
'accessEndDate must be after accessStartDate',
|
||||
);
|
||||
}
|
||||
const invitationCode = generateRandomString(6);
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
@ -114,6 +123,8 @@ export class InviteUserService {
|
||||
invitationCode,
|
||||
invitedBy: roleType,
|
||||
project: { uuid: projectUuid },
|
||||
accessEndDate,
|
||||
accessStartDate,
|
||||
});
|
||||
|
||||
const invitedUser = await queryRunner.manager.save(inviteUser);
|
||||
@ -132,12 +143,26 @@ export class InviteUserService {
|
||||
|
||||
// Send invitation email
|
||||
const spaceNames = validSpaces.map((space) => space.spaceName).join(', ');
|
||||
await this.emailService.sendEmailWithInvitationTemplate(email, {
|
||||
name: firstName,
|
||||
invitationCode,
|
||||
role: invitedRoleType.replace(/_/g, ' '),
|
||||
spacesList: spaceNames,
|
||||
});
|
||||
if (accessStartDate) {
|
||||
await this.timerService.createJob({
|
||||
type: TimerJobTypeEnum.INVITE_USER_EMAIL,
|
||||
triggerDate: accessStartDate,
|
||||
metadata: {
|
||||
email,
|
||||
name: firstName,
|
||||
invitationCode,
|
||||
role: invitedRoleType.replace(/_/g, ' '),
|
||||
spacesList: spaceNames,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.emailService.sendEmailWithInvitationTemplate(email, {
|
||||
name: firstName,
|
||||
invitationCode,
|
||||
role: invitedRoleType.replace(/_/g, ' '),
|
||||
spacesList: spaceNames,
|
||||
});
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
|
@ -17,6 +17,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { AddSpaceDto, CommunitySpaceParam, UpdateSpaceDto } from '../dtos';
|
||||
import { DuplicateSpaceDto } from '../dtos/duplicate-space.dto';
|
||||
import { GetSpaceDto } from '../dtos/get.space.dto';
|
||||
import { GetSpaceParam } from '../dtos/get.space.param';
|
||||
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
|
||||
@ -48,6 +49,26 @@ export class SpaceController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_ADD')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.SPACE.ACTIONS.DUPLICATE_SPACE_SUMMARY,
|
||||
description: ControllerRoute.SPACE.ACTIONS.DUPLICATE_SPACE_DESCRIPTION,
|
||||
})
|
||||
@Post(':spaceUuid/duplicate')
|
||||
async duplicateSpace(
|
||||
@Param('spaceUuid', ParseUUIDPipe) spaceUuid: string,
|
||||
@Body() dto: DuplicateSpaceDto,
|
||||
@Param() communitySpaceParam: CommunitySpaceParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return await this.spaceService.duplicateSpace(
|
||||
spaceUuid,
|
||||
communitySpaceParam,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_VIEW')
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
IsArray,
|
||||
IsMongoId,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
@ -48,14 +47,6 @@ export class AddSpaceDto {
|
||||
@IsOptional()
|
||||
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({
|
||||
description: 'UUID of the Space Model',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
|
18
src/space/dtos/duplicate-space.dto.ts
Normal file
18
src/space/dtos/duplicate-space.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, NotEquals } from 'class-validator';
|
||||
|
||||
export class DuplicateSpaceDto {
|
||||
@ApiProperty({
|
||||
description: 'Name of the space (e.g., Floor 1, Unit 101)',
|
||||
example: 'Unit 101',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@NotEquals(ORPHAN_SPACE_NAME, {
|
||||
message() {
|
||||
return `Space name cannot be "${ORPHAN_SPACE_NAME}". Please choose a different name.`;
|
||||
},
|
||||
})
|
||||
spaceName: string;
|
||||
}
|
@ -4,7 +4,6 @@ import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
NotEquals,
|
||||
@ -36,16 +35,6 @@ export class UpdateSpaceDto {
|
||||
@IsOptional()
|
||||
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({
|
||||
description: 'List of subspace modifications',
|
||||
type: [UpdateSubspaceDto],
|
||||
|
@ -133,7 +133,7 @@ export class ValidationService {
|
||||
'subspaces.productAllocations',
|
||||
'subspaces.productAllocations.product',
|
||||
'subspaces.devices',
|
||||
'spaceModel',
|
||||
// 'spaceModel',
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { generateRandomString } from '@app/common/helper/randomString';
|
||||
import { removeCircularReferences } from '@app/common/helper/removeCircularReferences';
|
||||
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||
import {
|
||||
InviteSpaceRepository,
|
||||
@ -24,6 +25,7 @@ import { DeviceService } from 'src/device/services';
|
||||
import { SpaceModelService } from 'src/space-model/services';
|
||||
import { TagService } from 'src/tags/services/tags.service';
|
||||
import { DataSource, In, Not, QueryRunner } from 'typeorm';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { DisableSpaceCommand } from '../commands';
|
||||
import {
|
||||
AddSpaceDto,
|
||||
@ -32,6 +34,7 @@ import {
|
||||
UpdateSpaceDto,
|
||||
} from '../dtos';
|
||||
import { CreateProductAllocationDto } from '../dtos/create-product-allocation.dto';
|
||||
import { DuplicateSpaceDto } from '../dtos/duplicate-space.dto';
|
||||
import { GetSpaceDto } from '../dtos/get.space.dto';
|
||||
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
|
||||
import { SpaceWithParentsDto } from '../dtos/space.parents.dto';
|
||||
@ -93,6 +96,9 @@ export class SpaceService {
|
||||
parentUuid && !isRecursiveCall
|
||||
? await this.validationService.validateSpace(parentUuid)
|
||||
: null;
|
||||
if (parent) {
|
||||
await this.validateNamingConflict(addSpaceDto.spaceName, parent);
|
||||
}
|
||||
|
||||
const spaceModel = spaceModelUuid
|
||||
? await this.validationService.validateSpaceModel(spaceModelUuid)
|
||||
@ -102,8 +108,6 @@ export class SpaceService {
|
||||
// todo: find a better way to handle this instead of naming every key
|
||||
spaceName: addSpaceDto.spaceName,
|
||||
icon: addSpaceDto.icon,
|
||||
x: addSpaceDto.x,
|
||||
y: addSpaceDto.y,
|
||||
spaceModel,
|
||||
parent: isRecursiveCall
|
||||
? recursiveCallParentEntity
|
||||
@ -181,6 +185,154 @@ export class SpaceService {
|
||||
!isRecursiveCall ? await queryRunner.release() : null;
|
||||
}
|
||||
}
|
||||
|
||||
async duplicateSpace(
|
||||
spaceUuid: string,
|
||||
{ communityUuid, projectUuid }: CommunitySpaceParam,
|
||||
dto: DuplicateSpaceDto,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<BaseResponseDto> {
|
||||
if (!queryRunner) {
|
||||
queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.validationService.validateCommunityAndProject(
|
||||
communityUuid,
|
||||
projectUuid,
|
||||
queryRunner,
|
||||
);
|
||||
await this.handleSpaceDuplication(spaceUuid, dto.spaceName, queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
const { data } = await this.getSpacesHierarchyForCommunity(
|
||||
{
|
||||
projectUuid,
|
||||
communityUuid,
|
||||
},
|
||||
{ onlyWithDevices: false },
|
||||
);
|
||||
return new SuccessResponseDto({
|
||||
message: `Space with ID ${spaceUuid} successfully duplicated`,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.log((error as Error).stack);
|
||||
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
private async handleSpaceDuplication(
|
||||
spaceUuid: string,
|
||||
newSpaceName: string | null,
|
||||
queryRunner: QueryRunner,
|
||||
parent?: SpaceEntity,
|
||||
) {
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceUuid },
|
||||
relations: [
|
||||
'children',
|
||||
'productAllocations',
|
||||
'subspaces',
|
||||
'subspaces.productAllocations',
|
||||
],
|
||||
});
|
||||
const clonedSpace = structuredClone(space);
|
||||
const newSpace = queryRunner.manager.create(SpaceEntity, {
|
||||
...clonedSpace,
|
||||
spaceName: newSpaceName || clonedSpace.spaceName,
|
||||
parent,
|
||||
children: undefined,
|
||||
subspaces: undefined,
|
||||
productAllocations: undefined,
|
||||
uuid: uuidV4(),
|
||||
});
|
||||
|
||||
if (clonedSpace.productAllocations?.length) {
|
||||
newSpace.productAllocations = this.copySpaceAllocations(
|
||||
newSpace,
|
||||
clonedSpace.productAllocations,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
if (clonedSpace.subspaces?.length) {
|
||||
newSpace.subspaces = this.copySpaceSubspaces(
|
||||
newSpace,
|
||||
clonedSpace.subspaces,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
const savedSpace = await queryRunner.manager.save(newSpace);
|
||||
|
||||
if (clonedSpace.children?.length) {
|
||||
for (const child of clonedSpace.children) {
|
||||
if (child.disabled) continue;
|
||||
await this.handleSpaceDuplication(
|
||||
child.uuid,
|
||||
child.spaceName,
|
||||
queryRunner,
|
||||
savedSpace,
|
||||
);
|
||||
}
|
||||
}
|
||||
return savedSpace;
|
||||
}
|
||||
private copySpaceSubspaces(
|
||||
newSpace: SpaceEntity,
|
||||
subspaces: SubspaceEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
const newSubspaces = [];
|
||||
for (const sub of subspaces) {
|
||||
if (sub.disabled) continue;
|
||||
const clonedSub = structuredClone(sub);
|
||||
delete clonedSub.uuid;
|
||||
const newSubspace = queryRunner.manager.create(SubspaceEntity, {
|
||||
...clonedSub,
|
||||
space: newSpace,
|
||||
productAllocations: [],
|
||||
uuid: uuidV4(),
|
||||
});
|
||||
if (sub.productAllocations?.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const { uuid, ...allocation } of sub.productAllocations) {
|
||||
newSubspace.productAllocations.push(
|
||||
queryRunner.manager.create(SubspaceProductAllocationEntity, {
|
||||
...allocation,
|
||||
subspace: newSubspace,
|
||||
uuid: uuidV4(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
newSubspaces.push(newSubspace);
|
||||
}
|
||||
return newSubspaces;
|
||||
}
|
||||
private copySpaceAllocations(
|
||||
newSpace: SpaceEntity,
|
||||
allocations: SpaceProductAllocationEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
const newAllocations = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const { uuid, ...allocation } of allocations) {
|
||||
newAllocations.push(
|
||||
queryRunner.manager.create(SpaceProductAllocationEntity, {
|
||||
...allocation,
|
||||
space: newSpace,
|
||||
uuid: uuidV4(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return newAllocations;
|
||||
}
|
||||
|
||||
private checkDuplicateTags(allocations: CreateProductAllocationDto[]) {
|
||||
const tagUuidSet = new Set<string>();
|
||||
const tagNameProductSet = new Set<string>();
|
||||
@ -505,6 +657,8 @@ export class SpaceService {
|
||||
spaceUuid,
|
||||
);
|
||||
|
||||
await this.validateNamingConflict(updateSpaceDto.spaceName, space, true);
|
||||
|
||||
if (space.spaceModel && !updateSpaceDto.spaceModelUuid) {
|
||||
await queryRunner.manager.update(SpaceEntity, space.uuid, {
|
||||
spaceModel: null,
|
||||
@ -655,13 +809,11 @@ export class SpaceService {
|
||||
updateSpaceDto: UpdateSpaceDto,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const { spaceName, x, y, icon } = updateSpaceDto;
|
||||
const { spaceName, icon } = updateSpaceDto;
|
||||
|
||||
const updateFields: Partial<SpaceEntity> = {};
|
||||
|
||||
if (spaceName) updateFields.spaceName = spaceName;
|
||||
if (x !== undefined) updateFields.x = x;
|
||||
if (y !== undefined) updateFields.y = y;
|
||||
if (icon) updateFields.icon = icon;
|
||||
|
||||
if (Object.keys(updateFields).length > 0) {
|
||||
@ -828,4 +980,34 @@ export class SpaceService {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
src/timer/create-job.dto.ts
Normal file
20
src/timer/create-job.dto.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||
|
||||
export class BaseCreateJobDto {
|
||||
type: TimerJobTypeEnum;
|
||||
triggerDate: Date;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
// validate based on jobType
|
||||
export class CreateUserInvitationJobDto extends BaseCreateJobDto {
|
||||
type: TimerJobTypeEnum.INVITE_USER_EMAIL;
|
||||
metadata: {
|
||||
email: string;
|
||||
name: string;
|
||||
invitationCode: string;
|
||||
role: string;
|
||||
spacesList: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateJobDto = CreateUserInvitationJobDto;
|
10
src/timer/timer.module.ts
Normal file
10
src/timer/timer.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { EmailService } from '@app/common/util/email/email.service';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TimerService } from './timer.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [TimerService, EmailService],
|
||||
exports: [TimerService],
|
||||
})
|
||||
export class TimerModule {}
|
53
src/timer/timer.service.ts
Normal file
53
src/timer/timer.service.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||
import { TimerEntity } from '@app/common/modules/timer/entities/timer.entity';
|
||||
import { TimerRepository } from '@app/common/modules/timer/repositories/timer.repository';
|
||||
import { EmailService } from '@app/common/util/email/email.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { In, LessThanOrEqual } from 'typeorm';
|
||||
import { CreateJobDto } from './create-job.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TimerService {
|
||||
constructor(
|
||||
private readonly timerRepository: TimerRepository,
|
||||
private readonly emailService: EmailService,
|
||||
) {}
|
||||
|
||||
createJob(job: CreateJobDto): Promise<TimerEntity> {
|
||||
const timerEntity = this.timerRepository.create(job);
|
||||
return this.timerRepository.save(timerEntity);
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||
async handleCron() {
|
||||
const jobsToRun = await this.timerRepository.find({
|
||||
where: { triggerDate: LessThanOrEqual(new Date()) },
|
||||
});
|
||||
const successfulJobs = [];
|
||||
for (const job of jobsToRun) {
|
||||
try {
|
||||
await this.handleJob(job);
|
||||
|
||||
successfulJobs.push(job.uuid);
|
||||
} catch (error) {
|
||||
console.error(`Job ${job.uuid} failed:`, error);
|
||||
}
|
||||
}
|
||||
await this.timerRepository.delete({ uuid: In(successfulJobs) });
|
||||
}
|
||||
|
||||
handleJob(job: TimerEntity) {
|
||||
switch (job.type) {
|
||||
case TimerJobTypeEnum.INVITE_USER_EMAIL:
|
||||
return this.emailService.sendEmailWithInvitationTemplate(
|
||||
job.metadata.email,
|
||||
job.metadata,
|
||||
);
|
||||
break;
|
||||
// Handle other job types as needed
|
||||
default:
|
||||
console.warn(`Unhandled job type: ${job.type}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ export class UserService {
|
||||
where: {
|
||||
uuid: userUuid,
|
||||
},
|
||||
relations: ['region', 'timezone', 'roleType', 'project'],
|
||||
relations: ['region', 'timezone', 'roleType', 'project', 'inviteUser'],
|
||||
});
|
||||
if (!user) {
|
||||
throw new BadRequestException('Invalid room UUID');
|
||||
@ -53,6 +53,10 @@ export class UserService {
|
||||
appAgreementAcceptedAt: user?.appAgreementAcceptedAt,
|
||||
role: user?.roleType,
|
||||
project: user?.project,
|
||||
bookingPoints: user?.bookingPoints ?? 0,
|
||||
accessStartDate: user?.inviteUser.accessStartDate,
|
||||
accessEndDate: user?.inviteUser.accessEndDate,
|
||||
bookingEnabled: user?.bookingEnabled,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) {
|
||||
|
Reference in New Issue
Block a user