made getMatchingBucketInterestedActivities api
This commit is contained in:
@@ -34,7 +34,7 @@ const bucket = config.aws.bucketName;
|
||||
|
||||
@Injectable()
|
||||
export class MinglarService {
|
||||
constructor(private prisma: PrismaService | PrismaClient) {}
|
||||
constructor(private prisma: PrismaService | PrismaClient) { }
|
||||
|
||||
async createPassword(user_xid: number, password: string): Promise<boolean> {
|
||||
// Find user by id
|
||||
@@ -314,6 +314,8 @@ export class MinglarService {
|
||||
companyName: true,
|
||||
user: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
userRefNumber: true,
|
||||
},
|
||||
},
|
||||
@@ -375,11 +377,52 @@ export class MinglarService {
|
||||
const {
|
||||
paginationService,
|
||||
} = require('@/common/utils/pagination/pagination.service');
|
||||
return paginationService.createPaginatedResponse(
|
||||
|
||||
let hostDetails = null;
|
||||
|
||||
if (hostXid) {
|
||||
hostDetails = await this.prisma.hostHeader.findUnique({
|
||||
where: { id: hostXid },
|
||||
select: {
|
||||
companyName: true,
|
||||
user: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
userRefNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const paginatedResponse = paginationService.createPaginatedResponse(
|
||||
hostActivities,
|
||||
totalCount,
|
||||
paginationOptions || { page: 1, limit: 10, skip: 0 },
|
||||
);
|
||||
|
||||
// 👇 ADD THIS BLOCK
|
||||
if (hostActivities.length === 0 && hostDetails) {
|
||||
paginatedResponse.data = [
|
||||
{
|
||||
id: null,
|
||||
activityRefNumber: null,
|
||||
activityTitle: null,
|
||||
totalScore: null,
|
||||
activityInternalStatus: null,
|
||||
activityDisplayStatus: null,
|
||||
amInternalStatus: null,
|
||||
amDisplayStatus: null,
|
||||
createdAt: null,
|
||||
host: hostDetails,
|
||||
ActivityAmDetails: [],
|
||||
activityType: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return paginatedResponse;
|
||||
}
|
||||
|
||||
async createUserRevenue(
|
||||
@@ -818,7 +861,7 @@ export class MinglarService {
|
||||
if (
|
||||
userStatus &&
|
||||
userStatus.trim().toLowerCase() ===
|
||||
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
|
||||
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
|
||||
) {
|
||||
filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
|
||||
}
|
||||
@@ -1188,15 +1231,15 @@ export class MinglarService {
|
||||
// Build search filter if search term is provided
|
||||
const searchFilter = search
|
||||
? {
|
||||
OR: [
|
||||
{ email: { contains: search, mode: 'insensitive' as const } },
|
||||
{ firstName: { contains: search, mode: 'insensitive' as const } },
|
||||
{ lastName: { contains: search, mode: 'insensitive' as const } },
|
||||
{
|
||||
userRefNumber: { contains: search, mode: 'insensitive' as const },
|
||||
},
|
||||
],
|
||||
}
|
||||
OR: [
|
||||
{ email: { contains: search, mode: 'insensitive' as const } },
|
||||
{ firstName: { contains: search, mode: 'insensitive' as const } },
|
||||
{ lastName: { contains: search, mode: 'insensitive' as const } },
|
||||
{
|
||||
userRefNumber: { contains: search, mode: 'insensitive' as const },
|
||||
},
|
||||
],
|
||||
}
|
||||
: {};
|
||||
|
||||
// 1. Fetch all required users (Admin, Co-Admin, AM)
|
||||
@@ -1827,8 +1870,8 @@ export class MinglarService {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async rejectActivityApplicationByAM(activityId: number, user_xid: number) {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
await tx.activities.update({
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { ItineraryService } from '../../services/itinerary.service';
|
||||
|
||||
const itineraryService = new ItineraryService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
|
||||
if (!userId || Number.isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user ID');
|
||||
}
|
||||
|
||||
let body: Record<string, any> = {};
|
||||
if (event.body) {
|
||||
try {
|
||||
body = JSON.parse(event.body);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON body');
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
userLat: Number(body.userLat),
|
||||
userLong: Number(body.userLong),
|
||||
startDate: body.startDate,
|
||||
endDate: body.endDate,
|
||||
startTime: body.startTime,
|
||||
endTime: body.endTime,
|
||||
energyLevelXid: Number(body.energyLevelXid),
|
||||
entryTypeXid: Number(body.entryTypeXid),
|
||||
page: body.page !== undefined ? Number(body.page) : 1,
|
||||
limit: body.limit !== undefined ? Number(body.limit) : 20,
|
||||
};
|
||||
|
||||
if (
|
||||
Number.isNaN(payload.userLat) ||
|
||||
Number.isNaN(payload.userLong) ||
|
||||
!payload.startDate ||
|
||||
!payload.endDate ||
|
||||
!payload.startTime ||
|
||||
!payload.endTime ||
|
||||
Number.isNaN(payload.energyLevelXid) ||
|
||||
Number.isNaN(payload.entryTypeXid) ||
|
||||
Number.isNaN(payload.page) ||
|
||||
Number.isNaN(payload.limit)
|
||||
) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'userLat, userLong, startDate, endDate, startTime, endTime, energyLevelXid, entryTypeXid, page and limit are required.',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await itineraryService.getMatchingBucketInterestedActivities(
|
||||
userId,
|
||||
payload,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Matching itinerary activities retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import {
|
||||
ACTIVITY_AM_INTERNAL_STATUS,
|
||||
ACTIVITY_INTERNAL_STATUS,
|
||||
@@ -9,6 +10,15 @@ import {
|
||||
import config from '@/config/config';
|
||||
|
||||
const bucket = config.aws.bucketName;
|
||||
const WEEKDAY_NAMES = [
|
||||
'SUNDAY',
|
||||
'MONDAY',
|
||||
'TUESDAY',
|
||||
'WEDNESDAY',
|
||||
'THURSDAY',
|
||||
'FRIDAY',
|
||||
'SATURDAY',
|
||||
] as const;
|
||||
|
||||
const attachPresignedUrl = async (file: string | null | undefined) => {
|
||||
if (!file) return null;
|
||||
@@ -41,6 +51,140 @@ const attachMediaWithPresignedUrl = async (
|
||||
);
|
||||
};
|
||||
|
||||
const calculateDistance = (
|
||||
lat1: number | null,
|
||||
lon1: number | null,
|
||||
lat2: number | null,
|
||||
lon2: number | null,
|
||||
) => {
|
||||
if (
|
||||
lat1 === null ||
|
||||
lon1 === null ||
|
||||
lat2 === null ||
|
||||
lon2 === null ||
|
||||
Number.isNaN(lat1) ||
|
||||
Number.isNaN(lon1) ||
|
||||
Number.isNaN(lat2) ||
|
||||
Number.isNaN(lon2)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const R = 6371;
|
||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos((lat1 * Math.PI) / 180) *
|
||||
Math.cos((lat2 * Math.PI) / 180) *
|
||||
Math.sin(dLon / 2) *
|
||||
Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return Number((R * c).toFixed(2));
|
||||
};
|
||||
|
||||
const parseDateValue = (value: string | Date) => {
|
||||
if (value instanceof Date) {
|
||||
return new Date(value.getTime());
|
||||
}
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
const isoMatch = trimmedValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (isoMatch) {
|
||||
const [, year, month, day] = isoMatch;
|
||||
return new Date(Number(year), Number(month) - 1, Number(day));
|
||||
}
|
||||
|
||||
const slashMatch = trimmedValue.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
|
||||
if (slashMatch) {
|
||||
const [, year, month, day] = slashMatch;
|
||||
return new Date(Number(year), Number(month) - 1, Number(day));
|
||||
}
|
||||
|
||||
const parsed = new Date(trimmedValue);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const parseTimeValue = (value: string) => {
|
||||
const trimmedValue = value.trim().toUpperCase().replace(/\s+/g, ' ');
|
||||
const match = trimmedValue.match(
|
||||
/^(\d{1,2})(?::(\d{2}))?(?::(\d{2}))?\s*(AM|PM)?$/,
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let hours = Number(match[1]);
|
||||
const minutes = Number(match[2] ?? '0');
|
||||
const seconds = Number(match[3] ?? '0');
|
||||
const meridiem = match[4];
|
||||
|
||||
if (minutes > 59 || seconds > 59) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (meridiem) {
|
||||
if (hours < 1 || hours > 12) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (meridiem === 'AM') {
|
||||
hours = hours === 12 ? 0 : hours;
|
||||
} else {
|
||||
hours = hours === 12 ? 12 : hours + 12;
|
||||
}
|
||||
} else if (hours > 23) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { hours, minutes, seconds };
|
||||
};
|
||||
|
||||
const combineDateAndTime = (dateValue: string | Date, timeValue: string) => {
|
||||
const date = parseDateValue(dateValue);
|
||||
const time = parseTimeValue(timeValue);
|
||||
|
||||
if (Number.isNaN(date.getTime()) || !time) {
|
||||
return null;
|
||||
}
|
||||
|
||||
date.setHours(time.hours, time.minutes, time.seconds, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
const startOfDay = (date: Date) => {
|
||||
const value = new Date(date.getTime());
|
||||
value.setHours(0, 0, 0, 0);
|
||||
return value;
|
||||
};
|
||||
|
||||
const formatDateKey = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const addMinutes = (date: Date, minutes: number) =>
|
||||
new Date(date.getTime() + minutes * 60 * 1000);
|
||||
|
||||
const getDateRange = (fromDate: Date, toDate: Date) => {
|
||||
const dates: Date[] = [];
|
||||
const cursor = startOfDay(fromDate);
|
||||
const end = startOfDay(toDate);
|
||||
|
||||
while (cursor <= end) {
|
||||
dates.push(new Date(cursor.getTime()));
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
return dates;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ItineraryService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
@@ -204,4 +348,440 @@ export class ItineraryService {
|
||||
interestedActivities: formattedActivities.filter((item) => !item.isBucket),
|
||||
};
|
||||
}
|
||||
|
||||
async getMatchingBucketInterestedActivities(
|
||||
userXid: number,
|
||||
payload: {
|
||||
userLat: number;
|
||||
userLong: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
energyLevelXid: number;
|
||||
entryTypeXid: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
},
|
||||
) {
|
||||
const requestedStart = combineDateAndTime(
|
||||
payload.startDate,
|
||||
payload.startTime,
|
||||
);
|
||||
const requestedEnd = combineDateAndTime(payload.endDate, payload.endTime);
|
||||
|
||||
if (!requestedStart || !requestedEnd) {
|
||||
throw new ApiError(400, 'Invalid start/end date or time values.');
|
||||
}
|
||||
|
||||
if (requestedStart >= requestedEnd) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'Start date and time must be earlier than end date and time.',
|
||||
);
|
||||
}
|
||||
|
||||
const rangeStartDay = startOfDay(requestedStart);
|
||||
const rangeEndDay = startOfDay(requestedEnd);
|
||||
|
||||
const activityEntries = await this.prisma.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
Activities: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityType: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
energyLevelXid: payload.energyLevelXid,
|
||||
},
|
||||
ActivityAllowedEntry: {
|
||||
some: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
allowedEntryTypeXid: payload.entryTypeXid,
|
||||
},
|
||||
},
|
||||
ActivityVenues: {
|
||||
some: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
ScheduleHeader: {
|
||||
some: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
startDate: { lte: rangeEndDay },
|
||||
OR: [
|
||||
{ endDate: null },
|
||||
{ endDate: { gte: rangeStartDay } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isBucket: true,
|
||||
bucketTypeName: true,
|
||||
activityXid: true,
|
||||
Activities: {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
activityDurationMins: true,
|
||||
checkInLat: true,
|
||||
checkInLong: true,
|
||||
checkInAddress: true,
|
||||
activityType: {
|
||||
select: {
|
||||
id: true,
|
||||
energyLevelXid: true,
|
||||
energyLevel: {
|
||||
select: {
|
||||
id: true,
|
||||
energyLevelName: true,
|
||||
energyIcon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivityAllowedEntry: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
allowedEntryTypeXid: payload.entryTypeXid,
|
||||
},
|
||||
select: {
|
||||
allowedEntryTypeXid: true,
|
||||
allowedEntryType: {
|
||||
select: {
|
||||
id: true,
|
||||
allowedEntryTypeName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivitiesMedia: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
mediaType: true,
|
||||
mediaFileName: true,
|
||||
isCoverImage: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
},
|
||||
ActivityVenues: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
ScheduleHeader: {
|
||||
some: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
startDate: { lte: rangeEndDay },
|
||||
OR: [
|
||||
{ endDate: null },
|
||||
{ endDate: { gte: rangeStartDay } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
venueName: true,
|
||||
venueLabel: true,
|
||||
venueCapacity: true,
|
||||
availableSeats: true,
|
||||
ScheduleHeader: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
startDate: { lte: rangeEndDay },
|
||||
OR: [
|
||||
{ endDate: null },
|
||||
{ endDate: { gte: rangeStartDay } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
scheduleType: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
ScheduleDetails: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
maxCapacity: { gt: 0 },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
occurenceDate: true,
|
||||
weekDay: true,
|
||||
dayOfMonth: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
maxCapacity: true,
|
||||
},
|
||||
},
|
||||
Cancellations: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
occurenceDate: {
|
||||
gte: rangeStartDay,
|
||||
lte: rangeEndDay,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
occurenceDate: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const formattedActivities = await Promise.all(
|
||||
activityEntries.map(async (entry) => {
|
||||
const activity = entry.Activities;
|
||||
const activityDurationMins = activity?.activityDurationMins ?? 0;
|
||||
const distance = calculateDistance(
|
||||
payload.userLat,
|
||||
payload.userLong,
|
||||
activity?.checkInLat ?? null,
|
||||
activity?.checkInLong ?? null,
|
||||
);
|
||||
|
||||
const availableSlots = activity.ActivityVenues.flatMap((venue) =>
|
||||
venue.ScheduleHeader.flatMap((header) => {
|
||||
const effectiveRangeStart =
|
||||
header.startDate > rangeStartDay ? header.startDate : rangeStartDay;
|
||||
const headerEndDate = header.endDate ?? rangeEndDay;
|
||||
const effectiveRangeEnd =
|
||||
headerEndDate < rangeEndDay ? headerEndDate : rangeEndDay;
|
||||
|
||||
if (effectiveRangeStart > effectiveRangeEnd) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cancelledSlots = new Set(
|
||||
header.Cancellations.map((cancellation) => {
|
||||
if (!cancellation.occurenceDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${formatDateKey(cancellation.occurenceDate)}|${cancellation.startTime}|${cancellation.endTime}`;
|
||||
}).filter(Boolean) as string[],
|
||||
);
|
||||
|
||||
return header.ScheduleDetails.flatMap((slot) => {
|
||||
const slotDates: Date[] = [];
|
||||
|
||||
if (slot.occurenceDate) {
|
||||
const occurrenceDay = startOfDay(slot.occurenceDate);
|
||||
if (
|
||||
occurrenceDay >= startOfDay(effectiveRangeStart) &&
|
||||
occurrenceDay <= startOfDay(effectiveRangeEnd)
|
||||
) {
|
||||
slotDates.push(occurrenceDay);
|
||||
}
|
||||
} else {
|
||||
for (const currentDate of getDateRange(
|
||||
effectiveRangeStart,
|
||||
effectiveRangeEnd,
|
||||
)) {
|
||||
const weekDayName = WEEKDAY_NAMES[currentDate.getDay()];
|
||||
|
||||
if (slot.weekDay && slot.weekDay !== weekDayName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
slot.dayOfMonth !== null &&
|
||||
slot.dayOfMonth !== undefined &&
|
||||
slot.dayOfMonth !== currentDate.getDate()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
slotDates.push(currentDate);
|
||||
}
|
||||
}
|
||||
|
||||
return slotDates
|
||||
.map((slotDate) => {
|
||||
const slotStart = combineDateAndTime(slotDate, slot.startTime);
|
||||
if (!slotStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slotEnd = activityDurationMins
|
||||
? addMinutes(slotStart, activityDurationMins)
|
||||
: combineDateAndTime(slotDate, slot.endTime);
|
||||
|
||||
if (!slotEnd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cancellationKey = `${formatDateKey(slotDate)}|${slot.startTime}|${slot.endTime}`;
|
||||
|
||||
if (cancelledSlots.has(cancellationKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (slotStart < requestedStart || slotEnd > requestedEnd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scheduleHeaderXid: header.id,
|
||||
slotId: slot.id,
|
||||
venueXid: venue.id,
|
||||
venueName: venue.venueName,
|
||||
venueLabel: venue.venueLabel,
|
||||
venueCapacity: venue.venueCapacity,
|
||||
availableSeats: venue.availableSeats,
|
||||
slotDate: formatDateKey(slotDate),
|
||||
startTime: slot.startTime,
|
||||
endTime: slotEnd.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
}),
|
||||
startDateTime: slotStart.toISOString(),
|
||||
endDateTime: slotEnd.toISOString(),
|
||||
maxCapacity: slot.maxCapacity,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (!availableSlots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
availableSlots.sort(
|
||||
(first, second) =>
|
||||
new Date(first!.startDateTime).getTime() -
|
||||
new Date(second!.startDateTime).getTime(),
|
||||
);
|
||||
|
||||
const coverImage =
|
||||
activity.ActivitiesMedia.find((media) => media.isCoverImage) ??
|
||||
activity.ActivitiesMedia[0] ??
|
||||
null;
|
||||
|
||||
const energyLevel = activity.activityType?.energyLevel ?? null;
|
||||
|
||||
return {
|
||||
userBucketInterestedXid: entry.id,
|
||||
activityXid: entry.activityXid,
|
||||
isBucket: entry.isBucket,
|
||||
bucketTypeName: entry.bucketTypeName,
|
||||
distance,
|
||||
activityTitle: activity.activityTitle,
|
||||
activityDurationMins,
|
||||
activityCoverImage: coverImage?.mediaFileName ?? null,
|
||||
activityCoverImagePresignedUrl: await attachPresignedUrl(
|
||||
coverImage?.mediaFileName,
|
||||
),
|
||||
venue: availableSlots[0]
|
||||
? {
|
||||
venueXid: availableSlots[0].venueXid,
|
||||
venueName: availableSlots[0].venueName,
|
||||
venueLabel: availableSlots[0].venueLabel,
|
||||
}
|
||||
: null,
|
||||
availableSlots,
|
||||
entryType: activity.ActivityAllowedEntry[0]?.allowedEntryType ?? null,
|
||||
energyLevel: energyLevel
|
||||
? {
|
||||
energyLevelXid: energyLevel.id,
|
||||
energyLevelName: energyLevel.energyLevelName,
|
||||
energyLevelIcon: energyLevel.energyIcon,
|
||||
energyLevelIconPresignedUrl: await attachPresignedUrl(
|
||||
energyLevel.energyIcon,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
checkInAddress: activity.checkInAddress,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const activities = formattedActivities
|
||||
.filter(Boolean)
|
||||
.sort((first, second) => {
|
||||
const firstDistance =
|
||||
first!.distance === null ? Number.POSITIVE_INFINITY : first!.distance;
|
||||
const secondDistance =
|
||||
second!.distance === null ? Number.POSITIVE_INFINITY : second!.distance;
|
||||
|
||||
if (firstDistance !== secondDistance) {
|
||||
return firstDistance - secondDistance;
|
||||
}
|
||||
|
||||
return first!.activityXid - second!.activityXid;
|
||||
});
|
||||
|
||||
const totalCount = activities.length;
|
||||
const sanitizedLimit = Math.min(Math.max(payload.limit, 1), 20);
|
||||
const sanitizedPage = Math.max(payload.page, 1);
|
||||
const totalPages = totalCount ? Math.ceil(totalCount / sanitizedLimit) : 0;
|
||||
const startIndex = (sanitizedPage - 1) * sanitizedLimit;
|
||||
const paginatedActivities = activities.slice(
|
||||
startIndex,
|
||||
startIndex + sanitizedLimit,
|
||||
);
|
||||
|
||||
return {
|
||||
filters: {
|
||||
userLat: payload.userLat,
|
||||
userLong: payload.userLong,
|
||||
startDate: payload.startDate,
|
||||
endDate: payload.endDate,
|
||||
startTime: payload.startTime,
|
||||
endTime: payload.endTime,
|
||||
energyLevelXid: payload.energyLevelXid,
|
||||
entryTypeXid: payload.entryTypeXid,
|
||||
page: sanitizedPage,
|
||||
limit: sanitizedLimit,
|
||||
},
|
||||
pagination: {
|
||||
page: sanitizedPage,
|
||||
limit: sanitizedLimit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage: sanitizedPage < totalPages,
|
||||
hasPreviousPage: sanitizedPage > 1 && totalPages > 0,
|
||||
},
|
||||
count: paginatedActivities.length,
|
||||
totalCount,
|
||||
activities: paginatedActivities,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user