287 lines
9.7 KiB
TypeScript
287 lines
9.7 KiB
TypeScript
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;
|
||
}
|
||
} |