Compare commits

..

5 Commits

40 changed files with 989 additions and 1379 deletions

View File

@ -1,64 +0,0 @@
name: 🤖 AI PR Description Commenter (100% Safe with jq)
on:
pull_request:
types: [opened, edited]
jobs:
generate-description:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Install GitHub CLI and jq
run: |
sudo apt-get update
sudo apt-get install gh jq -y
- name: Fetch PR Commits
id: fetch_commits
run: |
COMMITS=$(gh pr view ${{ github.event.pull_request.number }} --json commits --jq '.commits[].message' | sed 's/^/- /')
echo "commits<<EOF" >> $GITHUB_ENV
echo "$COMMITS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
- name: Generate PR Description with OpenAI (Safe JSON with jq)
run: |
REQUEST_BODY=$(jq -n \
--arg model "gpt-4o" \
--arg content "Given the following commit messages:\n\n${commits}\n\nGenerate a clear and professional pull request description." \
'{
model: $model,
messages: [{ role: "user", content: $content }]
}'
)
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d "$REQUEST_BODY")
DESCRIPTION=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
echo "---------- OpenAI Raw Response ----------"
echo "$RESPONSE"
echo "---------- Extracted Description ----------"
echo "$DESCRIPTION"
echo "description<<EOF" >> $GITHUB_ENV
echo "$DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
commits: ${{ env.commits }}
- name: Post AI Generated Description as Comment
run: |
gh pr comment ${{ github.event.pull_request.number }} --body "${{ env.description }}"
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}

View File

