diff --git a/src/modules/host/handlers/Activity_Hub/Scheduling/openCanceledSlot.ts b/src/modules/host/handlers/Activity_Hub/Scheduling/openCanceledSlot.ts index 17a2760..aa4fac2 100644 --- a/src/modules/host/handlers/Activity_Hub/Scheduling/openCanceledSlot.ts +++ b/src/modules/host/handlers/Activity_Hub/Scheduling/openCanceledSlot.ts @@ -1,24 +1,31 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; -import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; +import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../../common/utils/helper/ApiError'; import { SchedulingService } from '../../../services/activityScheduling.service'; import { HostService } from '../../../services/host.service'; -import ApiError from '../../../../../common/utils/helper/ApiError'; -import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; -import { scheduleActivity } from '../../../../../common/utils/validation/host/createSchedulingOfAct.validation'; -import { z } from 'zod'; const schedulingService = new SchedulingService(prismaClient); const hostService = new HostService(prismaClient); -export const handler = safeHandler(async ( +export const handler = safeHandler( + async ( event: APIGatewayProxyEvent, - context?: Context -): Promise => { + context?: Context, + ): Promise => { // Extract token from headers - const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'] + const token = + event.headers['x-auth-token'] || event.headers['X-Auth-Token']; if (!token) { - throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); + throw new ApiError( + 400, + 'This is a protected route. Please provide a valid token.', + ); } // Authenticate user using the shared authForHost function @@ -26,60 +33,72 @@ export const handler = safeHandler(async ( const hostId = userInfo.id; if (Number.isNaN(hostId)) { - throw new ApiError(400, 'Host id must be a number'); + throw new ApiError(400, 'Host id must be a number'); } const host = await hostService.getHostIdByUserXid(hostId); if (!host) { - throw new ApiError(404, 'Host not found'); + throw new ApiError(404, 'Host not found'); } - let body: { activityXid: number; venueXid: number; cancellationXid: number; slotXid: number; }; + let body: { + activityXid: number; + venueXid: number; + cancellations: { cancellationXid: number; slotXid: number }[]; + }; try { - body = event.body ? JSON.parse(event.body) : {}; + body = event.body ? JSON.parse(event.body) : {}; } catch { - throw new ApiError(400, 'Invalid JSON payload'); + throw new ApiError(400, 'Invalid JSON payload'); } - if (!body.activityXid || !body.venueXid || !body.cancellationXid || !body.slotXid) { - throw new ApiError(400, 'Missing required fields'); + + if ( + !body.activityXid || + !body.venueXid || + !Array.isArray(body.cancellations) || + body.cancellations.length === 0 + ) { + throw new ApiError(400, 'Missing required fields'); } const activity = await schedulingService.getActivityByXid(body.activityXid); if (!activity) { - throw new ApiError(404, "Activity not found"); + throw new ApiError(404, 'Activity not found'); } const venueExists = await schedulingService.getVenueFromVenueXid( - body.venueXid, - body.activityXid + body.venueXid, + body.activityXid, ); if (!venueExists) { - throw new ApiError( - 404, - `Venue not found for this activity` - ) + throw new ApiError(404, `Venue not found for this activity`); } - await schedulingService.openCanceledSlot( - Number(body.cancellationXid), - Number(body.slotXid), + body.cancellations.map((item: any) => ({ + cancellationXid: Number(item.cancellationXid), + slotXid: Number(item.slotXid), + })), ); - const result = await schedulingService.getVenueDurationByAct(Number(body.activityXid), Number(hostId)); + const result = await schedulingService.getVenueDurationByAct( + Number(body.activityXid), + Number(hostId), + ); return { - statusCode: 200, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - body: JSON.stringify({ - success: true, - message: 'Slot opened successfully', - data: result - }), + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Slot opened successfully', + data: result, + }), }; -}); \ No newline at end of file + }, +); diff --git a/src/modules/host/services/activityScheduling.service.ts b/src/modules/host/services/activityScheduling.service.ts index b46f0fb..36cab6b 100644 --- a/src/modules/host/services/activityScheduling.service.ts +++ b/src/modules/host/services/activityScheduling.service.ts @@ -1,727 +1,739 @@ import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; -import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, SCHEDULING_TYPE } from '../../../common/utils/constants/host.constant'; +import { + ACTIVITY_AM_DISPLAY_STATUS, + ACTIVITY_AM_INTERNAL_STATUS, + ACTIVITY_DISPLAY_STATUS, + ACTIVITY_INTERNAL_STATUS, + SCHEDULING_TYPE, +} from '../../../common/utils/constants/host.constant'; import ApiError from '../../../common/utils/helper/ApiError'; import { ScheduleActivityDTO } from '../../../common/utils/validation/host/createSchedulingOfAct.validation'; import config from '../../../config/config'; - const bucket = config.aws.bucketName; @Injectable() export class SchedulingService { - constructor(private prisma: PrismaClient) { } + constructor(private prisma: PrismaClient) {} - async getHostIdByUserId(userId: number) { - const host = await this.prisma.hostHeader.findFirst({ - where: { - userXid: userId, - isActive: true - }, - select: { - id: true - } - }) + async getHostIdByUserId(userId: number) { + const host = await this.prisma.hostHeader.findFirst({ + where: { + userXid: userId, + isActive: true, + }, + select: { + id: true, + }, + }); - if (!host) { - throw new ApiError(404, 'Host not found for the given user.'); - } - - return host.id; + if (!host) { + throw new ApiError(404, 'Host not found for the given user.'); } - async addSchedulingForActivity(data: ScheduleActivityDTO) { - const { - activityXid, - listNow, - scheduleType, - dateRange, - rules, - venues, - earlyCheckInMins, - bookingCutOffMins, - isLateCheckingAllowed, - isInstantBooking - } = data; + return host.id; + } - return this.prisma.$transaction(async (tx) => { + async addSchedulingForActivity(data: ScheduleActivityDTO) { + const { + activityXid, + listNow, + scheduleType, + dateRange, + rules, + venues, + earlyCheckInMins, + bookingCutOffMins, + isLateCheckingAllowed, + isInstantBooking, + } = data; - const venueXids = venues.map(v => v.venueXid); + return this.prisma.$transaction(async (tx) => { + const venueXids = venues.map((v) => v.venueXid); - /* ---------------------------------- + /* ---------------------------------- ๐Ÿงน 0๏ธโƒฃ DELETE OLD SCHEDULING (PER ACTIVITY + VENUES) ---------------------------------- */ - const oldHeaders = await tx.scheduleHeader.findMany({ - where: { - activityXid, - activityVenueXid: { in: venueXids }, - isActive: true, - }, - select: { id: true }, - }); + const oldHeaders = await tx.scheduleHeader.findMany({ + where: { + activityXid, + activityVenueXid: { in: venueXids }, + isActive: true, + }, + select: { id: true }, + }); - const headerIds = oldHeaders.map(h => h.id); + const headerIds = oldHeaders.map((h) => h.id); - if (headerIds.length) { - // Delete in correct FK order - await tx.cancellations.deleteMany({ - where: { scheduleHeaderXid: { in: headerIds } }, - }); + if (headerIds.length) { + // Delete in correct FK order + await tx.cancellations.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); - await tx.scheduleDetails.deleteMany({ - where: { scheduleHeaderXid: { in: headerIds } }, - }); + await tx.scheduleDetails.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); - await tx.scheduleOccurences.deleteMany({ - where: { scheduleHeaderXid: { in: headerIds } }, - }); + await tx.scheduleOccurences.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); - await tx.scheduleRecurrence.deleteMany({ - where: { scheduleHeaderXid: { in: headerIds } }, - }); + await tx.scheduleRecurrence.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); - await tx.scheduleHeader.deleteMany({ - where: { id: { in: headerIds } }, - }); - } + await tx.scheduleHeader.deleteMany({ + where: { id: { in: headerIds } }, + }); + } - /* ---------------------------------- + /* ---------------------------------- โž• 1๏ธโƒฃ CREATE NEW SCHEDULING ---------------------------------- */ - const createdHeaders: number[] = []; + const createdHeaders: number[] = []; - if (isInstantBooking !== undefined || isLateCheckingAllowed !== undefined) { - await tx.activities.update({ - where: { id: activityXid, isActive: true }, - data: { isInstantBooking, isLateCheckingAllowed }, - }); - } - - if (listNow) { - await tx.activities.update({ - where: { id: activityXid, isActive: true }, - data: { - activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, - activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ACTIVITY_LISTED, - amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, - amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_LISTED - } - }) - } - - - for (const venue of venues) { - - if (!venue.slots || venue.slots.length === 0) { - continue; - } - - const header = await tx.scheduleHeader.create({ - data: { - activityXid, - activityVenueXid: venue.venueXid, - scheduleType, - startDate: new Date(dateRange.startDate), - endDate: dateRange.endDate ? new Date(dateRange.endDate) : null, - earlyCheckInMins, - bookingCutOffMins, - isActive: true, - }, - }); - - createdHeaders.push(header.id); - - // WEEKLY - if (scheduleType === SCHEDULING_TYPE.WEEKLY) { - const uniqueWeekdays = [ - ...new Set( - venue.slots - .map(s => s.weekDay) - .filter((d): d is "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" | "SATURDAY" | "SUNDAY" => !!d) - ), - ]; - - if (!uniqueWeekdays.length) { - throw new ApiError(400, 'Weekly schedule requires weekDay in slots'); - } - - await tx.scheduleRecurrence.createMany({ - data: uniqueWeekdays.map(day => ({ - scheduleHeaderXid: header.id, - weekDay: day, - isActive: true, - })), - }); - } - - - // MONTHLY - if (scheduleType === SCHEDULING_TYPE.MONTHLY) { - const uniqueDays = [ - ...new Set( - venue.slots - .map(s => s.dayOfMonth) - .filter((d): d is number => d !== null && d !== undefined) - ), - ]; - - if (!uniqueDays.length) { - throw new ApiError(400, 'Monthly schedule requires dayOfMonth in slots'); - } - - await tx.scheduleRecurrence.createMany({ - data: uniqueDays.map(day => ({ - scheduleHeaderXid: header.id, - dayOfMonth: day, - isActive: true, - })), - }); - } - - - // CUSTOM / ONCE - if (scheduleType === SCHEDULING_TYPE.CUSTOM || scheduleType === SCHEDULING_TYPE.ONCE) { - const uniqueDates = [ - ...new Set( - venue.slots - .map(s => s.occurrenceDate) - .filter(Boolean) - ), - ]; - - await tx.scheduleOccurences.createMany({ - data: uniqueDates.map(d => ({ - scheduleHeaderXid: header.id, - occurenceDate: new Date(d!), - isActive: true, - })), - }); - } - - - // Slots - for (const slot of venue.slots) { - await tx.scheduleDetails.create({ - data: { - scheduleHeaderXid: header.id, - occurenceDate: slot.occurrenceDate ? new Date(slot.occurrenceDate) : null, - weekDay: slot.weekDay ?? null, - dayOfMonth: slot.dayOfMonth ?? null, - startTime: slot.startTime, - endTime: slot.endTime, - maxCapacity: slot.maxCapacity, - isActive: true, - }, - }); - } - } - - return { success: true, scheduleHeaderIds: createdHeaders }; + if ( + isInstantBooking !== undefined || + isLateCheckingAllowed !== undefined + ) { + await tx.activities.update({ + where: { id: activityXid, isActive: true }, + data: { isInstantBooking, isLateCheckingAllowed }, }); - } + } + if (listNow) { + await tx.activities.update({ + where: { id: activityXid, isActive: true }, + data: { + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_LISTED, + }, + }); + } - - async getAvailableSlotsForDate( - activityXid: number, - selectedDate: string - ) { - const date = new Date(selectedDate); - - if (isNaN(date.getTime())) { - throw new ApiError(400, 'Invalid date format'); + for (const venue of venues) { + if (!venue.slots || venue.slots.length === 0) { + continue; } - const weekDay = date.toLocaleDateString('en-US', { weekday: 'long' }).toUpperCase(); - const dayOfMonth = date.getDate(); + const header = await tx.scheduleHeader.create({ + data: { + activityXid, + activityVenueXid: venue.venueXid, + scheduleType, + startDate: new Date(dateRange.startDate), + endDate: dateRange.endDate ? new Date(dateRange.endDate) : null, + earlyCheckInMins, + bookingCutOffMins, + isActive: true, + }, + }); - /* -------------------------------- + createdHeaders.push(header.id); + + // WEEKLY + if (scheduleType === SCHEDULING_TYPE.WEEKLY) { + const uniqueWeekdays = [ + ...new Set( + venue.slots + .map((s) => s.weekDay) + .filter( + ( + d, + ): d is + | 'MONDAY' + | 'TUESDAY' + | 'WEDNESDAY' + | 'THURSDAY' + | 'FRIDAY' + | 'SATURDAY' + | 'SUNDAY' => !!d, + ), + ), + ]; + + if (!uniqueWeekdays.length) { + throw new ApiError( + 400, + 'Weekly schedule requires weekDay in slots', + ); + } + + await tx.scheduleRecurrence.createMany({ + data: uniqueWeekdays.map((day) => ({ + scheduleHeaderXid: header.id, + weekDay: day, + isActive: true, + })), + }); + } + + // MONTHLY + if (scheduleType === SCHEDULING_TYPE.MONTHLY) { + const uniqueDays = [ + ...new Set( + venue.slots + .map((s) => s.dayOfMonth) + .filter((d): d is number => d !== null && d !== undefined), + ), + ]; + + if (!uniqueDays.length) { + throw new ApiError( + 400, + 'Monthly schedule requires dayOfMonth in slots', + ); + } + + await tx.scheduleRecurrence.createMany({ + data: uniqueDays.map((day) => ({ + scheduleHeaderXid: header.id, + dayOfMonth: day, + isActive: true, + })), + }); + } + + // CUSTOM / ONCE + if ( + scheduleType === SCHEDULING_TYPE.CUSTOM || + scheduleType === SCHEDULING_TYPE.ONCE + ) { + const uniqueDates = [ + ...new Set( + venue.slots.map((s) => s.occurrenceDate).filter(Boolean), + ), + ]; + + await tx.scheduleOccurences.createMany({ + data: uniqueDates.map((d) => ({ + scheduleHeaderXid: header.id, + occurenceDate: new Date(d!), + isActive: true, + })), + }); + } + + // Slots + for (const slot of venue.slots) { + await tx.scheduleDetails.create({ + data: { + scheduleHeaderXid: header.id, + occurenceDate: slot.occurrenceDate + ? new Date(slot.occurrenceDate) + : null, + weekDay: slot.weekDay ?? null, + dayOfMonth: slot.dayOfMonth ?? null, + startTime: slot.startTime, + endTime: slot.endTime, + maxCapacity: slot.maxCapacity, + isActive: true, + }, + }); + } + } + + return { success: true, scheduleHeaderIds: createdHeaders }; + }); + } + + async getAvailableSlotsForDate(activityXid: number, selectedDate: string) { + const date = new Date(selectedDate); + + if (isNaN(date.getTime())) { + throw new ApiError(400, 'Invalid date format'); + } + + const weekDay = date + .toLocaleDateString('en-US', { weekday: 'long' }) + .toUpperCase(); + const dayOfMonth = date.getDate(); + + /* -------------------------------- 1๏ธโƒฃ FETCH ACTIVE SCHEDULE HEADERS -------------------------------- */ - const scheduleHeaders = await this.prisma.scheduleHeader.findMany({ - where: { - activityXid, - isActive: true, - startDate: { lte: date }, - OR: [ - { endDate: null }, - { endDate: { gte: date } }, - ], - }, - include: { - activityVenue: { - select: { - id: true, - venueName: true, - venueLabel: true, - venueCapacity: true, - }, - }, - scheduleRecurrences: { - where: { isActive: true }, - }, - ScheduleDetails: { - where: { - isActive: true, - OR: [ - { occurenceDate: date }, // ONLY_ONCE / CUSTOM - { weekDay: weekDay }, // WEEKLY - { dayOfMonth: dayOfMonth }, // MONTHLY - ], - }, - }, - Cancellations: { - where: { - occurenceDate: date, - isActive: true, - }, - }, - }, - }); + const scheduleHeaders = await this.prisma.scheduleHeader.findMany({ + where: { + activityXid, + isActive: true, + startDate: { lte: date }, + OR: [{ endDate: null }, { endDate: { gte: date } }], + }, + include: { + activityVenue: { + select: { + id: true, + venueName: true, + venueLabel: true, + venueCapacity: true, + }, + }, + scheduleRecurrences: { + where: { isActive: true }, + }, + ScheduleDetails: { + where: { + isActive: true, + OR: [ + { occurenceDate: date }, // ONLY_ONCE / CUSTOM + { weekDay: weekDay }, // WEEKLY + { dayOfMonth: dayOfMonth }, // MONTHLY + ], + }, + }, + Cancellations: { + where: { + occurenceDate: date, + isActive: true, + }, + }, + }, + }); - if (!scheduleHeaders.length) { - return []; - } + if (!scheduleHeaders.length) { + return []; + } - /* -------------------------------- + /* -------------------------------- 2๏ธโƒฃ BUILD RESPONSE -------------------------------- */ - const response = []; + const response = []; - for (const header of scheduleHeaders) { - const cancelledSlotIds = new Set( - header.Cancellations.map(c => c.slotXid) - ); + for (const header of scheduleHeaders) { + const cancelledSlotIds = new Set( + header.Cancellations.map((c) => c.slotXid), + ); - const slots = header.ScheduleDetails - .filter(slot => !cancelledSlotIds.has(slot.id)) - .map(slot => ({ - slotId: slot.id, - startTime: slot.startTime, - endTime: slot.endTime, - maxCapacity: slot.maxCapacity, - })); + const slots = header.ScheduleDetails.filter( + (slot) => !cancelledSlotIds.has(slot.id), + ).map((slot) => ({ + slotId: slot.id, + startTime: slot.startTime, + endTime: slot.endTime, + maxCapacity: slot.maxCapacity, + })); - if (!slots.length) continue; + if (!slots.length) continue; - response.push({ - venueXid: header.activityVenue.id, - venueName: header.activityVenue.venueName, - venueLabel: header.activityVenue.venueLabel, - slots, - }); - } - - return response; + response.push({ + venueXid: header.activityVenue.id, + venueName: header.activityVenue.venueName, + venueLabel: header.activityVenue.venueLabel, + slots, + }); } - /** - * Return full schedule header + venue + slots for a given activity and date - */ - async getScheduleDetailsForDate( - activityXid: number, - selectedDate: string - ) { - const date = new Date(selectedDate); + return response; + } - if (isNaN(date.getTime())) { - throw new ApiError(400, 'Invalid date format'); + /** + * Return full schedule header + venue + slots for a given activity and date + */ + async getScheduleDetailsForDate(activityXid: number, selectedDate: string) { + const date = new Date(selectedDate); + + if (isNaN(date.getTime())) { + throw new ApiError(400, 'Invalid date format'); + } + + const weekDay = date + .toLocaleDateString('en-US', { weekday: 'long' }) + .toUpperCase(); + const dayOfMonth = date.getDate(); + + const scheduleHeaders = await this.prisma.scheduleHeader.findMany({ + where: { + activityXid, + isActive: true, + startDate: { lte: date }, + OR: [{ endDate: null }, { endDate: { gte: date } }], + }, + include: { + activityVenue: { + select: { + id: true, + venueName: true, + venueLabel: true, + venueCapacity: true, + }, + }, + scheduleRecurrences: { + where: { isActive: true }, + }, + ScheduleDetails: { + where: { + isActive: true, + OR: [ + { occurenceDate: date }, + { weekDay: weekDay }, + { dayOfMonth: dayOfMonth }, + ], + }, + }, + Cancellations: { + where: { + occurenceDate: date, + isActive: true, + }, + }, + }, + }); + + if (!scheduleHeaders.length) return []; + + const response = scheduleHeaders.map((header) => { + const cancelledSlotIds = new Set( + header.Cancellations.map((c) => c.slotXid), + ); + + const slots = header.ScheduleDetails.filter( + (slot) => !cancelledSlotIds.has(slot.id), + ).map((slot) => ({ + slotId: slot.id, + occurenceDate: slot.occurenceDate, + weekDay: slot.weekDay, + dayOfMonth: slot.dayOfMonth, + startTime: slot.startTime, + endTime: slot.endTime, + maxCapacity: slot.maxCapacity, + })); + + return { + scheduleHeaderXid: header.id, + scheduleType: header.scheduleType, + startDate: header.startDate, + endDate: header.endDate, + earlyCheckInMins: header.earlyCheckInMins, + bookingCutOffMins: header.bookingCutOffMins, + activityVenue: { + venueXid: header.activityVenue.id, + venueName: header.activityVenue.venueName, + venueLabel: header.activityVenue.venueLabel, + venueCapacity: header.activityVenue.venueCapacity, + }, + slots, + }; + }); + + return response; + } + + async getVenueFromVenueXid(venueXid: number, activityXid: number) { + return await this.prisma.activityVenues.findUnique({ + where: { id: venueXid, activityXid: activityXid, isActive: true }, + select: { + id: true, + venueName: true, + venueLabel: true, + venueCapacity: true, + }, + }); + } + + async getActivityByXid(activityXid: number) { + return await this.prisma.activities.findUnique({ + where: { id: activityXid, isActive: true }, + select: { + id: true, + activityTitle: true, + }, + }); + } + + /** + * Get activities by status and host ID + * @param hostId - ID of the host + * @param status - Filter by status (Listed, Unlisted, Not_Listed) - optional + * @returns Array of activities matching the criteria + */ + async getActivitiesByStatus(hostId: number, status?: string) { + // Build where clause + const whereClause: any = { + hostXid: hostId, + isActive: true, + deletedAt: null, + activityInternalStatus: { + in: [ + ACTIVITY_INTERNAL_STATUS.ACTIVITY_APPROVED, + ACTIVITY_INTERNAL_STATUS.ACTIVITY_UNLISTED, + ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + ], + }, + }; + + // Add status filter if provided + if (status) { + whereClause.activityInternalStatus = status; + } + + // Query activities + const activities = await this.prisma.activities.findMany({ + where: whereClause, + select: { + id: true, + activityRefNumber: true, + activityTitle: true, + activityDescription: true, + activityDisplayStatus: true, + activityInternalStatus: true, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + ScheduleHeader: { + where: { isActive: true }, + select: { + id: true, + scheduleType: true, + startDate: true, + }, + orderBy: { createdAt: 'desc' }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + for (const activity of activities) { + if (activity.ActivitiesMedia?.length) { + for (const media of activity.ActivitiesMedia) { + if (!media.mediaFileName) continue; + + const key = media.mediaFileName.startsWith('http') + ? media.mediaFileName.split('.com/')[1] + : media.mediaFileName; + + (media as any).presignedUrl = await getPresignedUrl(bucket, key); } + } + } - const weekDay = date.toLocaleDateString('en-US', { weekday: 'long' }).toUpperCase(); - const dayOfMonth = date.getDate(); + // Transform response + return activities.map((activity) => ({ + activityId: activity.id, + activityRefNumber: activity.activityRefNumber, + activityName: activity.activityTitle, + activityDescription: activity.activityDescription, + activityInternalStatus: activity.activityInternalStatus, + activityDisplayStatus: activity.activityDisplayStatus, + media: activity.ActivitiesMedia, + scheduleType: activity.ScheduleHeader?.length + ? activity.ScheduleHeader[0].scheduleType + : null, + scheduleStartDate: activity.ScheduleHeader?.length + ? activity.ScheduleHeader[0].startDate + : null, + })); + } - const scheduleHeaders = await this.prisma.scheduleHeader.findMany({ - where: { - activityXid, - isActive: true, - startDate: { lte: date }, - OR: [ - { endDate: null }, - { endDate: { gte: date } }, - ], - }, - include: { - activityVenue: { - select: { - id: true, - venueName: true, - venueLabel: true, - venueCapacity: true, - }, - }, - scheduleRecurrences: { - where: { isActive: true }, - }, + async getVenueDurationByAct(activityXid: number, hostId: number) { + const result = await this.prisma.activities.findUnique({ + where: { id: activityXid, hostXid: hostId, isActive: true }, + select: { + id: true, + activityDurationMins: true, + activityTitle: true, + activityRefNumber: true, + isLateCheckingAllowed: true, + isInstantBooking: true, + frequenciesXid: true, + frequency: { + where: { isActive: true }, + select: { + id: true, + frequencyName: true, + }, + }, + ActivityVenues: { + where: { isActive: true }, + select: { + id: true, + venueName: true, + venueLabel: true, + ScheduleHeader: { + where: { isActive: true }, + select: { + id: true, + scheduleType: true, + startDate: true, + endDate: true, + earlyCheckInMins: true, + bookingCutOffMins: true, ScheduleDetails: { - where: { - isActive: true, - OR: [ - { occurenceDate: date }, - { weekDay: weekDay }, - { dayOfMonth: dayOfMonth }, - ], - }, + where: { isActive: true }, + select: { + id: true, + occurenceDate: true, + weekDay: true, + dayOfMonth: true, + startTime: true, + endTime: true, + }, }, Cancellations: { - where: { - occurenceDate: date, - isActive: true, + where: { isActive: true }, + select: { + id: true, + slotXid: true, + cancellationReason: true, + slot: { + select: { + id: true, + occurenceDate: true, + startTime: true, + endTime: true, + weekDay: true, + dayOfMonth: true, + }, }, + }, }, + scheduleOccurences: { + where: { isActive: true }, + select: { + id: true, + occurenceDate: true, + }, + }, + scheduleRecurrences: { + where: { isActive: true }, + select: { + id: true, + weekDay: true, + dayOfMonth: true, + }, + }, + }, }, - }); + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + }, + }); - if (!scheduleHeaders.length) return []; + if (!result) return null; - const response = scheduleHeaders.map((header) => { - const cancelledSlotIds = new Set(header.Cancellations.map(c => c.slotXid)); + if (result.ActivitiesMedia?.length) { + for (const media of result.ActivitiesMedia) { + if (!media.mediaFileName) continue; - const slots = header.ScheduleDetails - .filter(slot => !cancelledSlotIds.has(slot.id)) - .map(slot => ({ - slotId: slot.id, - occurenceDate: slot.occurenceDate, - weekDay: slot.weekDay, - dayOfMonth: slot.dayOfMonth, - startTime: slot.startTime, - endTime: slot.endTime, - maxCapacity: slot.maxCapacity, - })); + const key = media.mediaFileName.startsWith('http') + ? media.mediaFileName.split('.com/')[1] + : media.mediaFileName; - return { - scheduleHeaderXid: header.id, - scheduleType: header.scheduleType, - startDate: header.startDate, - endDate: header.endDate, - earlyCheckInMins: header.earlyCheckInMins, - bookingCutOffMins: header.bookingCutOffMins, - activityVenue: { - venueXid: header.activityVenue.id, - venueName: header.activityVenue.venueName, - venueLabel: header.activityVenue.venueLabel, - venueCapacity: header.activityVenue.venueCapacity, - }, - slots, - }; - }); - - return response; + (media as any).presignedUrl = await getPresignedUrl(bucket, key); + } } - async getVenueFromVenueXid(venueXid: number, activityXid: number) { - return await this.prisma.activityVenues.findUnique({ - where: { id: venueXid, activityXid: activityXid, isActive: true }, - select: { - id: true, - venueName: true, - venueLabel: true, - venueCapacity: true, - }, - }); - } - - async getActivityByXid(activityXid: number) { - return await this.prisma.activities.findUnique({ - where: { id: activityXid, isActive: true }, - select: { - id: true, - activityTitle: true, - } - }); - } - - /** - * Get activities by status and host ID - * @param hostId - ID of the host - * @param status - Filter by status (Listed, Unlisted, Not_Listed) - optional - * @returns Array of activities matching the criteria - */ - async getActivitiesByStatus(hostId: number, status?: string) { - // Build where clause - const whereClause: any = { - hostXid: hostId, - isActive: true, - deletedAt: null, - activityInternalStatus: { - in: [ACTIVITY_INTERNAL_STATUS.ACTIVITY_APPROVED, ACTIVITY_INTERNAL_STATUS.ACTIVITY_UNLISTED, ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED] - } - }; - - // Add status filter if provided - if (status) { - whereClause.activityInternalStatus = status; - } - - // Query activities - const activities = await this.prisma.activities.findMany({ - where: whereClause, - select: { - id: true, - activityRefNumber: true, - activityTitle: true, - activityDescription: true, - activityDisplayStatus: true, - activityInternalStatus: true, - ActivitiesMedia: { - where: { isActive: true }, - select: { - id: true, - mediaFileName: true, - mediaType: true, - }, - }, - ScheduleHeader: { - where: { isActive: true }, - select: { - id: true, - scheduleType: true, - startDate: true - }, - orderBy: { createdAt: 'desc' } - } - }, - orderBy: { - createdAt: 'desc', - }, - }); - - for (const activity of activities) { - if (activity.ActivitiesMedia?.length) { - for (const media of activity.ActivitiesMedia) { - if (!media.mediaFileName) continue; - - const key = media.mediaFileName.startsWith('http') - ? media.mediaFileName.split('.com/')[1] - : media.mediaFileName; - - (media as any).presignedUrl = await getPresignedUrl(bucket, key); - } - } - } - - - // Transform response - return activities.map((activity) => ({ - activityId: activity.id, - activityRefNumber: activity.activityRefNumber, - activityName: activity.activityTitle, - activityDescription: activity.activityDescription, - activityInternalStatus: activity.activityInternalStatus, - activityDisplayStatus: activity.activityDisplayStatus, - media: activity.ActivitiesMedia, - scheduleType: activity.ScheduleHeader?.length ? activity.ScheduleHeader[0].scheduleType : null, - scheduleStartDate: activity.ScheduleHeader?.length ? activity.ScheduleHeader[0].startDate : null, - })); - } - - async getVenueDurationByAct(activityXid: number, hostId: number) { - const result = await this.prisma.activities.findUnique({ - where: { id: activityXid, hostXid: hostId, isActive: true }, - select: { - id: true, - activityDurationMins: true, - activityTitle: true, - activityRefNumber: true, - isLateCheckingAllowed: true, - isInstantBooking: true, - frequenciesXid: true, - frequency: { - where: { isActive: true }, - select: { - id: true, - frequencyName: true, - } - }, - ActivityVenues: { - where: { isActive: true }, - select: { - id: true, - venueName: true, - venueLabel: true, - ScheduleHeader: { - where: { isActive: true }, - select: { - id: true, - scheduleType: true, - startDate: true, - endDate: true, - earlyCheckInMins: true, - bookingCutOffMins: true, - ScheduleDetails: { - where: { isActive: true }, - select: { - id: true, - occurenceDate: true, - weekDay: true, - dayOfMonth: true, - startTime: true, - endTime: true, - } - }, - Cancellations: { - where: { isActive: true }, - select: { - id: true, - slotXid: true, - cancellationReason: true, - slot: { - select: { - id: true, - occurenceDate: true, - startTime: true, - endTime: true, - weekDay: true, - dayOfMonth: true, - } - } - } - }, - scheduleOccurences: { - where: { isActive: true }, - select: { - id: true, - occurenceDate: true - } - }, - scheduleRecurrences: { - where: { isActive: true }, - select: { - id: true, - weekDay: true, - dayOfMonth: true - } - } - } - } - } - }, - ActivitiesMedia: { - where: { isActive: true }, - select: { - id: true, - mediaFileName: true, - mediaType: true, - } - } - - } - }) - - if (!result) return null; - - if (result.ActivitiesMedia?.length) { - for (const media of result.ActivitiesMedia) { - if (!media.mediaFileName) continue; - - const key = media.mediaFileName.startsWith('http') - ? media.mediaFileName.split('.com/')[1] - : media.mediaFileName; - - (media as any).presignedUrl = await getPresignedUrl(bucket, key); - } - } - - for (const venue of result.ActivityVenues ?? []) { - for (const header of venue.ScheduleHeader ?? []) { - - /* ------------------------------- + for (const venue of result.ActivityVenues ?? []) { + for (const header of venue.ScheduleHeader ?? []) { + /* ------------------------------- ๐Ÿšซ SLOT CANCELLATION FLAG -------------------------------- */ - const cancelledSlotIds = new Set( - header.Cancellations?.map(c => c.slotXid) - ); + const cancelledSlotIds = new Set( + header.Cancellations?.map((c) => c.slotXid), + ); - for (const slot of header.ScheduleDetails ?? []) { - (slot as any).isCancelled = cancelledSlotIds.has(slot.id); - } + for (const slot of header.ScheduleDetails ?? []) { + (slot as any).isCancelled = cancelledSlotIds.has(slot.id); + } - /* ------------------------------- + /* ------------------------------- ๐Ÿ“… FRONTEND FRIENDLY META -------------------------------- */ - // WEEKLY โ†’ send weekdays - if (header.scheduleType === 'WEEKLY') { - (header as any).scheduleDays = [ - ...new Set( - header.scheduleRecurrences - ?.map(r => r.weekDay) - .filter(Boolean) - ), - ]; - } - - // MONTHLY โ†’ send dates (1โ€“31) - if (header.scheduleType === 'MONTHLY') { - (header as any).scheduleDates = [ - ...new Set( - header.scheduleRecurrences - ?.map(r => r.dayOfMonth) - .filter(Boolean) - ), - ]; - } - - // CUSTOM / ONCE โ†’ send exact dates - if ( - header.scheduleType === 'CUSTOM' || - header.scheduleType === 'ONCE' - ) { - (header as any).scheduleDates = [ - ...new Set( - header.scheduleOccurences - ?.map(o => - o.occurenceDate - ?.toISOString() - .split('T')[0] - ) - .filter(Boolean) - ), - ]; - } - } + // WEEKLY โ†’ send weekdays + if (header.scheduleType === 'WEEKLY') { + (header as any).scheduleDays = [ + ...new Set( + header.scheduleRecurrences?.map((r) => r.weekDay).filter(Boolean), + ), + ]; } - (result as any).availableScheduleTypes = [ - 'ONCE', - 'WEEKLY', - 'MONTHLY', - 'CUSTOM', - ]; + // MONTHLY โ†’ send dates (1โ€“31) + if (header.scheduleType === 'MONTHLY') { + (header as any).scheduleDates = [ + ...new Set( + header.scheduleRecurrences + ?.map((r) => r.dayOfMonth) + .filter(Boolean), + ), + ]; + } - return result; + // CUSTOM / ONCE โ†’ send exact dates + if ( + header.scheduleType === 'CUSTOM' || + header.scheduleType === 'ONCE' + ) { + (header as any).scheduleDates = [ + ...new Set( + header.scheduleOccurences + ?.map((o) => o.occurenceDate?.toISOString().split('T')[0]) + .filter(Boolean), + ), + ]; + } + } } - async cancelMultipleSlotsForActivity(cancellations: { - scheduleHeaderXid: number; - slotXid: number; - cancellationReason?: string; - }[]) { - return await this.prisma.cancellations.createMany({ - data: cancellations.map(item => ({ - scheduleHeaderXid: item.scheduleHeaderXid, - slotXid: item.slotXid, - cancellationReason: item.cancellationReason || 'No reason provided' - })), - skipDuplicates: true - }); - } + (result as any).availableScheduleTypes = [ + 'ONCE', + 'WEEKLY', + 'MONTHLY', + 'CUSTOM', + ]; + return result; + } + async cancelMultipleSlotsForActivity( + cancellations: { + scheduleHeaderXid: number; + slotXid: number; + cancellationReason?: string; + }[], + ) { + return await this.prisma.cancellations.createMany({ + data: cancellations.map((item) => ({ + scheduleHeaderXid: item.scheduleHeaderXid, + slotXid: item.slotXid, + cancellationReason: item.cancellationReason || 'No reason provided', + })), + skipDuplicates: true, + }); + } - async openCanceledSlot( - cancellationXid: number, - slotXid?: number, - ) { - return await this.prisma.cancellations.update({ - where: { - id: cancellationXid, slotXid: slotXid - }, - data: { - isActive: false - } - }) - } -} \ No newline at end of file + async openCanceledSlot( + cancellations: { cancellationXid: number; slotXid: number }[], + ) { + return await this.prisma.cancellations.updateMany({ + where: { + id: { in: cancellations.map((c) => c.cancellationXid) }, + }, + data: { + isActive: false, + }, + }); + } +} diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 90c553c..183b23a 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -1546,16 +1546,21 @@ export class HostService { for (const item of answers) { const question = item.pqqQuestions; - const answer = item.pqqAnswers; + const answer = item.pqqAnswers; // may be null if no answer selected - const maxPoints = question.maxPoints; - const userPoints = answer.answerPoints; + // skip if question missing + if (!question) continue; + + const maxPoints = Number(question.maxPoints || 0); + const userPoints = Number(answer?.answerPoints || 0); totalUserPoints += userPoints; totalMaxPoints += maxPoints; - // Category - const category = question.pqqSubCategories.category; + // Category (guard nested relations) + const category = question.pqqSubCategories?.category; + if (!category) continue; + const categoryId = category.id; if (!categories[categoryId]) {