diff --git a/src/modules/event/dto/search-event.dto.ts b/src/modules/event/dto/search-event.dto.ts index 5c2edef..f37a421 100644 --- a/src/modules/event/dto/search-event.dto.ts +++ b/src/modules/event/dto/search-event.dto.ts @@ -61,7 +61,7 @@ export class FilterDto { @IsString() title?: string; - @ApiProperty({ example: 'CohortId', description: 'CohortId' }) + @ApiProperty({ example: 'CohortId', description: 'Cohort' }) @IsOptional() @IsUUID('4') cohortId?: string; diff --git a/src/modules/event/dto/update-event.dto.ts b/src/modules/event/dto/update-event.dto.ts index 4d533f6..9327d3a 100644 --- a/src/modules/event/dto/update-event.dto.ts +++ b/src/modules/event/dto/update-event.dto.ts @@ -1,181 +1,217 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsOptional, IsString, IsUUID, IsEnum, IsLongitude, IsLatitude, IsBoolean, IsInt, Min, IsDateString, IsObject } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsUUID, IsEnum, IsLongitude, IsLatitude, IsBoolean, IsInt, Min, IsDateString, IsObject, ValidateIf, ValidateNested } from 'class-validator'; +import { MeetingDetails } from "src/common/utils/types"; +import { MeetingDetailsDto } from "./create-event.dto"; +import { Type } from "class-transformer"; export class UpdateEventDto { - @ApiProperty({ - type: String, - description: 'title', - example: 'Sample Event' - }) - @IsString() - @IsNotEmpty() - @IsOptional() - title?: string; + // @ApiProperty({ + // type: String, + // description: 'title', + // example: 'Sample Event' + // }) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // title?: string; - @ApiProperty({ - type: String, - description: 'Short Description', - example: 'This is a sample event', - required: false, - }) - @IsString() - @IsNotEmpty() - @IsOptional() - shortDescription?: string; @ApiProperty({ type: String, - description: 'Description', - example: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + description: 'Status', + example: 'live' }) - @IsString() - @IsNotEmpty() - @IsOptional() - description: string; - - - @ApiProperty({ - type: String, - description: 'image', - example: 'https://example.com/sample-image.jpg' + @IsEnum(['live', 'draft', 'archived'], { + message: 'Status must be one of: live, draft, archived', }) @IsString() - @IsNotEmpty() @IsOptional() - image: string; - - @ApiProperty({ - type: String, - description: 'Event Type', - example: 'online' - }) - @IsEnum(['online', 'offline', 'onlineandoffline'], { - message: 'Event Type must be one of: online, offline, onlineandoffline' - } - ) - @IsString() @IsNotEmpty() - @IsOptional() - eventType: string; + status: string; @ApiProperty({ type: String, - description: 'isRestricted', + description: 'isRecurring', example: true }) @IsBoolean() - @IsOptional() - isRestricted: boolean; - - @ApiProperty({ - type: String, - description: 'Start Datetime', - example: '2024-03-18T10:00:00Z' - }) - @IsDateString() - @IsOptional() - startDatetime: Date; - - @ApiProperty({ - type: String, - description: 'End Datetime', - example: '2024-03-18T10:00:00Z' - }) - @IsDateString() - @IsOptional() - endDatetime: Date; - - @ApiProperty({ - type: String, - description: 'Location', - example: 'Event Location' - }) - @IsString() - @IsNotEmpty() - @IsOptional() - location: string; - - - @ApiProperty({ - type: Number, - description: 'Latitude', - example: 18.508345134886994 - }) - @IsLongitude() - @IsOptional() - longitude: number; - - @ApiProperty({ - type: Number, - description: 'Latitude', - example: 18.508345134886994 - }) - @IsLatitude() - @IsOptional() - latitude: number; - - @ApiProperty({ - type: String, - description: 'Online Provider', - example: 'Zoom' - }) - @IsString() - @IsNotEmpty() - @IsOptional() - onlineProvider: string; - - @ApiProperty({ - type: String, - description: 'Registration Deadline', - example: '2024-03-18T10:00:00Z' - }) - @IsDateString() - @IsOptional() - registrationDeadline: Date; - - @ApiProperty({ - type: Number, - description: 'Max Attendees', - example: 100 - }) - @IsInt() - @IsOptional() - @Min(0) - maxAttendees: number; - - @ApiProperty({ - type: Object, - description: 'Params', - // example: { cohortIds: ['eff008a8-2573-466d-b877-fddf6a4fc13e', 'e9fec05a-d6ab-44be-bfa4-eaeef2ef8fe9'] }, - // example: { userIds: ['eff008a8-2573-466d-b877-fddf6a4fc13e', 'e9fec05a-d6ab-44be-bfa4-eaeef2ef8fe9'] }, - example: { cohortIds: ['eff008a8-2573-466d-b877-fddf6a4fc13e'] }, - }) - @IsObject() - @IsOptional() - params: any; - - @ApiProperty({ - type: Object, - description: 'Recordings', - example: { url: 'https://example.com/recording' } - }) - @IsObject() - @IsOptional() - recordings: any; - - @ApiProperty({ - type: String, - description: 'Status', - example: 'live' - }) - @IsEnum(['live', 'draft', 'inActive'], { - message: 'Status must be one of: live, draft, inActive', - }) - @IsString() - @IsOptional() - @IsNotEmpty() - status: string; + isMainEvent: boolean; + + // @ApiProperty({ + // type: MeetingDetailsDto, + // description: 'Online Meeting Details', + // example: { + // url: 'https://example.com/meeting', + // id: '123-456-789', + // password: 'xxxxxxx', + // }, + // }) + // @IsObject() + // @ValidateNested({ each: true }) + // @IsOptional() + // @Type(() => MeetingDetailsDto) + // meetingDetails: MeetingDetails; + + // Validation to ensure if isMainEvent is true, title or status must be provided + @ValidateIf(o => !o.title && !o.status && !o.onlineDetails && !o.location && !o.latitude) // Ensure that if neither title nor status is provided, validation fails + @IsNotEmpty({ message: 'If isMainEvent is provided, at least one of title or status must be provided.' }) + dummyField?: any; + + + // @ApiProperty({ + // type: String, + // description: 'Event Type', + // example: 'online' + // }) + // @IsEnum(['online', 'offline', 'onlineandoffline'], { + // message: 'Event Type must be one of: online, offline, onlineandoffline' + // } + // ) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // eventType: string; + + + + // @ApiProperty({ + // type: String, + // description: 'Start Datetime', + // example: '2024-03-18T10:00:00Z' + // }) + // @IsDateString() + // @IsOptional() + // startDatetime: Date; + + // @ApiProperty({ + // type: String, + // description: 'End Datetime', + // example: '2024-03-18T10:00:00Z' + // }) + // @IsDateString() + // @IsOptional() + // endDatetime: Date; + + // @ApiProperty({ + // type: String, + // description: 'Location', + // example: 'Event Location' + // }) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // location: string; + + + // @ApiProperty({ + // type: Number, + // description: 'Latitude', + // example: 18.508345134886994 + // }) + // @IsLongitude() + // @IsOptional() + // longitude: number; + + // @ApiProperty({ + // type: Number, + // description: 'Latitude', + // example: 18.508345134886994 + // }) + // @IsLatitude() + // @IsOptional() + // latitude: number; + + + // @ApiProperty({ + // type: String, + // description: 'Short Description', + // example: 'This is a sample event', + // required: false, + // }) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // shortDescription?: string; + + // @ApiProperty({ + // type: String, + // description: 'Description', + // example: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + // }) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // description: string; + + + // @ApiProperty({ + // type: String, + // description: 'image', + // example: 'https://example.com/sample-image.jpg' + // }) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // image: string; + + // @ApiProperty({ + // type: String, + // description: 'Online Provider', + // example: 'Zoom' + // }) + // @IsString() + // @IsNotEmpty() + // @IsOptional() + // onlineProvider: string; + + // @ApiProperty({ + // type: String, + // description: 'Registration Deadline', + // example: '2024-03-18T10:00:00Z' + // }) + // @IsDateString() + // @IsOptional() + // registrationDeadline: Date; + + // @ApiProperty({ + // type: Number, + // description: 'Max Attendees', + // example: 100 + // }) + // @IsInt() + // @IsOptional() + // @Min(0) + // maxAttendees: number; + + // @ApiProperty({ + // type: Object, + // description: 'Params', + // // example: { cohortIds: ['eff008a8-2573-466d-b877-fddf6a4fc13e', 'e9fec05a-d6ab-44be-bfa4-eaeef2ef8fe9'] }, + // // example: { userIds: ['eff008a8-2573-466d-b877-fddf6a4fc13e', 'e9fec05a-d6ab-44be-bfa4-eaeef2ef8fe9'] }, + // example: { cohortIds: ['eff008a8-2573-466d-b877-fddf6a4fc13e'] }, + // }) + // @IsObject() + // @IsOptional() + // params: any; + + // @ApiProperty({ + // type: Object, + // description: 'Recordings', + // example: { url: 'https://example.com/recording' } + // }) + // @IsObject() + // @IsOptional() + // recordings: any; + + // @ApiProperty({ + // type: String, + // description: 'isRestricted', + // example: true + // }) + // @IsBoolean() + // @IsOptional() + // isRestricted: boolean; @IsString() diff --git a/src/modules/event/event.controller.ts b/src/modules/event/event.controller.ts index f5817f6..3c4e0a0 100644 --- a/src/modules/event/event.controller.ts +++ b/src/modules/event/event.controller.ts @@ -47,7 +47,7 @@ export class EventController { constructor( private readonly eventService: EventService, private readonly configService: ConfigService, - ) {} + ) { } @UseFilters(new AllExceptionsFilter(API_ID.CREATE_EVENT)) @Post('/create') @@ -121,7 +121,7 @@ export class EventController { throw new BadRequestException(ERROR_MESSAGES.INVALID_REQUEST_BODY); } const userId = '01455719-e84f-4bc8-8efa-7024874ade08'; // later come from JWT-token - // return this.eventService.updateEvent(id, updateEventDto, userId, response); + return this.eventService.updateEvent(id, updateEventDto, response); } @UseFilters(new AllExceptionsFilter(API_ID.DELETE_EVENT)) diff --git a/src/modules/event/event.service.ts b/src/modules/event/event.service.ts index bd9cabb..58786bd 100644 --- a/src/modules/event/event.service.ts +++ b/src/modules/event/event.service.ts @@ -8,7 +8,7 @@ import { import { CreateEventDto } from './dto/create-event.dto'; import { UpdateEventDto } from './dto/update-event.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; +import { Repository, In, Not, MoreThan, MoreThanOrEqual } from 'typeorm'; import { Events } from './entities/event.entity'; import { Response } from 'express'; import APIResponse from 'src/common/utils/response'; @@ -93,6 +93,7 @@ export class EventService { try { const { filters } = requestBody; const today = new Date(); + let finalquery = `SELECT er."eventDetailId" AS "eventRepetition_eventDetailId", er.*, @@ -109,7 +110,7 @@ export class EventService { //User not pass any things then it show today and upcoming event if (!filters || Object.keys(filters).length === 0) { finalquery += ` WHERE (er."startDateTime" >= CURRENT_TIMESTAMP - OR er."endDateTime" > CURRENT_TIMESTAMP) AND status='live'`; + OR er."endDateTime" > CURRENT_TIMESTAMP) AND ed.status='live'`; } // if user pass somthing in filter then make query @@ -123,11 +124,13 @@ export class EventService { // Append LIMIT and OFFSET to the query finalquery += ` LIMIT ${limit} OFFSET ${offset}`; + const result = await this.eventRepetitionRepository.query(finalquery); const totalCount = result[0]?.total_count; // Add isEnded key based on endDateTime const finalResult = result.map((event) => { + delete event.total_count; delete event.total_count; const endDateTime = new Date(event.endDateTime); return { @@ -136,7 +139,7 @@ export class EventService { }; }); if (finalResult.length === 0) { - throw new NotFoundException('Event Not Found'); + throw new NotFoundException('Event Not Found') } return response .status(HttpStatus.OK) @@ -219,7 +222,12 @@ export class EventService { // Handle cohortId filter if (filters.cohortId) { - whereClauses.push(`ed."metadata"->>'cohortId'='${filters.cohortId}'`); + whereClauses.push(`ed."metadata"->>'cohortId'='${filters.cohortId}'`) + } + + // Handle cohortId filter + if (filters.cohortId) { + whereClauses.push(`ed."metadata"->>'cohortId'='${filters.cohortId}'`) } // Construct final query @@ -229,6 +237,124 @@ export class EventService { return finalquery; } + async updateEvent(eventRepetitionId, updateBody, response) { + const apiId = 'api.update.event'; + try { + const currentTimestamp = new Date(); + // need to check startdate of this particuler event for edit permission + const eventRepetition = await this.eventRepetitionRepository.findOne({ where: { eventRepetitionId, startDateTime: MoreThan(currentTimestamp) } }); + if (!eventRepetition) { + throw new NotFoundException('Event Not found') + } + + const event = await this.eventRepository.findOne({ where: { eventId: eventRepetition.eventId } }); + // condition for prevent non recuring event + if (!event.isRecurring && !updateBody.isMainEvent) { + throw new BadRequestException('You can not pass isMainEvent false beacuse event is non recurring') + } + const eventDetail = await this.eventDetailRepository.findOne({ where: { eventDetailId: event.eventDetailId } }); + + if (this.isInvalidUpdate(updateBody, eventDetail)) { + throw new BadRequestException('Not editable field'); + } + let result; + if (updateBody?.isMainEvent) { + // Handle updates or deletions for all recurrence records + result = await this.handleAllEventUpdate(updateBody, event, eventRepetition); + } else { + // Handle updates or deletions for a specific recurrence record + result = await this.handleSpecificRecurrenceUpdate(updateBody, event, eventRepetition); + } + + return response + .status(HttpStatus.OK) + .json(APIResponse.success(apiId, result, 'OK')); + + } catch (error) { + throw error; + } + } + + async handleAllEventUpdate(updateBody, event, eventRepetition) { + const eventId = eventRepetition.eventId; + const eventDetailId = event.eventDetailId; + const existingEventDetails = await this.eventDetailRepository.findOne({ where: { eventDetailId: eventDetailId } }); + const startDateTime = eventRepetition.startDateTime; + const recurrenceRecords = await this.eventRepetitionRepository.find({ + where: { eventId: eventId, eventDetailId: Not(eventDetailId), startDateTime: MoreThanOrEqual(startDateTime) } + }); + + if (recurrenceRecords.length > 0) { + await this.eventRepetitionRepository.update( + { eventRepetitionId: In(recurrenceRecords.map(record => record.eventRepetitionId)) }, + { eventDetailId: eventDetailId } + ); + + await this.eventDetailRepository.delete( + { eventDetailId: In(recurrenceRecords.map(record => record.eventDetailId)) } + ); + } + + if (updateBody.onlineDetails) { + await this.eventRepetitionRepository.update( + { eventDetailId: eventDetailId }, + { onlineDetails: updateBody.onlineDetails } + ); + } + + Object.assign(existingEventDetails, updateBody, { eventRepetitionId: eventRepetition.eventRepetitionId }); + existingEventDetails.updatedAt = new Date(); + const result = await this.eventDetailRepository.save(existingEventDetails); + return result; + } + + async handleSpecificRecurrenceUpdate(updateBody, event, eventRepetition) { + const eventDetailId = eventRepetition.eventDetailId; + const existingEventDetails = await this.eventDetailRepository.findOne({ where: { eventDetailId: eventDetailId } }); + existingEventDetails.updatedAt = new Date() + let result; + if (event.eventDetailId === existingEventDetails.eventDetailId) { + if (existingEventDetails.status === 'archived') { + throw new BadRequestException('Event is already archived') + } + Object.assign(existingEventDetails, updateBody, { eventRepetitionId: eventRepetition.eventRepetitionId }); + delete existingEventDetails.eventDetailId; + result = await this.eventDetailRepository.save(existingEventDetails); + // const updateResult = await this.eventRepetitionRepository.update( + // { eventRepetitionId: eventRepetition.eventRepetitionId }, + // { eventDetailId: newEntry.eventDetailId } + // ); + eventRepetition.eventDetailId = result.eventDetailId; + const UpdateResult = await this.eventRepetitionRepository.save(eventRepetition); + } else { + Object.assign(existingEventDetails, updateBody, { eventRepetitionId: eventRepetition.eventRepetitionId }); + result = await this.eventDetailRepository.save(existingEventDetails); + } + if (updateBody.onlineDetails) { + result = await this.eventRepetitionRepository.update( + { eventDetailId: eventDetailId }, + { onlineDetails: updateBody.onlineDetails } + ); + } + return result; + } + + isInvalidUpdate(updateBody, eventDetail) { + if (updateBody.location || (updateBody.latitude && updateBody.longitude)) { + if (eventDetail.eventType === 'online') { + return true; + } + } + + if (updateBody.onlineDetails) { + if (eventDetail.eventType === 'offline') { + return true; + } + } + + return false; + } + async createEventDetailDB( createEventDto: CreateEventDto, ): Promise { @@ -538,7 +664,7 @@ export class EventService { if ( config.endCondition.type === 'endDate' && occurrences[occurrences.length - 1]?.endDateTime > - new Date(config.endCondition.value) + new Date(config.endCondition.value) ) { occurrences.pop(); }