Compare commits

...

4 Commits

Author SHA1 Message Date
5cf45c30f4 fix: check if device not found (#458) 2025-07-08 16:55:56 +03:00
0bb178ed10 make point nullable (#457) 2025-07-08 14:50:00 +03:00
9971fb953d SP-1812: Task/booking-system/update-api (#456)
* add update bookable spaces API

* add search to get bookable spaces API
2025-07-08 11:52:22 +03:00
7a07f39f16 add communities filter to devices by project API (#455) 2025-07-08 11:25:15 +03:00
10 changed files with 132 additions and 18 deletions

View File

@ -83,6 +83,12 @@ export class ControllerRoute {
public static readonly GET_ALL_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint retrieves all bookable spaces.';
public static readonly UPDATE_BOOKABLE_SPACES_SUMMARY =
'Update existing bookable spaces';
public static readonly UPDATE_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to update existing bookable spaces by providing the required details.';
};
};
static COMMUNITY = class {

View File

@ -40,8 +40,8 @@ export class BookableSpaceEntity extends AbstractEntity {
@Column({ type: Boolean, default: true })
active: boolean;
@Column({ type: 'int' })
points: number;
@Column({ type: 'int', default: null })
points?: number;
@CreateDateColumn()
createdAt: Date;

View File

@ -6,7 +6,9 @@ import {
Body,
Controller,
Get,
Param,
Post,
Put,
Query,
Req,
UseGuards,
@ -19,6 +21,7 @@ import { plainToInstance } from 'class-transformer';
import { CreateBookableSpaceDto } from '../dtos';
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.dto';
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
import { BookableSpaceService } from '../services';
@ApiTags('Booking Module')
@ -79,4 +82,25 @@ export class BookableSpaceController {
pagination,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put(':spaceUuid')
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.UPDATE_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS
.UPDATE_BOOKABLE_SPACES_DESCRIPTION,
})
async update(
@Param('spaceUuid') spaceUuid: string,
@Body() dto: UpdateBookableSpaceDto,
): Promise<BaseResponseDto> {
const result = await this.bookableSpaceService.update(spaceUuid, dto);
return new SuccessResponseDto({
data: result,
message: 'Successfully updated bookable spaces',
});
}
}

View File

@ -1,11 +1,11 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto';
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
export class BookableSpaceRequestDto extends OmitType(
PaginationRequestGetListDto,
PaginationRequestWithSearchGetListDto,
['includeSpaces'],
) {
@ApiProperty({

View File

@ -27,9 +27,10 @@ export class BookableSpaceConfigResponseDto {
@ApiProperty({
type: Number,
nullable: true,
})
@Expose()
points: number;
points?: number;
}
export class BookableSpaceResponseDto {

View File

@ -1,17 +1,17 @@
// dtos/bookable-space.dto.ts
import { DaysEnum } from '@app/common/constants/days.enum';
import { ApiProperty } from '@nestjs/swagger';
import {
ArrayMinSize,
IsArray,
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
IsInt,
ArrayMinSize,
Matches,
Max,
Min,
Matches,
} from 'class-validator';
export class CreateBookableSpaceDto {
@ -54,9 +54,10 @@ export class CreateBookableSpaceDto {
})
endTime: string;
@ApiProperty({ example: 10 })
@ApiProperty({ example: 10, required: false })
@IsOptional()
@IsInt()
@Min(0, { message: 'Points cannot be negative' })
@Max(1000, { message: 'Points cannot exceed 1000' })
points: number;
points?: number;
}

View File

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

View File

@ -14,6 +14,7 @@ import {
import { In } from 'typeorm';
import { CreateBookableSpaceDto } from '../dtos';
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
@Injectable()
export class BookableSpaceService {
@ -37,7 +38,7 @@ export class BookableSpaceService {
}
async findAll(
{ active, page, size, configured }: BookableSpaceRequestDto,
{ active, page, size, configured, search }: BookableSpaceRequestDto,
project: string,
): Promise<{
data: BaseResponseDto['data'];
@ -49,6 +50,12 @@ export class BookableSpaceService {
.leftJoinAndSelect('space.community', 'community')
.where('community.project = :project', { project });
if (search) {
qb = qb.andWhere(
'space.spaceName ILIKE :search OR community.name ILIKE :search OR parentSpace.spaceName ILIKE :search',
{ search: `%${search}%` },
);
}
if (configured) {
qb = qb
.leftJoinAndSelect('space.bookableConfig', 'bookableConfig')
@ -77,6 +84,30 @@ export class BookableSpaceService {
};
}
/**
* 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) {
// fetch spaces exist
const space = (await this.getSpacesOrFindMissing([spaceUuid]))[0];
if (!space.bookableConfig) {
throw new NotFoundException(
`Bookable configuration not found for space: ${spaceUuid}`,
);
}
if (dto.startTime || dto.endTime) {
// Validate time slots first
this.validateTimeSlot(
dto.startTime || space.bookableConfig.startTime,
dto.endTime || space.bookableConfig.endTime,
);
}
Object.assign(space.bookableConfig, dto);
return this.bookableSpaceEntityRepository.save(space.bookableConfig);
}
/**
* Fetch spaces by UUIDs and throw an error if any are missing
*/
@ -85,6 +116,7 @@ export class BookableSpaceService {
): Promise<SpaceEntity[]> {
const spaces = await this.spaceRepository.find({
where: { uuid: In(spaceUuids) },
relations: ['bookableConfig'],
});
if (spaces.length !== spaceUuids.length) {

View File

@ -1,7 +1,7 @@
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
@ -74,13 +74,34 @@ export class GetDevicesFilterDto {
@IsEnum(DeviceTypeEnum)
@IsOptional()
public deviceType: DeviceTypeEnum;
@ApiProperty({
description: 'List of Space IDs to filter devices',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@IsOptional()
@IsArray()
@Transform(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
return value;
})
@IsUUID('4', { each: true })
public spaces?: string[];
@ApiProperty({
description: 'List of Community IDs to filter devices',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@Transform(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
return value;
})
@IsOptional()
@IsUUID('4', { each: true })
public communities?: string[];
}

View File

@ -100,12 +100,15 @@ export class DeviceService {
async getAllDevices(
param: ProjectParam,
{ deviceType, spaces }: GetDevicesFilterDto,
{ deviceType, spaces, communities }: GetDevicesFilterDto,
): Promise<BaseResponseDto> {
try {
await this.validateProject(param.projectUuid);
if (deviceType === DeviceTypeEnum.DOOR_LOCK) {
return await this.getDoorLockDevices(param.projectUuid, spaces);
return await this.getDoorLockDevices(param.projectUuid, {
spaces,
communities,
});
} else if (!deviceType) {
const devices = await this.deviceRepository.find({
where: {
@ -113,7 +116,13 @@ export class DeviceService {
spaceDevice: {
uuid: spaces && spaces.length ? In(spaces) : undefined,
spaceName: Not(ORPHAN_SPACE_NAME),
community: { project: { uuid: param.projectUuid } },
community: {
uuid:
communities && communities.length
? In(communities)
: undefined,
project: { uuid: param.projectUuid },
},
},
},
relations: [
@ -708,6 +717,9 @@ export class DeviceService {
relations: ['productDevice'],
});
if (!deviceDetails) {
throw new NotFoundException('Device not found');
}
let result = await this.tuyaService.getDeviceDetails(deviceId);
if (!result) {
@ -1247,7 +1259,10 @@ export class DeviceService {
await this.deviceRepository.save(updatedDevices);
}
private async getDoorLockDevices(projectUuid: string, spaces?: string[]) {
private async getDoorLockDevices(
projectUuid: string,
{ communities, spaces }: { spaces?: string[]; communities?: string[] },
) {
await this.validateProject(projectUuid);
const devices = await this.deviceRepository.find({
@ -1259,6 +1274,8 @@ export class DeviceService {
spaceName: Not(ORPHAN_SPACE_NAME),
uuid: spaces && spaces.length ? In(spaces) : undefined,
community: {
uuid:
communities && communities.length ? In(communities) : undefined,
project: {
uuid: projectUuid,
},