2026-01-28 15:49:19 +05:30
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
|
|
|
|
import { PrismaClient } from '@prisma/client';
|
2026-01-29 17:47:48 +05:30
|
|
|
|
import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, SCHEDULING_TYPE } from '../../../common/utils/constants/host.constant';
|
2026-01-28 15:49:19 +05:30
|
|
|
|
import ApiError from '../../../common/utils/helper/ApiError';
|
2026-01-28 16:24:40 +05:30
|
|
|
|
import { ScheduleActivityDTO } from '../../../common/utils/validation/host/createSchedulingOfAct.validation';
|
2026-01-28 19:12:41 +05:30
|
|
|
|
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
|
|
|
|
|
import config from '../../../config/config';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const bucket = config.aws.bucketName;
|
2026-01-28 15:49:19 +05:30
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class SchedulingService {
|
|
|
|
|
|
constructor(private prisma: PrismaClient) { }
|
|
|
|
|
|
|
2026-02-02 13:33:07 +05:30
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
async addSchedulingForActivity(data: ScheduleActivityDTO) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
activityXid,
|
2026-01-29 17:47:48 +05:30
|
|
|
|
listNow,
|
2026-01-28 15:49:19 +05:30
|
|
|
|
scheduleType,
|
|
|
|
|
|
dateRange,
|
|
|
|
|
|
rules,
|
|
|
|
|
|
venues,
|
|
|
|
|
|
earlyCheckInMins,
|
|
|
|
|
|
bookingCutOffMins,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
isLateCheckingAllowed,
|
|
|
|
|
|
isInstantBooking
|
2026-01-28 15:49:19 +05:30
|
|
|
|
} = data;
|
|
|
|
|
|
|
|
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
|
|
|
|
|
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 headerIds = oldHeaders.map(h => h.id);
|
|
|
|
|
|
|
|
|
|
|
|
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.scheduleOccurences.deleteMany({
|
|
|
|
|
|
where: { scheduleHeaderXid: { in: headerIds } },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await tx.scheduleRecurrence.deleteMany({
|
|
|
|
|
|
where: { scheduleHeaderXid: { in: headerIds } },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await tx.scheduleHeader.deleteMany({
|
|
|
|
|
|
where: { id: { in: headerIds } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----------------------------------
|
|
|
|
|
|
➕ 1️⃣ CREATE NEW SCHEDULING
|
|
|
|
|
|
---------------------------------- */
|
2026-01-28 15:49:19 +05:30
|
|
|
|
const createdHeaders: number[] = [];
|
|
|
|
|
|
|
2026-02-05 16:07:43 +05:30
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
for (const venue of venues) {
|
|
|
|
|
|
|
2026-02-05 16:07:43 +05:30
|
|
|
|
if (!venue.slots || venue.slots.length === 0) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
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) {
|
2026-02-05 16:07:43 +05:30
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
await tx.scheduleRecurrence.createMany({
|
2026-02-05 16:07:43 +05:30
|
|
|
|
data: uniqueWeekdays.map(day => ({
|
2026-01-28 15:49:19 +05:30
|
|
|
|
scheduleHeaderXid: header.id,
|
|
|
|
|
|
weekDay: day,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 16:07:43 +05:30
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
// MONTHLY
|
|
|
|
|
|
if (scheduleType === SCHEDULING_TYPE.MONTHLY) {
|
2026-02-05 16:07:43 +05:30
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
await tx.scheduleRecurrence.createMany({
|
2026-02-05 16:07:43 +05:30
|
|
|
|
data: uniqueDays.map(day => ({
|
2026-01-28 15:49:19 +05:30
|
|
|
|
scheduleHeaderXid: header.id,
|
|
|
|
|
|
dayOfMonth: day,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 16:07:43 +05:30
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
// CUSTOM / ONCE
|
|
|
|
|
|
if (scheduleType === SCHEDULING_TYPE.CUSTOM || scheduleType === SCHEDULING_TYPE.ONCE) {
|
2026-02-05 16:07:43 +05:30
|
|
|
|
const uniqueDates = [
|
|
|
|
|
|
...new Set(
|
|
|
|
|
|
venue.slots
|
|
|
|
|
|
.map(s => s.occurrenceDate)
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
await tx.scheduleOccurences.createMany({
|
2026-02-05 16:07:43 +05:30
|
|
|
|
data: uniqueDates.map(d => ({
|
2026-01-28 15:49:19 +05:30
|
|
|
|
scheduleHeaderXid: header.id,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
occurenceDate: new Date(d!),
|
2026-01-28 15:49:19 +05:30
|
|
|
|
isActive: true,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 16:07:43 +05:30
|
|
|
|
|
2026-01-28 15:49:19 +05:30
|
|
|
|
// 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,
|
|
|
|
|
|
},
|
2026-01-28 16:24:40 +05:30
|
|
|
|
});
|
2026-01-28 15:49:19 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getActivityByXid(activityXid: number) {
|
|
|
|
|
|
return await this.prisma.activities.findUnique({
|
|
|
|
|
|
where: { id: activityXid, isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTitle: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-28 16:24:40 +05:30
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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: {
|
2026-01-28 16:55:14 +05:30
|
|
|
|
in: [ACTIVITY_INTERNAL_STATUS.ACTIVITY_APPROVED, ACTIVITY_INTERNAL_STATUS.ACTIVITY_UNLISTED, ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED]
|
2026-01-28 16:24:40 +05:30
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
2026-01-28 16:24:40 +05:30
|
|
|
|
// Add status filter if provided
|
|
|
|
|
|
if (status) {
|
|
|
|
|
|
whereClause.activityInternalStatus = status;
|
|
|
|
|
|
}
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
2026-01-28 16:24:40 +05:30
|
|
|
|
// 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: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-28 16:24:40 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
mediaType: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-05 16:07:43 +05:30
|
|
|
|
ScheduleHeader: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
scheduleType: true,
|
|
|
|
|
|
startDate: true
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: { createdAt: 'desc' }
|
|
|
|
|
|
}
|
2026-01-28 16:24:40 +05:30
|
|
|
|
},
|
|
|
|
|
|
orderBy: {
|
|
|
|
|
|
createdAt: 'desc',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
2026-01-28 19:12:41 +05:30
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-28 16:24:40 +05:30
|
|
|
|
// 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,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
scheduleType: activity.ScheduleHeader?.length ? activity.ScheduleHeader[0].scheduleType : null,
|
|
|
|
|
|
scheduleStartDate: activity.ScheduleHeader?.length ? activity.ScheduleHeader[0].startDate : null,
|
2026-01-28 16:24:40 +05:30
|
|
|
|
}));
|
|
|
|
|
|
}
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
2026-01-29 17:47:48 +05:30
|
|
|
|
activityTitle: true,
|
|
|
|
|
|
activityRefNumber: true,
|
2026-02-04 15:32:11 +05:30
|
|
|
|
isLateCheckingAllowed: true,
|
2026-01-30 14:49:43 +05:30
|
|
|
|
isInstantBooking: true,
|
|
|
|
|
|
frequenciesXid: true,
|
|
|
|
|
|
frequency: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
frequencyName: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-28 17:58:05 +05:30
|
|
|
|
ActivityVenues: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-28 17:58:05 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
venueName: true,
|
|
|
|
|
|
venueLabel: true,
|
2026-01-29 17:47:48 +05:30
|
|
|
|
ScheduleHeader: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-29 17:47:48 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
scheduleType: true,
|
|
|
|
|
|
startDate: true,
|
|
|
|
|
|
endDate: true,
|
|
|
|
|
|
earlyCheckInMins: true,
|
|
|
|
|
|
bookingCutOffMins: true,
|
|
|
|
|
|
ScheduleDetails: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-29 17:47:48 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
occurenceDate: true,
|
|
|
|
|
|
weekDay: true,
|
|
|
|
|
|
dayOfMonth: true,
|
|
|
|
|
|
startTime: true,
|
|
|
|
|
|
endTime: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
Cancellations: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-29 17:47:48 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
slotXid: true,
|
|
|
|
|
|
cancellationReason: true,
|
|
|
|
|
|
slot: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
occurenceDate: true,
|
|
|
|
|
|
startTime: true,
|
|
|
|
|
|
endTime: true,
|
|
|
|
|
|
weekDay: true,
|
|
|
|
|
|
dayOfMonth: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scheduleOccurences: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-29 17:47:48 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
occurenceDate: true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scheduleRecurrences: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-29 17:47:48 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
weekDay: true,
|
|
|
|
|
|
dayOfMonth: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
ActivitiesMedia: {
|
2026-01-30 11:32:43 +05:30
|
|
|
|
where: { isActive: true },
|
2026-01-29 17:47:48 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
mediaType: true,
|
2026-01-28 17:58:05 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-01-28 17:58:05 +05:30
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
|
|
|
|
|
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 ?? []) {
|
|
|
|
|
|
|
|
|
|
|
|
/* -------------------------------
|
|
|
|
|
|
🚫 SLOT CANCELLATION FLAG
|
|
|
|
|
|
-------------------------------- */
|
|
|
|
|
|
const cancelledSlotIds = new Set(
|
|
|
|
|
|
header.Cancellations?.map(c => c.slotXid)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
(result as any).availableScheduleTypes = [
|
|
|
|
|
|
'ONCE',
|
|
|
|
|
|
'WEEKLY',
|
|
|
|
|
|
'MONTHLY',
|
|
|
|
|
|
'CUSTOM',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-28 17:58:05 +05:30
|
|
|
|
return result;
|
|
|
|
|
|
}
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
|
|
|
|
|
async cancelSlotForActivity(
|
|
|
|
|
|
scheduleHeaderXid: number,
|
|
|
|
|
|
slotXid?: number,
|
|
|
|
|
|
cancellationReason?: string
|
2026-01-29 18:39:01 +05:30
|
|
|
|
) {
|
2026-01-29 17:47:48 +05:30
|
|
|
|
return await this.prisma.cancellations.create({
|
|
|
|
|
|
data: {
|
2026-01-29 18:39:01 +05:30
|
|
|
|
scheduleHeader: {
|
|
|
|
|
|
connect: { id: scheduleHeaderXid },
|
|
|
|
|
|
},
|
|
|
|
|
|
slot: {
|
|
|
|
|
|
connect: { id: slotXid },
|
|
|
|
|
|
},
|
2026-01-29 17:47:48 +05:30
|
|
|
|
cancellationReason
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-01-30 14:49:43 +05:30
|
|
|
|
|
|
|
|
|
|
async openCanceledSlot(
|
|
|
|
|
|
cancellationXid: number,
|
|
|
|
|
|
slotXid?: number,
|
|
|
|
|
|
) {
|
|
|
|
|
|
return await this.prisma.cancellations.update({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: cancellationXid, slotXid: slotXid
|
|
|
|
|
|
},
|
|
|
|
|
|
data: {
|
|
|
|
|
|
isActive: false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-01-28 15:49:19 +05:30
|
|
|
|
}
|