@ -188,11 +188,6 @@ export class ControllerRoute {
static SCENE = class {
public static readonly ROUTE = 'scene';
static ACTIONS = class {
public static readonly GET_TAP_TO_RUN_SCENES_SUMMARY =
'Get Tap-to-Run Scenes by spaces';
public static readonly GET_TAP_TO_RUN_SCENES_DESCRIPTION =
'Gets Tap-to-Run scenes by spaces';
public static readonly CREATE_TAP_TO_RUN_SCENE_SUMMARY =
'Create a Tap-to-Run Scene';
public static readonly CREATE_TAP_TO_RUN_SCENE_DESCRIPTION =
@ -228,10 +223,6 @@ 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';
@ -424,10 +415,6 @@ export class ControllerRoute {
public static readonly ROUTE = '/user';
static ACTIONS = class {
public static readonly GET_USERS_WITH_BOOKABLE_SPACES_SUMMARY =
'Retrieve list of users that has bookable spaces';
public static readonly GET_USERS_WITH_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint retrieves all the users that have access to bookable spaces, paginated & accepts sorting';
public static readonly GET_USER_DETAILS_SUMMARY =
'Retrieve user details by user UUID';
public static readonly GET_USER_DETAILS_DESCRIPTION =
@ -781,10 +768,6 @@ export class ControllerRoute {
public static readonly ADD_AUTOMATION_DESCRIPTION =
'This endpoint creates a new automation based on the provided details.';
public static readonly GET_AUTOMATION_SUMMARY = 'Get all automations';
public static readonly GET_AUTOMATION_DESCRIPTION =
'This endpoint retrieves automations data';
public static readonly GET_AUTOMATION_DETAILS_SUMMARY =
'Get automation details';
public static readonly GET_AUTOMATION_DETAILS_DESCRIPTION =

View File

@ -1,13 +1,15 @@
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { firebaseDataBase } from '../../firebase.config';
import {
Database,
DataSnapshot,
@ -15,9 +17,7 @@ import {
ref,
runTransaction,
} from 'firebase/database';
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
import { firebaseDataBase } from '../../firebase.config';
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
@Injectable()
export class DeviceStatusFirebaseService {
private tuya: TuyaContext;
@ -79,77 +79,64 @@ export class DeviceStatusFirebaseService {
device: any;
}[],
): Promise<void> {
console.log(`🔁 Preparing logs from batch of ${batch.length} items...`);
const allLogs = [];
console.log(`🔁 Preparing logs from batch of ${batch.length} items...`);
for (const item of batch) {
const device = item.device;
if (!device?.uuid) {
console.log(`⛔ Skipped unknown device: ${item.deviceTuyaUuid}`);
continue;
}
// Determine properties based on environment
const properties =
this.isDevEnv && Array.isArray(item.log?.properties)
? item.log.properties
: Array.isArray(item.status)
? item.status
: null;
if (!properties) {
console.log(
`⛔ Skipped invalid status/properties for device: ${item.deviceTuyaUuid}`,
);
continue;
}
const logs = properties.map((property) =>
const logs = item.log.properties.map((property) =>
this.deviceStatusLogRepository.create({
deviceId: device.uuid,
deviceTuyaId: item.deviceTuyaUuid,
productId: device.productDevice?.uuid,
productId: item.log.productId,
log: item.log,
code: property.code,
value: property.value,
eventId: item.log?.dataId,
eventTime: new Date(
this.isDevEnv ? property.time : property.t,
).toISOString(),
eventId: item.log.dataId,
eventTime: new Date(property.time).toISOString(),
}),
);
allLogs.push(...logs);
}
console.log(`📝 Total logs to insert: ${allLogs.length}`);
const chunkSize = 300;
let insertedCount = 0;
const insertLogsPromise = (async () => {
const chunkSize = 300;
let insertedCount = 0;
for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize);
try {
const result = await this.deviceStatusLogRepository
.createQueryBuilder()
.insert()
.into('device-status-log')
.values(chunk)
.orIgnore()
.execute();
for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize);
try {
const result = await this.deviceStatusLogRepository
.createQueryBuilder()
.insert()
.into('device-status-log') // or use DeviceStatusLogEntity
.values(chunk)
.orIgnore() // skip duplicates
.execute();
insertedCount += result.identifiers.length;
console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
);
} catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message);
insertedCount += result.identifiers.length;
console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
);
} catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message);
}
}
}
console.log(`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`);
console.log(
`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`,
);
})();
await insertLogsPromise;
}
async addDeviceStatusToFirebase(

View File

@ -1,9 +1,9 @@
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as NodeCache from 'node-cache';
import TuyaWebsocket from '../../config/tuya-web-socket-config';
import { ConfigService } from '@nestjs/config';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SosHandlerService } from './sos.handler.service';
import * as NodeCache from 'node-cache';
@Injectable()
export class TuyaWebSocketService implements OnModuleInit {
@ -74,12 +74,7 @@ export class TuyaWebSocketService implements OnModuleInit {
this.client.message(async (ws: WebSocket, message: any) => {
try {
const { devId, status, logData } = this.extractMessageData(message);
// console.log(
// `📬 Received message for device: ${devId}, status:`,
// status,
// logData,
// );
if (!Array.isArray(status)) {
if (!Array.isArray(logData?.properties)) {
this.client.ackMessage(message.messageId);
return;
}
@ -167,8 +162,6 @@ export class TuyaWebSocketService implements OnModuleInit {
status: any;
logData: any;
} {
// console.log('Received message:', message);
const payloadData = message.payload.data;
if (this.isDevEnv) {

View File

@ -22,11 +22,6 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
})
public space: SpaceEntity;
@Column({
name: 'space_uuid',
})
public spaceUuid: string;
@ManyToOne(() => SpaceModelProductAllocationEntity, {
nullable: true,
onDelete: 'SET NULL',
@ -36,19 +31,9 @@ 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);

View File

@ -9,7 +9,6 @@ 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';
@ -21,6 +20,7 @@ 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,26 +47,12 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
@JoinColumn({ name: 'community_id' })
community: CommunityEntity;
@Column({
name: 'community_id',
})
communityId: string;
@ManyToOne(() => SpaceEntity, (space) => space.children, {
nullable: true,
})
@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[];
@ -87,10 +73,16 @@ 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,

View File

@ -22,11 +22,6 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
})
public subspace: SubspaceEntity;
@Column({
name: 'subspace_uuid',
})
public subspaceUuid: string;
@ManyToOne(() => SubspaceModelProductAllocationEntity, {
nullable: true,
onDelete: 'SET NULL',
@ -36,19 +31,9 @@ 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);

View File

@ -26,11 +26,6 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
name: 'space_uuid',
})
public spaceUuid: string;
@Column({
nullable: false,
default: false,

View File

@ -44,7 +44,6 @@ export class UserEntity extends AbstractEntity<UserDto> {
nullable: true,
type: 'text',
default: defaultProfilePicture,
select: false,
})
public profilePicture: string;

View File

@ -0,0 +1,91 @@
WITH total_energy AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumed'
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedA'
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedB'
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedC'
GROUP BY 1,2,3,4,5
)
, final_data as (
SELECT
t.device_id,
t.date,
t.event_year::text,
t.event_month,
t.hour,
(t.max_value - t.min_value) AS energy_consumed_kW,
(a.max_value - a.min_value) AS energy_consumed_A,
(b.max_value - b.min_value) AS energy_consumed_B,
(c.max_value - c.min_value) AS energy_consumed_C
FROM total_energy t
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
ORDER BY 1,2)
INSERT INTO public."power-clamp-energy-consumed-daily"(
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
date
)
SELECT
device_id,
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
date
FROM final_data
GROUP BY device_id, date;

View File

@ -0,0 +1,94 @@
WITH total_energy AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumed'
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedA'
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedB'
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedC'
GROUP BY 1,2,3,4,5
)
, final_data as (
SELECT
t.device_id,
t.date,
t.event_year::text,
t.event_month,
t.hour,
(t.max_value - t.min_value) AS energy_consumed_kW,
(a.max_value - a.min_value) AS energy_consumed_A,
(b.max_value - b.min_value) AS energy_consumed_B,
(c.max_value - c.min_value) AS energy_consumed_C
FROM total_energy t
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
ORDER BY 1,2)
INSERT INTO public."power-clamp-energy-consumed-hourly"(
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
date,
hour
)
SELECT
device_id,
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
date,
hour
FROM final_data
GROUP BY 1,6,7

View File

@ -5,8 +5,8 @@ WITH total_energy AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumed'
GROUP BY 1,2,3,4,5
@ -19,8 +19,8 @@ energy_phase_A AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedA'
GROUP BY 1,2,3,4,5
@ -33,8 +33,8 @@ energy_phase_B AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedB'
GROUP BY 1,2,3,4,5
@ -47,8 +47,8 @@ energy_phase_C AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log
WHERE log.code = 'EnergyConsumedC'
GROUP BY 1,2,3,4,5
@ -71,49 +71,6 @@ JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hou
ORDER BY 1,2)
INSERT INTO public."power-clamp-energy-consumed-daily"(
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
date
)
SELECT
device_id,
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
date
FROM final_data
GROUP BY device_id, date;
INSERT INTO public."power-clamp-energy-consumed-hourly"(
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
date,
hour
)
SELECT
device_id,
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
date,
hour
FROM final_data
GROUP BY 1,6,7
INSERT INTO public."power-clamp-energy-consumed-monthly"(
device_uuid,
energy_consumed_kw,
@ -131,5 +88,4 @@ SELECT
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
TO_CHAR(date, 'MM-YYYY')
FROM final_data
GROUP BY 1,6;
GROUP BY 1,6;

View File

@ -9,8 +9,8 @@ total_energy AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.event_time::date = params.target_date
@ -23,8 +23,8 @@ energy_phase_A AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.event_time::date = params.target_date
@ -37,8 +37,8 @@ energy_phase_B AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.event_time::date = params.target_date
@ -51,8 +51,8 @@ energy_phase_C AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.event_time::date = params.target_date

View File

@ -9,8 +9,8 @@ total_energy AS (
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.event_time::date = params.target_date
@ -23,8 +23,8 @@ energy_phase_A AS (
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.event_time::date = params.target_date
@ -37,8 +37,8 @@ energy_phase_B AS (
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.event_time::date = params.target_date
@ -51,8 +51,8 @@ energy_phase_C AS (
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.event_time::date = params.target_date

View File

@ -9,8 +9,8 @@ total_energy AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
@ -23,8 +23,8 @@ energy_phase_A AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
@ -37,8 +37,8 @@ energy_phase_B AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
@ -51,8 +51,8 @@ energy_phase_C AS (
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
MIN(log.value)::integer/100 AS min_value,
MAX(log.value)::integer/100 AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month

View File

@ -4,8 +4,8 @@ WITH total_energy AS (
SELECT
device_id,
event_time::date AS date,
MIN(value)::integer AS min_value,
MAX(value)::integer AS max_value
MIN(value)::integer/100 AS min_value,
MAX(value)::integer/100 AS max_value
FROM "device-status-log"
where code='EnergyConsumed'
GROUP BY device_id, date
@ -15,8 +15,8 @@ WITH total_energy AS (
SELECT
device_id,
event_time::date AS date,
MIN(value)::integer AS min_value,
MAX(value)::integer AS max_value
MIN(value)::integer/100 AS min_value,
MAX(value)::integer/100 AS max_value
FROM "device-status-log"
where code='EnergyConsumedA'
GROUP BY device_id, date
@ -26,8 +26,8 @@ WITH total_energy AS (
SELECT
device_id,
event_time::date AS date,
MIN(value)::integer AS min_value,
MAX(value)::integer AS max_value
MIN(value)::integer/100 AS min_value,
MAX(value)::integer/100 AS max_value
FROM "device-status-log"
where code='EnergyConsumedB'
GROUP BY device_id, date
@ -37,8 +37,8 @@ WITH total_energy AS (
SELECT
device_id,
event_time::date AS date,
MIN(value)::integer AS min_value,
MAX(value)::integer AS max_value
MIN(value)::integer/100 AS min_value,
MAX(value)::integer/100 AS max_value
FROM "device-status-log"
where code='EnergyConsumedC'
GROUP BY device_id, date

View File

@ -1,7 +1,7 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import * as nodemailer from 'nodemailer';
import nodemailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer';
import { BatchEmailData } from './batch-email.interface';
import { SingleEmailData } from './single-email.interface';

31
package-lock.json generated
View File

@ -48,7 +48,6 @@
"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"
},
@ -2366,18 +2365,6 @@
"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",
@ -12762,6 +12749,18 @@
"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",
@ -12897,9 +12896,9 @@
}
},
"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==",
"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"

View File

@ -60,7 +60,6 @@
"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"
},

View File

@ -1,7 +1,4 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { AutomationService } from '../services/automation.service';
import {
Body,
Controller,
@ -12,20 +9,20 @@ import {
Patch,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { AutomationParamDto } from '../dtos';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import {
AddAutomationDto,
UpdateAutomationDto,
UpdateAutomationStatusDto,
} from '../dtos/automation.dto';
import { GetAutomationBySpacesDto } from '../dtos/get-automation-by-spaces.dto';
import { AutomationService } from '../services/automation.service';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { AutomationParamDto } from '../dtos';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
import { ProjectParam } from '@app/common/dto/project-param.dto';
@ApiTags('Automation Module')
@Controller({
@ -59,28 +56,6 @@ export class AutomationController {
};
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('AUTOMATION_VIEW')
@Get('')
@ApiOperation({
summary: ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_SUMMARY,
description: ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_DESCRIPTION,
})
async getAutomationBySpaces(
@Param() param: ProjectParam,
@Query() spaces: GetAutomationBySpacesDto,
) {
const automation = await this.automationService.getAutomationBySpaces(
spaces,
param.projectUuid,
);
return new SuccessResponseDto({
message: 'Automation retrieved Successfully',
data: automation,
});
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('AUTOMATION_VIEW')

View File

@ -1,20 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsOptional, IsUUID } from 'class-validator';
export class GetAutomationBySpacesDto {
@ApiProperty({
description: 'List of Space IDs to filter automation',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@IsOptional()
@Transform(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
return value;
})
@IsUUID('4', { each: true })
public spaces?: string[];
}

View File

@ -1,42 +1,20 @@
import {
ActionExecutorEnum,
ActionTypeEnum,
AUTO_PREFIX,
AUTOMATION_TYPE,
EntityTypeEnum,
} from '@app/common/constants/automation.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { GetSpaceParam } from '@app/common/dto/get.space.param';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { AutomationEntity } from '@app/common/modules/automation/entities';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { SceneRepository } from '@app/common/modules/scene/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
BadRequestException,
Injectable,
HttpException,
HttpStatus,
Injectable,
BadRequestException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { DeviceService } from 'src/device/services';
import { DeleteTapToRunSceneInterface } from 'src/scene/interface/scene.interface';
import { In } from 'typeorm';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
AddAutomationDto,
AutomationParamDto,
UpdateAutomationDto,
UpdateAutomationStatusDto,
} from '../dtos';
import { GetAutomationBySpacesDto } from '../dtos/get-automation-by-spaces.dto';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services';
import {
Action,
AddAutomationParams,
@ -44,6 +22,26 @@ import {
AutomationResponseData,
Condition,
} from '../interface/automation.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import {
ActionExecutorEnum,
ActionTypeEnum,
AUTO_PREFIX,
AUTOMATION_TYPE,
EntityTypeEnum,
} from '@app/common/constants/automation.enum';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { SceneRepository } from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { AutomationEntity } from '@app/common/modules/automation/entities';
import { DeleteTapToRunSceneInterface } from 'src/scene/interface/scene.interface';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { GetSpaceParam } from '@app/common/dto/get.space.param';
@Injectable()
export class AutomationService {
@ -114,25 +112,128 @@ export class AutomationService {
);
}
}
async getAutomationBySpace({ projectUuid, spaceUuid }: GetSpaceParam) {
return this.getAutomationBySpaces({ spaces: [spaceUuid] }, projectUuid);
}
async getAutomationBySpaces(
{ spaces }: GetAutomationBySpacesDto,
async createAutomationExternalService(
params: AddAutomationParams,
projectUuid: string,
) {
try {
await this.validateProject(projectUuid);
const formattedActions = await this.prepareActions(
params.actions,
projectUuid,
);
const formattedCondition = await this.prepareConditions(
params.conditions,
projectUuid,
);
const response = await this.tuyaService.createAutomation(
params.spaceTuyaId,
params.automationName,
params.effectiveTime,
params.decisionExpr,
formattedCondition,
formattedActions,
);
if (!response.result?.id) {
throw new HttpException(
'Failed to create automation in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return response;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create automation',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
`An Internal error has been occured ${err}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async add(params: AddAutomationParams, projectUuid: string) {
try {
const response = await this.createAutomationExternalService(
params,
projectUuid,
);
const automation = await this.automationRepository.save({
automationTuyaUuid: response.result.id,
space: { uuid: params.spaceUuid },
});
return automation;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create automation',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
'Database error: Failed to save automation',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getSpaceByUuid(spaceUuid: string, projectUuid: string) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
community: {
project: {
uuid: projectUuid,
},
},
},
relations: ['community'],
});
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
}
}
async getAutomationBySpace(param: GetSpaceParam) {
try {
await this.validateProject(param.projectUuid);
// Fetch automation data from the repository
const automationData = await this.automationRepository.find({
where: {
space: {
uuid: In(spaces ?? []),
uuid: param.spaceUuid,
community: {
uuid: param.communityUuid,
project: {
uuid: projectUuid,
uuid: param.projectUuid,
},
},
},
@ -189,277 +290,46 @@ export class AutomationService {
}
}
async getAutomationDetails(param: AutomationParamDto) {
await this.validateProject(param.projectUuid);
async findAutomationBySpace(spaceUuid: string, projectUuid: string) {
try {
const automation = await this.findAutomationByUuid(
param.automationUuid,
param.projectUuid,
);
await this.getSpaceByUuid(spaceUuid, projectUuid);
const automationDetails = await this.getAutomation(automation);
return automationDetails;
} catch (error) {
console.error(
`Error fetching automation details for automationUuid ${param.automationUuid}:`,
error,
);
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException(
'An error occurred while retrieving automation details',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async updateAutomation(
updateAutomationDto: UpdateAutomationDto,
param: AutomationParamDto,
) {
await this.validateProject(param.projectUuid);
try {
const automation = await this.findAutomationByUuid(
param.automationUuid,
param.projectUuid,
);
const space = await this.getSpaceByUuid(
automation.space.uuid,
param.projectUuid,
);
const updateTuyaAutomationResponse =
await this.updateAutomationExternalService(
space.spaceTuyaUuid,
automation.automationTuyaUuid,
updateAutomationDto,
param.projectUuid,
);
if (!updateTuyaAutomationResponse.success) {
throw new HttpException(
`Failed to update a external automation`,
HttpStatus.BAD_GATEWAY,
);
}
const updatedScene = await this.automationRepository.update(
{ uuid: param.automationUuid },
{
space: { uuid: automation.space.uuid },
},
);
return new SuccessResponseDto({
data: updatedScene,
message: `Automation with ID ${param.automationUuid} updated successfully`,
});
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async updateAutomationStatus(
updateAutomationStatusDto: UpdateAutomationStatusDto,
param: AutomationParamDto,
) {
const { isEnable, spaceUuid } = updateAutomationStatusDto;
await this.validateProject(param.projectUuid);
try {
const automation = await this.findAutomationByUuid(
param.automationUuid,
param.projectUuid,
);
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
if (!space.spaceTuyaUuid) {
throw new HttpException(
`Invalid space UUID ${spaceUuid}`,
HttpStatus.NOT_FOUND,
);
}
const response = await this.tuyaService.updateAutomationState(
space.spaceTuyaUuid,
automation.automationTuyaUuid,
isEnable,
);
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async deleteAutomation(param: AutomationParamDto) {
const { automationUuid } = param;
await this.validateProject(param.projectUuid);
try {
const automationData = await this.findAutomationByUuid(
automationUuid,
param.projectUuid,
);
const space = await this.getSpaceByUuid(
automationData.space.uuid,
param.projectUuid,
);
await this.delete(automationData.automationTuyaUuid, space.spaceTuyaUuid);
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
where: { automationTuyaUuid: automationData.automationTuyaUuid },
});
if (existingSceneDevice) {
await this.sceneDeviceRepository.delete({
automationTuyaUuid: automationData.automationTuyaUuid,
});
}
await this.automationRepository.update(
{
uuid: automationUuid,
},
{ disabled: true },
);
return new SuccessResponseDto({
message: `Automation with ID ${automationUuid} deleted successfully`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
err.message || `Automation not found for id ${param.automationUuid}`,
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
private async createAutomationExternalService(
params: AddAutomationParams,
projectUuid: string,
) {
try {
const formattedActions = await this.prepareActions(
params.actions,
projectUuid,
);
const formattedCondition = await this.prepareConditions(
params.conditions,
projectUuid,
);
const response = await this.tuyaService.createAutomation(
params.spaceTuyaId,
params.automationName,
params.effectiveTime,
params.decisionExpr,
formattedCondition,
formattedActions,
);
if (!response.result?.id) {
throw new HttpException(
'Failed to create automation in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return response;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create automation',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
`An Internal error has been occured ${err}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async add(params: AddAutomationParams, projectUuid: string) {
try {
const response = await this.createAutomationExternalService(
params,
projectUuid,
);
const automation = await this.automationRepository.save({
automationTuyaUuid: response.result.id,
space: { uuid: params.spaceUuid },
});
return automation;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create automation',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
'Database error: Failed to save automation',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async getSpaceByUuid(spaceUuid: string, projectUuid: string) {
try {
const space = await this.spaceRepository.findOne({
const automationData = await this.automationRepository.find({
where: {
uuid: spaceUuid,
community: {
project: {
uuid: projectUuid,
},
},
space: { uuid: spaceUuid },
disabled: false,
},
relations: ['community'],
relations: ['space'],
});
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
};
const automations = await Promise.all(
automationData.map(async (automation) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { actions, ...automationDetails } =
await this.getAutomation(automation);
return automationDetails;
}),
);
return automations;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
console.error(
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
err.message,
);
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
throw new HttpException(
'An error occurred while retrieving scenes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async getTapToRunSceneDetailsTuya(
async getTapToRunSceneDetailsTuya(
sceneUuid: string,
): Promise<AutomationDetailsResult> {
try {
@ -491,8 +361,35 @@ export class AutomationService {
}
}
}
async getAutomationDetails(param: AutomationParamDto) {
await this.validateProject(param.projectUuid);
private async getAutomation(automation: AutomationEntity) {
try {
const automation = await this.findAutomation(
param.automationUuid,
param.projectUuid,
);
const automationDetails = await this.getAutomation(automation);
return automationDetails;
} catch (error) {
console.error(
`Error fetching automation details for automationUuid ${param.automationUuid}:`,
error,
);
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException(
'An error occurred while retrieving automation details',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getAutomation(automation: AutomationEntity) {
try {
const response = await this.tuyaService.getSceneRule(
automation.automationTuyaUuid,
@ -599,13 +496,13 @@ export class AutomationService {
}
}
}
private async findAutomationByUuid(
uuid: string,
async findAutomation(
sceneUuid: string,
projectUuid: string,
): Promise<AutomationEntity> {
const automation = await this.automationRepository.findOne({
where: {
uuid: uuid,
uuid: sceneUuid,
space: { community: { project: { uuid: projectUuid } } },
},
relations: ['space'],
@ -613,14 +510,57 @@ export class AutomationService {
if (!automation) {
throw new HttpException(
`Invalid automation with id ${uuid}`,
`Invalid automation with id ${sceneUuid}`,
HttpStatus.NOT_FOUND,
);
}
return automation;
}
private async delete(tuyaAutomationId: string, tuyaSpaceId: string) {
async deleteAutomation(param: AutomationParamDto) {
const { automationUuid } = param;
await this.validateProject(param.projectUuid);
try {
const automationData = await this.findAutomation(
automationUuid,
param.projectUuid,
);
const space = await this.getSpaceByUuid(
automationData.space.uuid,
param.projectUuid,
);
await this.delete(automationData.automationTuyaUuid, space.spaceTuyaUuid);
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
where: { automationTuyaUuid: automationData.automationTuyaUuid },
});
if (existingSceneDevice) {
await this.sceneDeviceRepository.delete({
automationTuyaUuid: automationData.automationTuyaUuid,
});
}
await this.automationRepository.update(
{
uuid: automationUuid,
},
{ disabled: true },
);
return new SuccessResponseDto({
message: `Automation with ID ${automationUuid} deleted successfully`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
err.message || `Automation not found for id ${param.automationUuid}`,
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async delete(tuyaAutomationId: string, tuyaSpaceId: string) {
try {
const response = (await this.tuyaService.deleteSceneRule(
tuyaAutomationId,
@ -638,7 +578,7 @@ export class AutomationService {
}
}
}
private async updateAutomationExternalService(
async updateAutomationExternalService(
spaceTuyaUuid: string,
automationUuid: string,
updateAutomationDto: UpdateAutomationDto,
@ -686,6 +626,95 @@ export class AutomationService {
}
}
}
async updateAutomation(
updateAutomationDto: UpdateAutomationDto,
param: AutomationParamDto,
) {
await this.validateProject(param.projectUuid);
try {
const automation = await this.findAutomation(
param.automationUuid,
param.projectUuid,
);
const space = await this.getSpaceByUuid(
automation.space.uuid,
param.projectUuid,
);
const updateTuyaAutomationResponse =
await this.updateAutomationExternalService(
space.spaceTuyaUuid,
automation.automationTuyaUuid,
updateAutomationDto,
param.projectUuid,
);
if (!updateTuyaAutomationResponse.success) {
throw new HttpException(
`Failed to update a external automation`,
HttpStatus.BAD_GATEWAY,
);
}
const updatedScene = await this.automationRepository.update(
{ uuid: param.automationUuid },
{
space: { uuid: automation.space.uuid },
},
);
return new SuccessResponseDto({
data: updatedScene,
message: `Automation with ID ${param.automationUuid} updated successfully`,
});
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async updateAutomationStatus(
updateAutomationStatusDto: UpdateAutomationStatusDto,
param: AutomationParamDto,
) {
const { isEnable, spaceUuid } = updateAutomationStatusDto;
await this.validateProject(param.projectUuid);
try {
const automation = await this.findAutomation(
param.automationUuid,
param.projectUuid,
);
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
if (!space.spaceTuyaUuid) {
throw new HttpException(
`Invalid space UUID ${spaceUuid}`,
HttpStatus.NOT_FOUND,
);
}
const response = await this.tuyaService.updateAutomationState(
space.spaceTuyaUuid,
automation.automationTuyaUuid,
isEnable,
);
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
private async prepareActions(
actions: Action[],
@ -724,7 +753,7 @@ export class AutomationService {
action.action_executor === ActionExecutorEnum.RULE_ENABLE
) {
if (action.action_type === ActionTypeEnum.AUTOMATION) {
const automation = await this.findAutomationByUuid(
const automation = await this.findAutomation(
action.entity_id,
projectUuid,
);

View File

@ -3,11 +3,11 @@ import { IsNotEmpty, IsOptional, IsUUID, Matches } from 'class-validator';
export class BookingRequestDto {
@ApiProperty({
description: 'Month in MM-YYYY format',
example: '07-2025',
description: 'Month in MM/YYYY format',
example: '07/2025',
})
@IsNotEmpty()
@Matches(/^(0[1-9]|1[0-2])\-\d{4}$/, {
@Matches(/^(0[1-9]|1[0-2])\/\d{4}$/, {
message: 'Date must be in MM/YYYY format',
})
month: string;

View File

@ -1,12 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsDate,
IsNotEmpty,
IsString,
IsUUID,
Matches,
MinDate,
} from 'class-validator';
import { IsDate, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator';
export class CreateBookingDto {
@ApiProperty({
@ -22,7 +15,6 @@ export class CreateBookingDto {
})
@IsNotEmpty()
@IsDate()
@MinDate(new Date())
date: Date;
@ApiProperty({ example: '09:00' })

View File

@ -54,8 +54,7 @@ export class BookableSpaceService {
.createQueryBuilder('space')
.leftJoinAndSelect('space.parent', 'parentSpace')
.leftJoinAndSelect('space.community', 'community')
.where('space.disabled = :disabled', { disabled: false })
.andWhere('community.project = :project', { project });
.where('community.project = :project', { project });
if (search) {
qb = qb.andWhere(

View File

@ -51,7 +51,7 @@ export class BookingService {
}
async findAll({ month, space }: BookingRequestDto, project: string) {
const [monthNumber, year] = month.split('-').map(Number);
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({

View File

@ -25,7 +25,7 @@ import {
NotFoundException,
} from '@nestjs/common';
import { SpaceService } from 'src/space/services';
import { Brackets, QueryRunner, SelectQueryBuilder } from 'typeorm';
import { QueryRunner, SelectQueryBuilder } from 'typeorm';
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
@ -184,46 +184,18 @@ 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 != :orphanCommunityName', {
orphanCommunityName: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
})
.andWhere(`c.uuid IN (${matchingCommunityIdsQb.getQuery()})`)
.setParameters(matchingCommunityIdsQb.getParameters());
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
.distinct(true);
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(
@ -232,7 +204,16 @@ 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 } =

View File

@ -1,7 +1,4 @@
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 { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SceneService } from '../services/scene.service';
import {
Body,
Controller,
@ -11,21 +8,21 @@ import {
Param,
Post,
Put,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { SceneParamDto } from '../dtos';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import {
AddSceneIconDto,
AddSceneTapToRunDto,
GetSceneDto,
UpdateSceneTapToRunDto,
} from '../dtos/scene.dto';
import { SceneService } from '../services/scene.service';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { SceneParamDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
@ApiTags('Scene Module')
@Controller({
@ -55,27 +52,6 @@ export class SceneController {
);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SCENES_VIEW')
@Get('tap-to-run')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENES_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENES_DESCRIPTION,
})
async getTapToRunSceneBySpaces(
@Query() dto: GetSceneDto,
@Req() req: any,
): Promise<BaseResponseDto> {
const projectUuid = req.user.project.uuid;
const data = await this.sceneService.findScenesBySpaces(dto, projectUuid);
return new SuccessResponseDto({
message: 'Scenes Retrieved Successfully',
data,
});
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SCENES_DELETE')

View File

@ -1,16 +1,15 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
IsArray,
ValidateNested,
IsOptional,
IsNumber,
IsBoolean,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
class ExecutorProperty {
@ApiProperty({
@ -188,19 +187,4 @@ export class GetSceneDto {
return value.obj.showInHomePage === BooleanValues.TRUE;
})
public showInHomePage: boolean = false;
@ApiProperty({
description: 'List of Space IDs to filter automation',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@IsOptional()
@Transform(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
return value;
})
@IsUUID('4', { each: true })
public spaces?: string[];
}

View File

@ -1,36 +1,12 @@
import {
ActionExecutorEnum,
ActionTypeEnum,
} from '@app/common/constants/automation.enum';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import {
SceneEntity,
SceneIconEntity,
} from '@app/common/modules/scene/entities';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
BadRequestException,
forwardRef,
Injectable,
HttpException,
HttpStatus,
BadRequestException,
forwardRef,
Inject,
Injectable,
} from '@nestjs/common';
import { HttpStatusCode } from 'axios';
import { DeviceService } from 'src/device/services';
import { In } from 'typeorm';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
Action,
AddSceneIconDto,
@ -39,12 +15,35 @@ import {
SceneParamDto,
UpdateSceneTapToRunDto,
} from '../dtos';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import {
AddTapToRunSceneInterface,
DeleteTapToRunSceneInterface,
SceneDetails,
SceneDetailsResult,
} from '../interface/scene.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import {
ActionExecutorEnum,
ActionTypeEnum,
} from '@app/common/constants/automation.enum';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import {
SceneEntity,
SceneIconEntity,
} from '@app/common/modules/scene/entities';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { HttpStatusCode } from 'axios';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
import { DeviceService } from 'src/device/services';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
@Injectable()
export class SceneService {
@ -93,48 +92,158 @@ export class SceneService {
}
}
async findScenesBySpace(spaceUuid: string, { showInHomePage }: GetSceneDto) {
async create(
spaceTuyaUuid: string,
addSceneTapToRunDto: AddSceneTapToRunDto,
projectUuid: string,
): Promise<SceneEntity> {
const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto;
try {
await this.getSpaceByUuid(spaceUuid);
return this.findScenesBySpaces({ showInHomePage, spaces: [spaceUuid] });
} catch (error) {
console.error(
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
error.message,
const [defaultSceneIcon] = await Promise.all([
this.getDefaultSceneIcon(),
]);
if (!defaultSceneIcon) {
throw new HttpException(
'Default scene icon not found',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const response = await this.createSceneExternalService(
spaceTuyaUuid,
addSceneTapToRunDto,
projectUuid,
);
throw error instanceof HttpException
? error
: new HttpException(
'An error occurred while retrieving scenes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
const scene = await this.sceneRepository.save({
sceneTuyaUuid: response.result.id,
sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid },
showInHomePage,
space: { uuid: spaceUuid },
});
return scene;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
'Database error: Failed to save scene',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async updateSceneExternalService(
spaceTuyaUuid: string,
sceneTuyaUuid: string,
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
projectUuid: string,
) {
const { sceneName, decisionExpr, actions } = updateSceneTapToRunDto;
try {
const formattedActions = await this.prepareActions(actions, projectUuid);
const response = (await this.tuyaService.updateTapToRunScene(
sceneTuyaUuid,
spaceTuyaUuid,
sceneName,
formattedActions,
decisionExpr,
)) as AddTapToRunSceneInterface;
if (!response.success) {
throw new HttpException(
'Failed to update scene in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return response;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to update scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
`An Internal error has been occured ${err}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async findScenesBySpaces(
{ showInHomePage, spaces }: GetSceneDto,
projectUuid?: string,
async createSceneExternalService(
spaceTuyaUuid: string,
addSceneTapToRunDto: AddSceneTapToRunDto,
projectUuid: string,
) {
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
try {
const formattedActions = await this.prepareActions(actions, projectUuid);
const response = (await this.tuyaService.addTapToRunScene(
spaceTuyaUuid,
sceneName,
formattedActions,
decisionExpr,
)) as AddTapToRunSceneInterface;
if (!response.result?.id) {
throw new HttpException(
'Failed to create scene in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return response;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
`An Internal error has been occured ${err}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) {
try {
await this.getSpaceByUuid(spaceUuid);
const showInHomePage = filter?.showInHomePage;
const scenesData = await this.sceneRepository.find({
where: {
space: {
uuid: In(spaces ?? []),
community: projectUuid ? { project: { uuid: projectUuid } } : null,
},
space: { uuid: spaceUuid },
disabled: false,
...(showInHomePage ? { showInHomePage } : {}),
},
relations: ['sceneIcon', 'space', 'space.community'],
});
const safeFetch = async (scene: SceneEntity) => {
const safeFetch = async (scene: any) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { actions, ...sceneDetails } = await this.getScene(
scene,
scene.space.uuid,
spaceUuid,
);
return sceneDetails;
} catch (error) {
@ -150,7 +259,7 @@ export class SceneService {
return scenes.filter(Boolean); // Remove null values
} catch (error) {
console.error(
`Error fetching Tap-to-Run scenes for specified spaces:`,
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
error.message,
);
@ -182,6 +291,45 @@ export class SceneService {
}
}
async fetchSceneDetailsFromTuya(
sceneId: string,
): Promise<SceneDetailsResult> {
try {
const response = await this.tuyaService.getSceneRule(sceneId);
const camelCaseResponse = convertKeysToCamelCase(response);
const {
id,
name,
type,
status,
actions: tuyaActions = [],
} = camelCaseResponse.result;
const actions = tuyaActions.map((action) => ({ ...action }));
return {
id,
name,
type,
status,
actions,
} as SceneDetailsResult;
} catch (err) {
console.error(
`Error fetching scene details for scene ID ${sceneId}:`,
err,
);
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
'An error occurred while fetching scene details from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async updateTapToRunScene(
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
sceneUuid: string,
@ -238,38 +386,6 @@ export class SceneService {
}
}
async deleteScene(params: SceneParamDto): Promise<BaseResponseDto> {
const { sceneUuid } = params;
try {
const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(scene.space.uuid);
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
await this.sceneDeviceRepository.update(
{ uuid: sceneUuid },
{ disabled: true },
);
await this.sceneRepository.update(
{
uuid: sceneUuid,
},
{ disabled: true },
);
return new SuccessResponseDto({
message: `Scene with ID ${sceneUuid} deleted successfully`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
err.message || `Scene not found for id ${params.sceneUuid}`,
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async addSceneIcon(addSceneIconDto: AddSceneIconDto) {
try {
const icon = await this.sceneIconRepository.save({
@ -338,237 +454,7 @@ export class SceneService {
}
}
async findScene(sceneUuid: string): Promise<SceneEntity> {
const scene = await this.sceneRepository.findOne({
where: { uuid: sceneUuid },
relations: ['sceneIcon', 'space', 'space.community'],
});
if (!scene) {
throw new HttpException(
`Invalid scene with id ${sceneUuid}`,
HttpStatus.NOT_FOUND,
);
}
return scene;
}
async getSpaceByUuid(spaceUuid: string) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
relations: ['community'],
});
if (!space) {
throw new HttpException(
`Invalid space UUID ${spaceUuid}`,
HttpStatusCode.BadRequest,
);
}
if (!space.community.externalId) {
throw new HttpException(
`Space doesn't have any association with tuya${spaceUuid}`,
HttpStatusCode.BadRequest,
);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err;
} else {
throw new HttpException(
`Space with id ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}
}
private async create(
spaceTuyaUuid: string,
addSceneTapToRunDto: AddSceneTapToRunDto,
projectUuid: string,
): Promise<SceneEntity> {
const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto;
try {
const [defaultSceneIcon] = await Promise.all([
this.getDefaultSceneIcon(),
]);
if (!defaultSceneIcon) {
throw new HttpException(
'Default scene icon not found',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const response = await this.createSceneExternalService(
spaceTuyaUuid,
addSceneTapToRunDto,
projectUuid,
);
const scene = await this.sceneRepository.save({
sceneTuyaUuid: response.result.id,
sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid },
showInHomePage,
space: { uuid: spaceUuid },
});
return scene;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
'Database error: Failed to save scene',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async updateSceneExternalService(
spaceTuyaUuid: string,
sceneTuyaUuid: string,
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
projectUuid: string,
) {
const { sceneName, decisionExpr, actions } = updateSceneTapToRunDto;
try {
const formattedActions = await this.prepareActions(actions, projectUuid);
const response = (await this.tuyaService.updateTapToRunScene(
sceneTuyaUuid,
spaceTuyaUuid,
sceneName,
formattedActions,
decisionExpr,
)) as AddTapToRunSceneInterface;
if (!response.success) {
throw new HttpException(
'Failed to update scene in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return response;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to update scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
`An Internal error has been occured ${err}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async createSceneExternalService(
spaceTuyaUuid: string,
addSceneTapToRunDto: AddSceneTapToRunDto,
projectUuid: string,
) {
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
try {
const formattedActions = await this.prepareActions(actions, projectUuid);
const response = (await this.tuyaService.addTapToRunScene(
spaceTuyaUuid,
sceneName,
formattedActions,
decisionExpr,
)) as AddTapToRunSceneInterface;
if (!response.result?.id) {
throw new HttpException(
'Failed to create scene in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return response;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
`An Internal error has been occured ${err}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async fetchSceneDetailsFromTuya(
sceneId: string,
): Promise<SceneDetailsResult> {
try {
const response = await this.tuyaService.getSceneRule(sceneId);
const camelCaseResponse = convertKeysToCamelCase(response);
const {
id,
name,
type,
status,
actions: tuyaActions = [],
} = camelCaseResponse.result;
const actions = tuyaActions.map((action) => ({ ...action }));
return {
id,
name,
type,
status,
actions,
} as SceneDetailsResult;
} catch (err) {
console.error(
`Error fetching scene details for scene ID ${sceneId}:`,
err,
);
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
'An error occurred while fetching scene details from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async getScene(
scene: SceneEntity,
spaceUuid: string,
): Promise<SceneDetails> {
async getScene(scene: SceneEntity, spaceUuid: string): Promise<SceneDetails> {
try {
const { actions, name, status } = await this.fetchSceneDetailsFromTuya(
scene.sceneTuyaUuid,
@ -633,7 +519,54 @@ export class SceneService {
}
}
private async delete(tuyaSceneId: string, tuyaSpaceId: string) {
async deleteScene(params: SceneParamDto): Promise<BaseResponseDto> {
const { sceneUuid } = params;
try {
const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(scene.space.uuid);
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
await this.sceneDeviceRepository.update(
{ uuid: sceneUuid },
{ disabled: true },
);
await this.sceneRepository.update(
{
uuid: sceneUuid,
},
{ disabled: true },
);
return new SuccessResponseDto({
message: `Scene with ID ${sceneUuid} deleted successfully`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
err.message || `Scene not found for id ${params.sceneUuid}`,
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async findScene(sceneUuid: string): Promise<SceneEntity> {
const scene = await this.sceneRepository.findOne({
where: { uuid: sceneUuid },
relations: ['sceneIcon', 'space', 'space.community'],
});
if (!scene) {
throw new HttpException(
`Invalid scene with id ${sceneUuid}`,
HttpStatus.NOT_FOUND,
);
}
return scene;
}
async delete(tuyaSceneId: string, tuyaSpaceId: string) {
try {
const response = (await this.tuyaService.deleteSceneRule(
tuyaSceneId,
@ -693,4 +626,45 @@ export class SceneService {
});
return defaultIcon;
}
async getSpaceByUuid(spaceUuid: string) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
relations: ['community'],
});
if (!space) {
throw new HttpException(
`Invalid space UUID ${spaceUuid}`,
HttpStatusCode.BadRequest,
);
}
if (!space.community.externalId) {
throw new HttpException(
`Space doesn't have any association with tuya${spaceUuid}`,
HttpStatusCode.BadRequest,
);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err;
} else {
throw new HttpException(
`Space with id ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}
}
}

View File

@ -17,7 +17,6 @@ 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';
@ -49,26 +48,6 @@ 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')

View File

@ -6,6 +6,7 @@ import {
IsArray,
IsMongoId,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
@ -47,6 +48,14 @@ 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',

View File

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

View File

@ -4,6 +4,7 @@ import { Type } from 'class-transformer';
import {
ArrayUnique,
IsArray,
IsNumber,
IsOptional,
IsString,
NotEquals,
@ -35,6 +36,16 @@ 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],

View File

@ -133,7 +133,7 @@ export class ValidationService {
'subspaces.productAllocations',
'subspaces.productAllocations.product',
'subspaces.devices',
// 'spaceModel',
'spaceModel',
],
});

View File

@ -8,7 +8,6 @@ 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,
@ -25,7 +24,6 @@ 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,
@ -34,7 +32,6 @@ 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';
@ -96,9 +93,6 @@ 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)
@ -108,6 +102,8 @@ 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
@ -185,154 +181,6 @@ 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>();
@ -657,8 +505,6 @@ 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,
@ -809,11 +655,13 @@ export class SpaceService {
updateSpaceDto: UpdateSpaceDto,
queryRunner: QueryRunner,
): Promise<void> {
const { spaceName, icon } = updateSpaceDto;
const { spaceName, x, y, 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) {
@ -980,34 +828,4 @@ 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,
);
}
}
}
}

View File

@ -1,11 +1,11 @@
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 { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { UserParamDto } from '../dtos';
import { UserSpaceService } from '../services';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { UserParamDto } from '../dtos';
@ApiTags('User Module')
@Controller({

View File

@ -11,7 +11,6 @@ import {
Param,
Patch,
Put,
Query,
Req,
UseGuards,
} from '@nestjs/common';
@ -25,7 +24,6 @@ import {
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from '../dtos';
import { UsersWithBookableSpacesFilterDto } from '../dtos/users-with-bookable-spaces-filter.dto';
import { UserService } from '../services/user.service';
@ApiTags('User Module')
@ -36,21 +34,6 @@ import { UserService } from '../services/user.service';
export class UserController {
constructor(private readonly userService: UserService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('with-bookable-spaces')
@ApiOperation({
summary:
ControllerRoute.USER.ACTIONS.GET_USERS_WITH_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.USER.ACTIONS.GET_USERS_WITH_BOOKABLE_SPACES_DESCRIPTION,
})
async getUsersWithBookableSpaces(
@Query() dto: UsersWithBookableSpacesFilterDto,
): Promise<BaseResponseDto> {
return this.userService.getUsersWithBookableSpaces(dto);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':userUuid')

View File

@ -1,21 +0,0 @@
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsOptional, ValidateIf } from 'class-validator';
export class UsersWithBookableSpacesFilterDto extends PaginationRequestGetListDto {
@ApiProperty({
enum: ['accessStartDate', 'accessEndDate'],
required: false,
})
@IsEnum(['accessStartDate', 'accessEndDate'])
@ValidateIf((o) => o.sortDirection)
sortBy: 'accessStartDate' | 'accessEndDate';
@ApiProperty({
enum: ['asc', 'desc'],
required: false,
})
@IsEnum(['asc', 'desc'])
@IsOptional()
sortDirection: 'asc' | 'desc';
}

View File

@ -1,18 +1,15 @@
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { UserEntity } from '@app/common/modules/user/entities';
import { UserRepository } from '@app/common/modules/user/repositories';
import { getPaginationResponseDto } from '@app/common/util/getPaginationResponseDto';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { UsersWithBookableSpacesFilterDto } from '../dtos/users-with-bookable-spaces-filter.dto';
import {
UpdateNameDto,
UpdateProfilePictureDataDto,
@ -66,38 +63,6 @@ export class UserService {
}
}
async getUsersWithBookableSpaces({
sortBy,
sortDirection,
page,
size,
}: UsersWithBookableSpacesFilterDto) {
size = size ?? 10;
page = page ?? 1;
const qb = this.userRepository
.createQueryBuilder('user')
.innerJoin('user.userSpaces', 'userSpaces')
.innerJoin('userSpaces.space', 'space')
.innerJoin('space.bookableConfig', 'bookableConfig')
.where('bookableConfig.uuid IS NOT NULL')
.leftJoinAndSelect('user.inviteUser', 'inviteUser')
.take(size)
.skip((page - 1) * size)
.distinct(true);
if (sortBy) {
qb.orderBy(
':sortBy',
sortDirection == 'desc' ? 'DESC' : 'ASC',
).setParameter('sortBy', sortBy);
}
const [data, count] = await qb.getManyAndCount();
return new PageResponse(
{ message: 'users fetched successfully', data },
getPaginationResponseDto(count, page, size),
);
}
async updateProfilePictureByUserUuid(
userUuid: string,
updateProfilePictureDataDto: UpdateProfilePictureDataDto,