2026-01-28 15:49:19 +05:30
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
|
|
|
|
import { PrismaClient } from '@prisma/client';
|
2026-02-09 15:34:13 +05:30
|
|
|
|
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
2026-02-26 11:05:56 +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 config from '../../../config/config';
|
|
|
|
|
|
|
|
|
|
|
|
const bucket = config.aws.bucketName;
|
2026-01-28 15:49:19 +05:30
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class SchedulingService {
|
2026-02-26 18:55:34 +05:30
|
|
|
|
constructor(private prisma: PrismaClient) { }
|
2026-02-26 11:05:56 +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.');
|
2026-02-02 13:33:07 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
return host.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async addSchedulingForActivity(data: ScheduleActivityDTO) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
listNow,
|
|
|
|
|
|
scheduleType,
|
|
|
|
|
|
dateRange,
|
|
|
|
|
|
rules,
|
|
|
|
|
|
venues,
|
|
|
|
|
|
earlyCheckInMins,
|
|
|
|
|
|
bookingCutOffMins,
|
|
|
|
|
|
isLateCheckingAllowed,
|
|
|
|
|
|
isInstantBooking,
|
|
|
|
|
|
} = data;
|
|
|
|
|
|
|
|
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
|
|
|
|
const venueXids = venues.map((v) => v.venueXid);
|
|
|
|
|
|
|
|
|
|
|
|
/* ----------------------------------
|
2026-01-29 17:47:48 +05:30
|
|
|
|
🧹 0️⃣ DELETE OLD SCHEDULING (PER ACTIVITY + VENUES)
|
|
|
|
|
|
---------------------------------- */
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
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 } },
|
|
|
|
|
|
});
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
await tx.scheduleDetails.deleteMany({
|
|
|
|
|
|
where: { scheduleHeaderXid: { in: headerIds } },
|
|
|
|
|
|
});
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
await tx.scheduleOccurences.deleteMany({
|
|
|
|
|
|
where: { scheduleHeaderXid: { in: headerIds } },
|
|
|
|
|
|
});
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
await tx.scheduleRecurrence.deleteMany({
|
|
|
|
|
|
where: { scheduleHeaderXid: { in: headerIds } },
|
|
|
|
|
|
});
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
await tx.scheduleHeader.deleteMany({
|
|
|
|
|
|
where: { id: { in: headerIds } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
/* ----------------------------------
|
2026-01-29 17:47:48 +05:30
|
|
|
|
➕ 1️⃣ CREATE NEW SCHEDULING
|
|
|
|
|
|
---------------------------------- */
|
2026-02-26 11:05:56 +05:30
|
|
|
|
const createdHeaders: number[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
isInstantBooking !== undefined ||
|
|
|
|
|
|
isLateCheckingAllowed !== undefined
|
|
|
|
|
|
) {
|
|
|
|
|
|
await tx.activities.update({
|
|
|
|
|
|
where: { id: activityXid, isActive: true },
|
|
|
|
|
|
data: { isInstantBooking, isLateCheckingAllowed },
|
2026-01-28 15:49:19 +05:30
|
|
|
|
});
|
2026-02-26 11:05:56 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
for (const venue of venues) {
|
|
|
|
|
|
if (!venue.slots || venue.slots.length === 0) {
|
|
|
|
|
|
continue;
|
2026-01-28 15:49:19 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 11:05:56 +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,
|
|
|
|
|
|
},
|
2026-01-28 15:49:19 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
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',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-28 15:49:19 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
await tx.scheduleRecurrence.createMany({
|
|
|
|
|
|
data: uniqueWeekdays.map((day) => ({
|
|
|
|
|
|
scheduleHeaderXid: header.id,
|
|
|
|
|
|
weekDay: day,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-28 15:49:19 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// 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',
|
2026-01-28 15:49:19 +05:30
|
|
|
|
);
|
2026-02-26 11:05:56 +05:30
|
|
|
|
}
|
2026-01-28 15:49:19 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
await tx.scheduleRecurrence.createMany({
|
|
|
|
|
|
data: uniqueDays.map((day) => ({
|
|
|
|
|
|
scheduleHeaderXid: header.id,
|
|
|
|
|
|
dayOfMonth: day,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
2026-01-28 15:49:19 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// 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,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
2026-02-09 15:34:13 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 11:05:56 +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,
|
2026-02-09 15:34:13 +05:30
|
|
|
|
},
|
2026-02-26 11:05:56 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-09 15:34:13 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
return { success: true, scheduleHeaderIds: createdHeaders };
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-02-09 15:34:13 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
async getAvailableSlotsForDate(activityXid: number, selectedDate: string) {
|
|
|
|
|
|
const date = new Date(selectedDate);
|
2026-02-09 15:34:13 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
if (isNaN(date.getTime())) {
|
|
|
|
|
|
throw new ApiError(400, 'Invalid date format');
|
2026-01-28 15:49:19 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
const weekDay = date
|
|
|
|
|
|
.toLocaleDateString('en-US', { weekday: 'long' })
|
|
|
|
|
|
.toUpperCase();
|
|
|
|
|
|
const dayOfMonth = date.getDate();
|
2026-01-28 16:24:40 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
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: {
|
2026-01-28 16:24:40 +05:30
|
|
|
|
isActive: true,
|
2026-02-26 11:05:56 +05:30
|
|
|
|
OR: [
|
|
|
|
|
|
{ occurenceDate: date }, // ONLY_ONCE / CUSTOM
|
|
|
|
|
|
{ weekDay: weekDay }, // WEEKLY
|
|
|
|
|
|
{ dayOfMonth: dayOfMonth }, // MONTHLY
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
Cancellations: {
|
|
|
|
|
|
where: {
|
|
|
|
|
|
occurenceDate: date,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
if (!scheduleHeaders.length) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
2️⃣ BUILD RESPONSE
|
|
|
|
|
|
-------------------------------- */
|
|
|
|
|
|
const response = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const header of scheduleHeaders) {
|
2026-02-26 18:55:34 +05:30
|
|
|
|
|
|
|
|
|
|
// Build cancellation set using time matching
|
|
|
|
|
|
const cancelledSlots = new Set(
|
|
|
|
|
|
header.Cancellations.map(
|
|
|
|
|
|
(c) => `${c.startTime}-${c.endTime}`
|
|
|
|
|
|
)
|
2026-02-26 11:05:56 +05:30
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-26 18:55:34 +05:30
|
|
|
|
const slots = header.ScheduleDetails
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
(slot) =>
|
|
|
|
|
|
!cancelledSlots.has(`${slot.startTime}-${slot.endTime}`)
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((slot) => ({
|
|
|
|
|
|
slotId: slot.id,
|
|
|
|
|
|
startTime: slot.startTime,
|
|
|
|
|
|
endTime: slot.endTime,
|
|
|
|
|
|
maxCapacity: slot.maxCapacity,
|
|
|
|
|
|
}));
|
2026-02-26 11:05:56 +05:30
|
|
|
|
|
|
|
|
|
|
if (!slots.length) continue;
|
|
|
|
|
|
|
|
|
|
|
|
response.push({
|
|
|
|
|
|
venueXid: header.activityVenue.id,
|
|
|
|
|
|
venueName: header.activityVenue.venueName,
|
|
|
|
|
|
venueLabel: header.activityVenue.venueLabel,
|
2026-02-26 18:55:34 +05:30
|
|
|
|
venueCapacity: header.activityVenue.venueCapacity,
|
2026-02-26 11:05:56 +05:30
|
|
|
|
slots,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-28 19:12:41 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
return response;
|
|
|
|
|
|
}
|
2026-01-28 19:12:41 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
/**
|
|
|
|
|
|
* Return full schedule header + venue + slots for a given activity and date
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getScheduleDetailsForDate(activityXid: number, selectedDate: string) {
|
|
|
|
|
|
const date = new Date(selectedDate);
|
2026-01-28 19:12:41 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
if (isNaN(date.getTime())) {
|
|
|
|
|
|
throw new ApiError(400, 'Invalid date format');
|
|
|
|
|
|
}
|
2026-01-28 19:12:41 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
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 } }],
|
2026-03-06 15:40:21 +05:30
|
|
|
|
ScheduleDetails: {
|
|
|
|
|
|
some: {}
|
|
|
|
|
|
}
|
2026-02-26 11:05:56 +05:30
|
|
|
|
},
|
|
|
|
|
|
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) => {
|
2026-02-26 18:55:34 +05:30
|
|
|
|
|
|
|
|
|
|
// Match cancelled slots using startTime + endTime
|
|
|
|
|
|
const cancelledSlots = new Set(
|
|
|
|
|
|
header.Cancellations.map(
|
|
|
|
|
|
(c) => `${c.startTime}-${c.endTime}`
|
|
|
|
|
|
)
|
2026-02-26 11:05:56 +05:30
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-26 18:55:34 +05:30
|
|
|
|
const slots = header.ScheduleDetails
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
(slot) =>
|
|
|
|
|
|
!cancelledSlots.has(`${slot.startTime}-${slot.endTime}`)
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((slot) => ({
|
|
|
|
|
|
slotId: slot.id,
|
|
|
|
|
|
occurenceDate: slot.occurenceDate,
|
|
|
|
|
|
weekDay: slot.weekDay,
|
|
|
|
|
|
dayOfMonth: slot.dayOfMonth,
|
|
|
|
|
|
startTime: slot.startTime,
|
|
|
|
|
|
endTime: slot.endTime,
|
|
|
|
|
|
maxCapacity: slot.maxCapacity,
|
|
|
|
|
|
}));
|
2026-02-26 11:05:56 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
2026-02-26 18:55:34 +05:30
|
|
|
|
slots, // only active slots, no cancellation flag
|
2026-02-26 11:05:56 +05:30
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-01-28 16:24:40 +05:30
|
|
|
|
}
|
2026-01-28 17:58:05 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +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: {
|
|
|
|
|
|
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: {
|
2026-01-28 17:58:05 +05:30
|
|
|
|
id: true,
|
2026-02-26 11:05:56 +05:30
|
|
|
|
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,
|
|
|
|
|
|
},
|
2026-01-30 14:49:43 +05:30
|
|
|
|
},
|
2026-02-26 11:05:56 +05:30
|
|
|
|
Cancellations: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
cancellationReason: true,
|
2026-02-26 18:55:34 +05:30
|
|
|
|
occurenceDate: true,
|
|
|
|
|
|
startTime: true,
|
|
|
|
|
|
endTime: true,
|
2026-02-26 11:05:56 +05:30
|
|
|
|
},
|
2026-01-29 17:47:48 +05:30
|
|
|
|
},
|
2026-02-26 11:05:56 +05:30
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
for (const venue of result.ActivityVenues ?? []) {
|
|
|
|
|
|
for (const header of venue.ScheduleHeader ?? []) {
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
/* -------------------------------
|
2026-01-29 17:47:48 +05:30
|
|
|
|
📅 FRONTEND FRIENDLY META
|
|
|
|
|
|
-------------------------------- */
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// WEEKLY → send weekdays
|
|
|
|
|
|
if (header.scheduleType === 'WEEKLY') {
|
|
|
|
|
|
(header as any).scheduleDays = [
|
|
|
|
|
|
...new Set(
|
|
|
|
|
|
header.scheduleRecurrences?.map((r) => r.weekDay).filter(Boolean),
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
2026-01-29 17:47:48 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// MONTHLY → send dates (1–31)
|
|
|
|
|
|
if (header.scheduleType === 'MONTHLY') {
|
|
|
|
|
|
(header as any).scheduleDates = [
|
|
|
|
|
|
...new Set(
|
|
|
|
|
|
header.scheduleRecurrences
|
|
|
|
|
|
?.map((r) => r.dayOfMonth)
|
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2026-01-29 17:47:48 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// 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),
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-29 17:47:48 +05:30
|
|
|
|
}
|
2026-01-30 14:49:43 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
(result as any).availableScheduleTypes = [
|
|
|
|
|
|
'ONCE',
|
|
|
|
|
|
'WEEKLY',
|
|
|
|
|
|
'MONTHLY',
|
|
|
|
|
|
'CUSTOM',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async cancelMultipleSlotsForActivity(
|
|
|
|
|
|
cancellations: {
|
|
|
|
|
|
scheduleHeaderXid: number;
|
2026-02-26 18:55:34 +05:30
|
|
|
|
occurenceDate: string;
|
|
|
|
|
|
startTime: string;
|
|
|
|
|
|
endTime: string;
|
2026-02-26 11:05:56 +05:30
|
|
|
|
cancellationReason?: string;
|
|
|
|
|
|
}[],
|
|
|
|
|
|
) {
|
|
|
|
|
|
return await this.prisma.cancellations.createMany({
|
|
|
|
|
|
data: cancellations.map((item) => ({
|
|
|
|
|
|
scheduleHeaderXid: item.scheduleHeaderXid,
|
2026-02-26 18:55:34 +05:30
|
|
|
|
occurenceDate: item.occurenceDate,
|
|
|
|
|
|
startTime: item.startTime,
|
|
|
|
|
|
endTime: item.endTime,
|
2026-02-26 11:05:56 +05:30
|
|
|
|
cancellationReason: item.cancellationReason || 'No reason provided',
|
|
|
|
|
|
})),
|
|
|
|
|
|
skipDuplicates: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async openCanceledSlot(
|
2026-02-26 19:01:59 +05:30
|
|
|
|
cancellations: { cancellationXid: number; }[],
|
2026-02-26 11:05:56 +05:30
|
|
|
|
) {
|
|
|
|
|
|
return await this.prisma.cancellations.updateMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: { in: cancellations.map((c) => c.cancellationXid) },
|
|
|
|
|
|
},
|
|
|
|
|
|
data: {
|
|
|
|
|
|
isActive: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|