import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import { 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'; @Injectable() export class SchedulingService { constructor(private prisma: PrismaClient) { } async addSchedulingForActivity(data: ScheduleActivityDTO) { const { activityXid, scheduleType, dateRange, rules, venues, earlyCheckInMins, bookingCutOffMins, } = data; return this.prisma.$transaction(async (tx) => { const createdHeaders: number[] = []; for (const venue of venues) { 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) { await tx.scheduleRecurrence.createMany({ data: rules.weekdays!.map(day => ({ scheduleHeaderXid: header.id, weekDay: day, isActive: true, })), }); } // MONTHLY if (scheduleType === SCHEDULING_TYPE.MONTHLY) { await tx.scheduleRecurrence.createMany({ data: rules.monthDates!.map(day => ({ scheduleHeaderXid: header.id, dayOfMonth: day, isActive: true, })), }); } // CUSTOM / ONCE if (scheduleType === SCHEDULING_TYPE.CUSTOM || scheduleType === SCHEDULING_TYPE.ONCE) { await tx.scheduleOccurences.createMany({ data: rules.customDates!.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, }, }, }, }); if (!scheduleHeaders.length) { return []; } /* -------------------------------- 2️⃣ BUILD RESPONSE -------------------------------- */ const response = []; 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, })); if (!slots.length) continue; response.push({ venueXid: header.activityVenue.id, venueName: header.activityVenue.venueName, venueLabel: header.activityVenue.venueLabel, 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: { select: { id: true, mediaFileName: true, mediaType: true, }, }, }, orderBy: { createdAt: 'desc', }, }); // 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, })); } 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, ActivityVenues: { select: { id: true, venueName: true, venueLabel: true, } } } }) return result; } }