Compare commits
17 Commits
e149884f72
...
8f428fc1cb
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f428fc1cb | |||
| 0b503cf8bb | |||
| 7110d0462c | |||
| 96648fe37e | |||
|
|
2095f8e124 | ||
| 21c8799502 | |||
|
|
ad9e8e1a3f | ||
| b200e2cb94 | |||
| cae66237d2 | |||
| 25be8a5647 | |||
| 7a4aecdd45 | |||
| 5cced2981a | |||
|
|
b9fbab3717 | ||
|
|
90c897ad48 | ||
| 4a069cc67a | |||
|
|
5d046c4bcf | ||
| accfc4b769 |
@@ -1038,13 +1038,13 @@ model ActivityOtherDetails {
|
||||
id Int @id @default(autoincrement())
|
||||
activityXid Int @map("activity_xid")
|
||||
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
|
||||
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500)
|
||||
SafetyInstruction String? @map("safety_instruction") @db.VarChar(400)
|
||||
Cancellations String? @map("cancellations") @db.VarChar(400)
|
||||
dosNotes String? @map("dos_notes") @db.VarChar(400)
|
||||
dontsNotes String? @map("donts_notes") @db.VarChar(400)
|
||||
tipsNotes String? @map("tips_notes") @db.VarChar(400)
|
||||
termsAndCondition String? @map("terms_and_condition") @db.VarChar(500)
|
||||
exclusiveNotes String? @map("exclusive_notes") @db.Text
|
||||
SafetyInstruction String? @map("safety_instruction") @db.Text
|
||||
Cancellations String? @map("cancellations") @db.Text
|
||||
dosNotes String? @map("dos_notes") @db.Text
|
||||
dontsNotes String? @map("donts_notes") @db.Text
|
||||
tipsNotes String? @map("tips_notes") @db.Text
|
||||
termsAndCondition String? @map("terms_and_condition") @db.Text
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Host Module Functions
|
||||
# All authentication and host management endpoints
|
||||
|
||||
getHosts:
|
||||
handler: src/modules/host/handlers/host.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/host.*'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /host
|
||||
method: get
|
||||
# getHosts:
|
||||
# handler: src/modules/host/handlers/host.handler
|
||||
# memorySize: 384
|
||||
# package:
|
||||
# patterns:
|
||||
# - 'src/modules/host/handlers/host.*'
|
||||
# - 'src/modules/host/services/**'
|
||||
# - ${file(./serverless/patterns/base.yml):pattern1}
|
||||
# - ${file(./serverless/patterns/base.yml):pattern2}
|
||||
# - ${file(./serverless/patterns/base.yml):pattern3}
|
||||
# - ${file(./serverless/patterns/base.yml):pattern4}
|
||||
# events:
|
||||
# - httpApi:
|
||||
# path: /host
|
||||
# method: get
|
||||
|
||||
verifyOTP:
|
||||
handler: src/modules/host/handlers/Host_Admin/onboarding/verifyOTP.handler
|
||||
|
||||
@@ -362,3 +362,33 @@ addActivityToBucketInterested:
|
||||
- httpApi:
|
||||
path: /user/activities/add-to-bucket-interested
|
||||
method: post
|
||||
|
||||
removeActivityFromBucketInterested:
|
||||
handler: src/modules/user/handlers/activities/removeFromBucketInterested.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/user/handlers/activities/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/activities/remove-from-bucket-interested
|
||||
method: post
|
||||
|
||||
getFilteredLandingPageAllDetails:
|
||||
handler: src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler
|
||||
memorySize: 512
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/user/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/activities/get-filtered-landing-page-details
|
||||
method: get
|
||||
@@ -362,6 +362,9 @@ export class SchedulingService {
|
||||
isActive: true,
|
||||
startDate: { lte: date },
|
||||
OR: [{ endDate: null }, { endDate: { gte: date } }],
|
||||
ScheduleDetails: {
|
||||
some: {}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
activityVenue: {
|
||||
|
||||
@@ -177,8 +177,8 @@ function computeBasePriceAndTaxes(
|
||||
return { basePrice, taxDetails };
|
||||
}
|
||||
|
||||
const normalize = (v?: string | null) =>
|
||||
v ? v.trim().toLowerCase() : null;
|
||||
const normalize = (v?: string | null, maxLength: number = 50) =>
|
||||
v ? v.trim().toLowerCase().substring(0, maxLength) : null;
|
||||
|
||||
async function renderAgreementPdf(vars: {
|
||||
effectiveDate: string;
|
||||
@@ -338,9 +338,11 @@ const findOrCreateState = async (
|
||||
) => {
|
||||
if (!stateName || !countryXid) return null;
|
||||
|
||||
const trimmedStateName = stateName.trim().substring(0, 50);
|
||||
|
||||
const state = await tx.states.findFirst({
|
||||
where: {
|
||||
stateName: { equals: stateName.trim(), mode: 'insensitive' },
|
||||
stateName: { equals: trimmedStateName, mode: 'insensitive' },
|
||||
countryXid,
|
||||
isActive: true,
|
||||
},
|
||||
@@ -350,7 +352,7 @@ const findOrCreateState = async (
|
||||
|
||||
const created = await tx.states.create({
|
||||
data: {
|
||||
stateName: stateName.trim(),
|
||||
stateName: trimmedStateName,
|
||||
countryXid,
|
||||
},
|
||||
});
|
||||
@@ -365,9 +367,11 @@ const findOrCreateCity = async (
|
||||
) => {
|
||||
if (!cityName || !stateXid) return null;
|
||||
|
||||
const trimmedCityName = cityName.trim().substring(0, 50);
|
||||
|
||||
const city = await tx.cities.findFirst({
|
||||
where: {
|
||||
cityName: { equals: cityName.trim(), mode: 'insensitive' },
|
||||
cityName: { equals: trimmedCityName, mode: 'insensitive' },
|
||||
stateXid,
|
||||
isActive: true,
|
||||
},
|
||||
@@ -377,7 +381,7 @@ const findOrCreateCity = async (
|
||||
|
||||
const created = await tx.cities.create({
|
||||
data: {
|
||||
cityName: cityName.trim(),
|
||||
cityName: trimmedCityName,
|
||||
stateXid,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,10 +53,10 @@ export class TokenService {
|
||||
config.jwt.secret
|
||||
);
|
||||
|
||||
await this.prisma.token.deleteMany({
|
||||
where: { userXid: user_xid }
|
||||
})
|
||||
|
||||
// Optionally keep existing refresh tokens alive instead of deleting
|
||||
// Removed deleteMany call so the same refresh token can be used multiple
|
||||
// times. If you want to limit refresh tokens later you can implement
|
||||
// rotation or blacklist logic elsewhere.
|
||||
await this.prisma.token.create({
|
||||
data: {
|
||||
token: refreshToken.token,
|
||||
|
||||
@@ -50,64 +50,73 @@ export const handler = safeHandler(async (
|
||||
const activity = activityDetails.activity;
|
||||
|
||||
// Rooms: combine ActivityVenues with their respective slots for the selected date
|
||||
const Venues = (activity.ActivityVenues || []).map((v: any) => {
|
||||
const header = scheduleDetails.find((h: any) => h.activityVenue?.venueXid === v.id);
|
||||
const Venues = (activity.ActivityVenues || [])
|
||||
.map((v: any) => {
|
||||
const header = scheduleDetails.find(
|
||||
(h: any) => h.activityVenue?.venueXid === v.id
|
||||
);
|
||||
|
||||
const roomSlots = (header?.slots || []).map((s: any) => {
|
||||
let status = 'Available';
|
||||
if (s.maxCapacity === 0) status = 'Housefull';
|
||||
else if (s.maxCapacity <= 2) status = '2 Slots Left';
|
||||
else if (s.maxCapacity <= 5) status = 'Fast Filling';
|
||||
if (!header || !header.slots?.length) {
|
||||
return null; // ❌ venue has no slots for selected date
|
||||
}
|
||||
|
||||
const roomSlots = header.slots.map((s: any) => {
|
||||
let status = "Available";
|
||||
|
||||
if (s.maxCapacity === 0) status = "Housefull";
|
||||
else if (s.maxCapacity <= 2) status = "2 Slots Left";
|
||||
else if (s.maxCapacity <= 5) status = "Fast Filling";
|
||||
|
||||
return {
|
||||
slotId: s.slotId,
|
||||
startTime: s.startTime,
|
||||
endTime: s.endTime,
|
||||
status,
|
||||
maxCapacity: s.maxCapacity,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
slotId: s.slotId,
|
||||
startTime: s.startTime,
|
||||
endTime: s.endTime,
|
||||
status,
|
||||
maxCapacity: s.maxCapacity,
|
||||
venueXid: v.id,
|
||||
venueName: v.venueName,
|
||||
venueLabel: v.venueLabel,
|
||||
venueCapacity: v.venueCapacity,
|
||||
availableSeats: v.availableSeats ?? null,
|
||||
price: v.ActivityPrices?.[0]?.sellPrice ?? null,
|
||||
endDate: header?.endDate ?? null,
|
||||
slots: roomSlots,
|
||||
slotsCount: roomSlots.length,
|
||||
venueMedia: (v.ActivityVenueArtifacts || []).map((media: any) => ({
|
||||
id: media.id,
|
||||
mediaType: media.mediaType,
|
||||
mediaFileName: media.mediaFileName,
|
||||
presignedUrl: media.presignedUrl,
|
||||
})),
|
||||
};
|
||||
});
|
||||
})
|
||||
.filter(Boolean); // ✅ removes null venues
|
||||
|
||||
return {
|
||||
venueXid: v.id,
|
||||
venueName: v.venueName,
|
||||
venueLabel: v.venueLabel,
|
||||
venueCapacity: v.venueCapacity,
|
||||
availableSeats: v.availableSeats ?? null,
|
||||
price: v.ActivityPrices?.[0]?.sellPrice ?? null,
|
||||
endDate: header?.endDate ?? null,
|
||||
slots: roomSlots,
|
||||
slotsCount: roomSlots.length,
|
||||
venueMedia: (v.ActivityVenueArtifacts || []).map((media: any) => ({
|
||||
id: media.id,
|
||||
mediaType: media.mediaType,
|
||||
mediaFileName: media.mediaFileName, // original S3 key / URL
|
||||
presignedUrl: media.presignedUrl, // presigned URL
|
||||
})),
|
||||
// derive check-in/out from all room slots (earliest start, latest end)
|
||||
const allSlots = Venues.flatMap(r => r.slots || []);
|
||||
const startTimes = allSlots.map(s => s.startTime).filter(Boolean);
|
||||
const endTimes = allSlots.map(s => s.endTime).filter(Boolean);
|
||||
const checkInTime = startTimes.length ? startTimes.sort()[0] : null;
|
||||
const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null;
|
||||
|
||||
const responsePayload = {
|
||||
selectedDate,
|
||||
Venues,
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({ success: true, data: responsePayload }),
|
||||
};
|
||||
});
|
||||
|
||||
// derive check-in/out from all room slots (earliest start, latest end)
|
||||
const allSlots = Venues.flatMap(r => r.slots || []);
|
||||
const startTimes = allSlots.map(s => s.startTime).filter(Boolean);
|
||||
const endTimes = allSlots.map(s => s.endTime).filter(Boolean);
|
||||
const checkInTime = startTimes.length ? startTimes.sort()[0] : null;
|
||||
const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null;
|
||||
|
||||
const responsePayload = {
|
||||
selectedDate,
|
||||
Venues,
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({ success: true, data: responsePayload }),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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 { FilteredLandingPageService } from '../../services/filteredLandingPage.service';
|
||||
|
||||
const filteredLandingPageService = new FilteredLandingPageService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
// Extract token from headers
|
||||
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.');
|
||||
}
|
||||
|
||||
// Verify token and get user info
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
|
||||
if (!userId || isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user ID');
|
||||
}
|
||||
|
||||
const page = Number(event.queryStringParameters?.page ?? 1);
|
||||
const limit = Number(event.queryStringParameters?.limit ?? 20);
|
||||
const countryName = event.queryStringParameters?.countryName ?? '';
|
||||
const stateName = event.queryStringParameters?.stateName ?? '';
|
||||
const cityName = event.queryStringParameters?.cityName ?? '';
|
||||
const userLat = event.queryStringParameters?.userLat ?? '';
|
||||
const userLong = event.queryStringParameters?.userLong ?? '';
|
||||
|
||||
let activityTypeXids: number[] | undefined;
|
||||
if (event.queryStringParameters?.activityTypeXids) {
|
||||
try {
|
||||
activityTypeXids = JSON.parse(event.queryStringParameters.activityTypeXids);
|
||||
} catch (error) {
|
||||
// Handle invalid JSON if needed
|
||||
}
|
||||
}
|
||||
|
||||
if (page < 1 || limit < 1) {
|
||||
throw new ApiError(400, 'Invalid pagination values');
|
||||
}
|
||||
|
||||
// Fetch filtered landing page details
|
||||
const result = await filteredLandingPageService.getFilteredLandingPageAllDetails(
|
||||
userId,
|
||||
page,
|
||||
limit,
|
||||
countryName,
|
||||
stateName,
|
||||
cityName,
|
||||
userLat,
|
||||
userLong,
|
||||
activityTypeXids
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Filtered landing page data retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
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 { UserService } from '../../services/user.service';
|
||||
|
||||
const userService = new UserService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
// Extract token from headers
|
||||
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.');
|
||||
}
|
||||
|
||||
// Authenticate user using verifyUserToken
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = userInfo.id;
|
||||
|
||||
if (Number.isNaN(userId)) {
|
||||
throw new ApiError(400, 'User id must be a number');
|
||||
}
|
||||
|
||||
const user = await userService.getUserById(userId);
|
||||
if (!user) {
|
||||
throw new ApiError(404, 'User not found');
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; };
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { activityXid, isBucket, bucketTypeName } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
typeof activityXid !== 'number' ||
|
||||
typeof isBucket !== 'boolean' ||
|
||||
!bucketTypeName
|
||||
) {
|
||||
throw new ApiError(400, 'Required fields missing or invalid');
|
||||
}
|
||||
|
||||
// Remove from bucket/interested
|
||||
const counts = await userService.removeFromBucketInterested(userId, isBucket, bucketTypeName, activityXid);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: `Activity removed from ${isBucket ? 'bucket' : 'interested'} successfully`,
|
||||
data: {
|
||||
bucketCount: counts.bucketCount,
|
||||
interestedCount: counts.interestedCount,
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
949
src/modules/user/services/filteredLandingPage.service.ts
Normal file
949
src/modules/user/services/filteredLandingPage.service.ts
Normal file
@@ -0,0 +1,949 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||
import {
|
||||
ACTIVITY_AM_INTERNAL_STATUS,
|
||||
ACTIVITY_INTERNAL_STATUS,
|
||||
} from '../../../common/utils/constants/host.constant';
|
||||
import config from '../../../config/config';
|
||||
|
||||
const bucket = config.aws.bucketName;
|
||||
|
||||
@Injectable()
|
||||
export class FilteredLandingPageService {
|
||||
constructor(private readonly prisma: PrismaClient) {}
|
||||
|
||||
normalizeName = (name: string): string => {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
attachPresignedUrl = async (key: string | null): Promise<string | null> => {
|
||||
if (!key) return null;
|
||||
try {
|
||||
return await getPresignedUrl(bucket, key);
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate presigned URL for key: ${key}`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
findOrCreateLocation = async (
|
||||
countryName: string,
|
||||
stateName: string,
|
||||
cityName: string,
|
||||
tx: any,
|
||||
) => {
|
||||
const normalizedCountry = this.normalizeName(countryName);
|
||||
const normalizedState = this.normalizeName(stateName);
|
||||
const normalizedCity = this.normalizeName(cityName);
|
||||
|
||||
let country = await tx.countries.findFirst({
|
||||
where: {
|
||||
countryName: { contains: normalizedCountry, mode: 'insensitive' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!country) {
|
||||
country = await tx.countries.create({
|
||||
data: {
|
||||
countryName: countryName.trim(),
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let state = await tx.states.findFirst({
|
||||
where: {
|
||||
countryXid: country.id,
|
||||
stateName: { contains: normalizedState, mode: 'insensitive' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!state) {
|
||||
state = await tx.states.create({
|
||||
data: {
|
||||
countryXid: country.id,
|
||||
stateName: stateName.trim(),
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let city = await tx.cities.findFirst({
|
||||
where: {
|
||||
stateXid: state.id,
|
||||
cityName: { contains: normalizedCity, mode: 'insensitive' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!city) {
|
||||
city = await tx.cities.create({
|
||||
data: {
|
||||
stateXid: state.id,
|
||||
cityName: cityName.trim(),
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
countryXid: country.id,
|
||||
stateXid: state.id,
|
||||
cityXid: city.id,
|
||||
};
|
||||
};
|
||||
|
||||
attachMediaWithPresignedUrl = async (mediaArr = []) => {
|
||||
return (
|
||||
await Promise.all(
|
||||
mediaArr.map(async (m) => {
|
||||
return {
|
||||
...m,
|
||||
presignedUrl: await this.attachPresignedUrl(m.mediaFileName),
|
||||
};
|
||||
}),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
calculateDistance = (
|
||||
lat1: number | null,
|
||||
lon1: number | null,
|
||||
lat2: number | null,
|
||||
lon2: number | null,
|
||||
) => {
|
||||
if (!lat1 || !lon1 || !lat2 || !lon2) return null;
|
||||
|
||||
const R = 6371; // km
|
||||
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 R * c;
|
||||
};
|
||||
|
||||
async rankAndPaginateActivities(
|
||||
tx: any,
|
||||
whereClause: any,
|
||||
page: number,
|
||||
limit: number,
|
||||
connectionInterestMap: Map<number, number>,
|
||||
) {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Get total count
|
||||
const totalCount = await tx.activities.count({ where: whereClause });
|
||||
|
||||
// Fetch activities with ranking metadata
|
||||
const activities = await tx.activities.findMany({
|
||||
where: whereClause,
|
||||
skip,
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
sustainabilityScore: true,
|
||||
totalScore: true,
|
||||
activityType: {
|
||||
select: {
|
||||
interestXid: true,
|
||||
energyLevel: {
|
||||
select: {
|
||||
id: true,
|
||||
energyLevelName: true,
|
||||
energyColor: true,
|
||||
energyIcon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivitiesMedia: {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
mediaFileName: true,
|
||||
mediaType: true,
|
||||
},
|
||||
},
|
||||
// Fetch ranking metadata
|
||||
ItineraryActivities: {
|
||||
select: {
|
||||
ActivityFeedbacks: {
|
||||
select: { activityStars: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivityVenues: {
|
||||
select: {
|
||||
ActivityPrices: {
|
||||
select: { sellPrice: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Sort and format
|
||||
const sortedActivities = activities
|
||||
.map((act) => {
|
||||
const feedbacks = act.ItineraryActivities.flatMap(
|
||||
(ia) => ia.ActivityFeedbacks,
|
||||
);
|
||||
const totalStars = feedbacks.reduce(
|
||||
(sum, f) => sum + f.activityStars,
|
||||
0,
|
||||
);
|
||||
const avgRating =
|
||||
feedbacks.length > 0 ? totalStars / feedbacks.length : 0;
|
||||
const prices = act.ActivityVenues.flatMap((v) =>
|
||||
v.ActivityPrices.map((p) => p.sellPrice),
|
||||
).filter((p) => p !== null) as number[];
|
||||
const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity;
|
||||
|
||||
return {
|
||||
...act,
|
||||
avgRating,
|
||||
minPrice,
|
||||
sustainabilityScore: act.sustainabilityScore ?? 0,
|
||||
totalScore: act.totalScore ?? 0,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating;
|
||||
if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice;
|
||||
if (b.sustainabilityScore !== a.sustainabilityScore)
|
||||
return b.sustainabilityScore - a.sustainabilityScore;
|
||||
return b.totalScore - a.totalScore;
|
||||
});
|
||||
|
||||
const formattedActivities = await Promise.all(
|
||||
sortedActivities.map(async (activity) => ({
|
||||
interestXid: activity.activityType.interestXid,
|
||||
activityId: activity.id,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(activity.id) ?? 0,
|
||||
activityTitle: activity.activityTitle,
|
||||
sustainabilityScore: activity.sustainabilityScore,
|
||||
cheapestPrice: activity.minPrice === Infinity ? null : activity.minPrice,
|
||||
distance: 0,
|
||||
rating: activity.avgRating,
|
||||
energyLevel: activity.activityType.energyLevel
|
||||
? {
|
||||
...activity.activityType.energyLevel,
|
||||
presignedUrl: await this.attachPresignedUrl(
|
||||
activity.activityType.energyLevel.energyIcon,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
hasMore: skip + limit < totalCount,
|
||||
activities: formattedActivities,
|
||||
};
|
||||
}
|
||||
|
||||
async getFilteredLandingPageAllDetails(
|
||||
userId: number,
|
||||
page: number,
|
||||
limit: number,
|
||||
countryName: string,
|
||||
stateName: string,
|
||||
cityName: string,
|
||||
userLat: string,
|
||||
userLong: string,
|
||||
activityTypeXids?: number[],
|
||||
) {
|
||||
const data = await this.prisma.$transaction(async (tx) => {
|
||||
const userAddressDetails = await tx.userAddressDetails.findFirst({
|
||||
where: { userXid: userId },
|
||||
select: {
|
||||
id: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
pinCode: true,
|
||||
locationName: true,
|
||||
stateXid: true,
|
||||
cityXid: true,
|
||||
countryXid: true,
|
||||
locationLat: true,
|
||||
locationLong: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userLatitude = userAddressDetails?.locationLat ?? null;
|
||||
const userLongitude = userAddressDetails?.locationLong ?? null;
|
||||
|
||||
let effectiveLocation: {
|
||||
countryXid?: number | null;
|
||||
stateXid?: number | null;
|
||||
cityXid?: number | null;
|
||||
} | null = null;
|
||||
|
||||
const hasRequestLocation = countryName && stateName && cityName;
|
||||
|
||||
if (hasRequestLocation) {
|
||||
effectiveLocation = await this.findOrCreateLocation(
|
||||
countryName!,
|
||||
stateName!,
|
||||
cityName!,
|
||||
tx,
|
||||
);
|
||||
} else if (userAddressDetails) {
|
||||
effectiveLocation = {
|
||||
countryXid: userAddressDetails.countryXid,
|
||||
stateXid: userAddressDetails.stateXid,
|
||||
cityXid: userAddressDetails.cityXid,
|
||||
};
|
||||
}
|
||||
|
||||
const effectiveCountryXid = effectiveLocation?.countryXid ?? null;
|
||||
const effectiveStateXid = effectiveLocation?.stateXid ?? null;
|
||||
|
||||
const userInterests = await tx.userInterests.findMany({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
interestXid: true,
|
||||
interest: {
|
||||
select: {
|
||||
id: true,
|
||||
interestName: true,
|
||||
interestColor: true,
|
||||
interestImage: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInterests.length) {
|
||||
return {
|
||||
userAddressDetails,
|
||||
interests: [],
|
||||
activityTypes: [],
|
||||
otherStatesActivities: null,
|
||||
overSeasActivities: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Get all activity types for user interests, filtered by selected activity types if provided
|
||||
const activityTypeWhere: any = {
|
||||
interestXid: { in: userInterests.map((ui) => ui.interestXid) },
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
if (activityTypeXids && activityTypeXids.length > 0) {
|
||||
activityTypeWhere.id = { in: activityTypeXids };
|
||||
}
|
||||
|
||||
const activityTypesWithInterests = await tx.activityTypes.findMany({
|
||||
where: activityTypeWhere,
|
||||
select: {
|
||||
id: true,
|
||||
activityTypeName: true,
|
||||
interestXid: true,
|
||||
interests: {
|
||||
select: {
|
||||
id: true,
|
||||
interestName: true,
|
||||
interestColor: true,
|
||||
interestImage: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!activityTypesWithInterests.length) {
|
||||
return {
|
||||
userAddressDetails,
|
||||
interests: [],
|
||||
activityTypes: [],
|
||||
otherStatesActivities: null,
|
||||
overSeasActivities: null,
|
||||
};
|
||||
}
|
||||
|
||||
const userBucketInterested = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid: userId,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
isBucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userBucketActivityIds = userBucketInterested
|
||||
.filter(u => u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const userInterestedActivityIds = userBucketInterested
|
||||
.filter(u => !u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const allUserExcludedActivityIds = userBucketInterested.map(
|
||||
u => u.activityXid,
|
||||
);
|
||||
|
||||
const userConnectionDetails = await tx.connectDetails.findMany({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
schoolCompanyXid: true,
|
||||
}
|
||||
})
|
||||
|
||||
const otherConnectionUsers = await tx.connectDetails.findMany({
|
||||
where: { userXid: { notIn: [userId] }, isActive: true, schoolCompanyXid: { in: userConnectionDetails.map((u) => u.schoolCompanyXid) } },
|
||||
select: {
|
||||
id: true,
|
||||
userXid: true,
|
||||
}
|
||||
})
|
||||
|
||||
const connectionUserIds =
|
||||
otherConnectionUsers.length > 0
|
||||
? otherConnectionUsers.map(u => u.userXid)
|
||||
: [-1];
|
||||
|
||||
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
|
||||
by: ['activityXid'],
|
||||
where: {
|
||||
userXid: { in: connectionUserIds },
|
||||
isActive: true,
|
||||
},
|
||||
_count: {
|
||||
activityXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connectionInterestMap = new Map(
|
||||
connectionInterestByActivity.map(item => [
|
||||
item.activityXid,
|
||||
item._count.activityXid,
|
||||
])
|
||||
);
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Group activity types by interest
|
||||
const activityTypesByInterest = activityTypesWithInterests.reduce((acc, at) => {
|
||||
if (!acc[at.interestXid]) {
|
||||
acc[at.interestXid] = {
|
||||
interest: at.interests,
|
||||
activityTypes: [],
|
||||
};
|
||||
}
|
||||
acc[at.interestXid].activityTypes.push({
|
||||
activityTypeId: at.id,
|
||||
activityTypeName: at.activityTypeName,
|
||||
});
|
||||
return acc;
|
||||
}, {} as any);
|
||||
|
||||
// Fetch activities for each activity type
|
||||
const activitiesByActivityType = await Promise.all(
|
||||
activityTypesWithInterests.map(async (activityType) => {
|
||||
const activities = await tx.activities.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: activityType.id,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { id: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
activityDurationMins: true,
|
||||
sustainabilityScore: true,
|
||||
checkInLat: true,
|
||||
checkInLong: true,
|
||||
activityType: {
|
||||
select: {
|
||||
id: true,
|
||||
activityTypeName: true,
|
||||
interestXid: true,
|
||||
energyLevel: {
|
||||
select: {
|
||||
id: true,
|
||||
energyLevelName: true,
|
||||
energyColor: true,
|
||||
energyIcon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivityVenues: {
|
||||
select: {
|
||||
ActivityPrices: {
|
||||
select: {
|
||||
sellPrice: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivitiesMedia: {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
mediaFileName: true,
|
||||
mediaType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const formattedActivities = await Promise.all(
|
||||
activities.map(async (activity) => {
|
||||
const cheapestPrice =
|
||||
activity.ActivityVenues.flatMap((v) => v.ActivityPrices)
|
||||
.map((p) => p.sellPrice)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a - b)[0] ?? null;
|
||||
|
||||
const distance = this.calculateDistance(
|
||||
userLatitude,
|
||||
userLongitude,
|
||||
activity.checkInLat,
|
||||
activity.checkInLong,
|
||||
);
|
||||
|
||||
return {
|
||||
activityId: activity.id,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(activity.id) ?? 0,
|
||||
activityTitle: activity.activityTitle,
|
||||
activityDurationMins: activity.activityDurationMins,
|
||||
sustainabilityScore: activity.sustainabilityScore,
|
||||
cheapestPrice,
|
||||
distance,
|
||||
rating: 0,
|
||||
energyLevel: activity.activityType.energyLevel
|
||||
? {
|
||||
...activity.activityType.energyLevel,
|
||||
presignedUrl: await this.attachPresignedUrl(
|
||||
activity.activityType.energyLevel.energyIcon,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
activityTypeId: activityType.id,
|
||||
activityTypeName: activityType.activityTypeName,
|
||||
interestXid: activityType.interestXid,
|
||||
activities: formattedActivities,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
hasMore: formattedActivities.length === limit,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Group by interests for the final structure
|
||||
const interestsWithActivityTypes = await Promise.all(Object.values(activityTypesByInterest).map(
|
||||
async (interestGroup: any) => ({
|
||||
interestId: interestGroup.interest.id,
|
||||
interestName: interestGroup.interest.interestName,
|
||||
interestColor: interestGroup.interest.interestColor,
|
||||
interestImage: interestGroup.interest.interestImage,
|
||||
interestImagePresignedUrl: await this.attachPresignedUrl(interestGroup.interest.interestImage),
|
||||
displayOrder: interestGroup.interest.displayOrder,
|
||||
activityTypes: interestGroup.activityTypes.map((at: any) => {
|
||||
const activityTypeData = activitiesByActivityType.find(
|
||||
(adata) => adata.activityTypeId === at.activityTypeId
|
||||
);
|
||||
return {
|
||||
...at,
|
||||
activities: activityTypeData?.activities || [],
|
||||
pagination: activityTypeData?.pagination || { page, limit, hasMore: false },
|
||||
};
|
||||
}),
|
||||
}),
|
||||
));
|
||||
|
||||
// Most Hyped Activities with filtering
|
||||
const mostHypedGrouped = await tx.userBucketInterested.groupBy({
|
||||
by: ['activityXid'],
|
||||
where: {
|
||||
isActive: true,
|
||||
isBucket: false,
|
||||
},
|
||||
_count: {
|
||||
activityXid: true,
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
activityXid: 'desc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Filter most hyped activities by activity type if provided
|
||||
let filteredMostHypedActivityIds = mostHypedGrouped.map((a) => a.activityXid);
|
||||
|
||||
if (activityTypeXids && activityTypeXids.length > 0) {
|
||||
const activitiesWithTypes = await tx.activities.findMany({
|
||||
where: {
|
||||
id: { in: filteredMostHypedActivityIds },
|
||||
activityTypeXid: { in: activityTypeXids },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
filteredMostHypedActivityIds = activitiesWithTypes.map(a => a.id);
|
||||
}
|
||||
|
||||
const finalMostHypedGrouped = mostHypedGrouped
|
||||
.filter(group => filteredMostHypedActivityIds.includes(group.activityXid))
|
||||
.slice(skip, skip + limit);
|
||||
|
||||
const totalHypedActivities = filteredMostHypedActivityIds.length;
|
||||
const mostHypedActivityIds = finalMostHypedGrouped.map((a) => a.activityXid);
|
||||
|
||||
const mostHypedActivitiesRaw = await tx.activities.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: mostHypedActivityIds,
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
sustainabilityScore: true,
|
||||
totalScore: true,
|
||||
activityType: {
|
||||
select: {
|
||||
energyLevel: {
|
||||
select: {
|
||||
id: true,
|
||||
energyLevelName: true,
|
||||
energyColor: true,
|
||||
energyIcon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivitiesMedia: {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
mediaFileName: true,
|
||||
mediaType: true,
|
||||
},
|
||||
},
|
||||
ItineraryActivities: {
|
||||
select: {
|
||||
ActivityFeedbacks: {
|
||||
select: { activityStars: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivityVenues: {
|
||||
select: {
|
||||
ActivityPrices: {
|
||||
select: { sellPrice: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Sort Most Hyped by the 4 criteria
|
||||
const mostHypedSorted = mostHypedActivitiesRaw
|
||||
.map((act) => {
|
||||
const feedbacks = act.ItineraryActivities.flatMap(
|
||||
(ia) => ia.ActivityFeedbacks,
|
||||
);
|
||||
const totalStars = feedbacks.reduce(
|
||||
(sum, f) => sum + f.activityStars,
|
||||
0,
|
||||
);
|
||||
const avgRating =
|
||||
feedbacks.length > 0 ? totalStars / feedbacks.length : 0;
|
||||
const prices = act.ActivityVenues.flatMap((v) =>
|
||||
v.ActivityPrices.map((p) => p.sellPrice),
|
||||
).filter((p) => p !== null) as number[];
|
||||
const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity;
|
||||
|
||||
return {
|
||||
...act,
|
||||
avgRating,
|
||||
minPrice,
|
||||
sustainabilityScore: act.sustainabilityScore ?? 0,
|
||||
totalScore: act.totalScore ?? 0,
|
||||
hypeCount:
|
||||
finalMostHypedGrouped.find((g) => g.activityXid === act.id)?._count
|
||||
.activityXid ?? 0,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating;
|
||||
if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice;
|
||||
if (b.sustainabilityScore !== a.sustainabilityScore)
|
||||
return b.sustainabilityScore - a.sustainabilityScore;
|
||||
return b.totalScore - a.totalScore;
|
||||
});
|
||||
|
||||
const mostHypedActivities = await Promise.all(
|
||||
mostHypedSorted.map(async (activity) => ({
|
||||
activityId: activity.id,
|
||||
activityTitle: activity.activityTitle,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(activity.id) ?? 0,
|
||||
hypeCount: activity.hypeCount,
|
||||
distance: 0,
|
||||
rating: 0,
|
||||
energyLevel: activity.activityType.energyLevel
|
||||
? {
|
||||
...activity.activityType.energyLevel,
|
||||
presignedUrl: await this.attachPresignedUrl(
|
||||
activity.activityType.energyLevel.energyIcon,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
||||
})),
|
||||
);
|
||||
|
||||
const formattedMostHypedActivities = {
|
||||
page,
|
||||
limit,
|
||||
totalCount: totalHypedActivities,
|
||||
hasMore: skip + limit < totalHypedActivities,
|
||||
activities: mostHypedActivities,
|
||||
};
|
||||
|
||||
// New Arrivals with filtering
|
||||
const newArrivalsWhere: any = {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
|
||||
};
|
||||
|
||||
if (activityTypeXids && activityTypeXids.length > 0) {
|
||||
newArrivalsWhere.activityTypeXid = { in: activityTypeXids };
|
||||
}
|
||||
|
||||
const formattedNewArrivalsActivities = await this.rankAndPaginateActivities(
|
||||
tx,
|
||||
newArrivalsWhere,
|
||||
page,
|
||||
limit,
|
||||
connectionInterestMap
|
||||
);
|
||||
|
||||
// Other States Activities with filtering
|
||||
const otherStatesWhere: any = {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
};
|
||||
|
||||
if (effectiveCountryXid) {
|
||||
otherStatesWhere.checkInCountryXid = effectiveCountryXid;
|
||||
}
|
||||
if (effectiveStateXid) {
|
||||
otherStatesWhere.checkInStateXid = { not: effectiveStateXid };
|
||||
}
|
||||
if (activityTypeXids && activityTypeXids.length > 0) {
|
||||
otherStatesWhere.activityTypeXid = { in: activityTypeXids };
|
||||
}
|
||||
|
||||
const formattedOtherStatesActivities = await this.rankAndPaginateActivities(
|
||||
tx,
|
||||
otherStatesWhere,
|
||||
page,
|
||||
limit,
|
||||
connectionInterestMap
|
||||
);
|
||||
|
||||
// Random Activities with filtering
|
||||
const totalActiveCount = await tx.activities.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
...(activityTypeXids && activityTypeXids.length > 0 && {
|
||||
activityTypeXid: { in: activityTypeXids },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
let randomActivities: any[] = [];
|
||||
|
||||
if (totalActiveCount > 0) {
|
||||
const takeCount = Math.min(5, totalActiveCount);
|
||||
|
||||
const randomOffsets = new Set<number>();
|
||||
while (randomOffsets.size < takeCount) {
|
||||
randomOffsets.add(Math.floor(Math.random() * totalActiveCount));
|
||||
}
|
||||
|
||||
const randomFetched = await Promise.all(
|
||||
Array.from(randomOffsets).map((offset) =>
|
||||
tx.activities.findFirst({
|
||||
skip: offset,
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus:
|
||||
ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus:
|
||||
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
...(activityTypeXids && activityTypeXids.length > 0 && {
|
||||
activityTypeXid: { in: activityTypeXids },
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
ActivitiesMedia: {
|
||||
where: { isActive: true, isCoverImage: true },
|
||||
orderBy: { displayOrder: 'asc' },
|
||||
take: 1,
|
||||
select: {
|
||||
mediaFileName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
randomActivities = await Promise.all(
|
||||
randomFetched
|
||||
.filter(Boolean)
|
||||
.map(async (activity) => {
|
||||
const cover = activity!.ActivitiesMedia?.[0];
|
||||
|
||||
return {
|
||||
activityId: activity!.id,
|
||||
activityTitle: activity!.activityTitle,
|
||||
coverImage: cover?.mediaFileName ?? null,
|
||||
coverImagePresignedUrl: cover?.mediaFileName
|
||||
? await this.attachPresignedUrl(cover.mediaFileName)
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Overseas Activities with filtering
|
||||
const overseasWhere: any = {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
};
|
||||
|
||||
if (effectiveCountryXid) {
|
||||
overseasWhere.checkInCountryXid = { not: effectiveCountryXid };
|
||||
}
|
||||
if (activityTypeXids && activityTypeXids.length > 0) {
|
||||
overseasWhere.activityTypeXid = { in: activityTypeXids };
|
||||
}
|
||||
|
||||
const formattedOverSeasActivities = await this.rankAndPaginateActivities(
|
||||
tx,
|
||||
overseasWhere,
|
||||
page,
|
||||
limit,
|
||||
connectionInterestMap
|
||||
);
|
||||
|
||||
return {
|
||||
userAddressDetails,
|
||||
experiencesLogged: 0,
|
||||
citiesDiscovered: 0,
|
||||
loggedInNetworkCount: 0,
|
||||
citiesInNetworkCount: 0,
|
||||
rating: 0,
|
||||
interestedCount: userInterestedActivityIds.length,
|
||||
bucketCount: userBucketActivityIds.length,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
},
|
||||
randomActivities,
|
||||
interests: interestsWithActivityTypes,
|
||||
activityTypes: activitiesByActivityType,
|
||||
otherStatesActivities: formattedOtherStatesActivities,
|
||||
overSeasActivities: formattedOverSeasActivities,
|
||||
newArrivalsActivities: formattedNewArrivalsActivities,
|
||||
mostHypedActivities: formattedMostHypedActivities,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,30 @@ import config from '@/config/config';
|
||||
// return R * c;
|
||||
// }
|
||||
|
||||
const calculateDistance = (
|
||||
lat1: number | null,
|
||||
lon1: number | null,
|
||||
lat2: number | null,
|
||||
lon2: number | null,
|
||||
) => {
|
||||
if (!lat1 || !lon1 || !lat2 || !lon2) return null;
|
||||
|
||||
const R = 6371; // km
|
||||
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 normalizeName = (name: string) =>
|
||||
name.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
|
||||
@@ -637,6 +661,9 @@ export class UserService {
|
||||
},
|
||||
});
|
||||
|
||||
const userLatitude = userAddressDetails?.locationLat ?? null;
|
||||
const userLongitude = userAddressDetails?.locationLong ?? null;
|
||||
|
||||
let effectiveLocation: {
|
||||
countryXid?: number | null;
|
||||
stateXid?: number | null;
|
||||
@@ -709,6 +736,9 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
const userInterestActivityTypeIds =
|
||||
activitiyTypesOfUserInterests.map((a) => a.id);
|
||||
|
||||
const userBucketInterested = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid: userId,
|
||||
@@ -783,7 +813,7 @@ export class UserService {
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: {
|
||||
in: activitiyTypesOfUserInterests.map((at) => at.id),
|
||||
in: userInterestActivityTypeIds
|
||||
},
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
@@ -839,6 +869,17 @@ export class UserService {
|
||||
where: {
|
||||
isActive: true,
|
||||
isBucket: false,
|
||||
activityXid: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
Activities: {
|
||||
activityTypeXid: { in: userInterestActivityTypeIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -850,6 +891,9 @@ export class UserService {
|
||||
where: {
|
||||
isActive: true,
|
||||
isBucket: false,
|
||||
activityXid: {
|
||||
notIn: allUserExcludedActivityIds.length ? allUserExcludedActivityIds : [-1],
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
activityXid: true,
|
||||
@@ -876,6 +920,7 @@ export class UserService {
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
activityTypeXid: { in: userInterestActivityTypeIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -999,10 +1044,11 @@ export class UserService {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: { in: userInterestActivityTypeIds },
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
: [-1],
|
||||
},
|
||||
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
|
||||
};
|
||||
@@ -1022,6 +1068,7 @@ export class UserService {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: { in: userInterestActivityTypeIds },
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
@@ -1129,6 +1176,7 @@ export class UserService {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: { in: userInterestActivityTypeIds },
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
@@ -1156,6 +1204,13 @@ export class UserService {
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a - b)[0] ?? null;
|
||||
|
||||
const distance = calculateDistance(
|
||||
userLatitude,
|
||||
userLongitude,
|
||||
activity.checkInLat,
|
||||
activity.checkInLong,
|
||||
);
|
||||
|
||||
return {
|
||||
interestXid: activity.activityType.interestXid,
|
||||
activityId: activity.id,
|
||||
@@ -1165,7 +1220,7 @@ export class UserService {
|
||||
activityDurationMins: activity.activityDurationMins,
|
||||
sustainabilityScore: activity.sustainabilityScore,
|
||||
cheapestPrice,
|
||||
distance: 0,
|
||||
distance,
|
||||
rating: 0,
|
||||
energyLevel: activity.activityType.energyLevel
|
||||
? {
|
||||
@@ -1246,9 +1301,14 @@ export class UserService {
|
||||
stateXid: true,
|
||||
cityXid: true,
|
||||
countryXid: true,
|
||||
locationLat: true,
|
||||
locationLong: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userLat = userAddressDetails?.locationLat ?? null;
|
||||
const userLng = userAddressDetails?.locationLong ?? null;
|
||||
|
||||
let effectiveLocation: {
|
||||
countryXid?: number | null;
|
||||
stateXid?: number | null;
|
||||
@@ -1398,6 +1458,8 @@ export class UserService {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
checkInLat: true,
|
||||
checkInLong: true,
|
||||
activityType: {
|
||||
select: {
|
||||
interestXid: true,
|
||||
@@ -1418,7 +1480,12 @@ export class UserService {
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(a.id) ?? 0,
|
||||
activityTitle: a.activityTitle,
|
||||
distance: 0,
|
||||
distance: calculateDistance(
|
||||
userLat,
|
||||
userLng,
|
||||
a.checkInLat,
|
||||
a.checkInLong
|
||||
),
|
||||
rating: 0,
|
||||
energyLevel: {
|
||||
...a.activityType.energyLevel,
|
||||
@@ -1489,6 +1556,8 @@ export class UserService {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
checkInLat: true,
|
||||
checkInLong: true,
|
||||
activityType: { select: { energyLevel: true } },
|
||||
ActivitiesMedia: {
|
||||
where: { isActive: true },
|
||||
@@ -1507,7 +1576,12 @@ export class UserService {
|
||||
hypeCount: g._count.activityXid,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(act.id) ?? 0,
|
||||
distance: 0,
|
||||
distance: calculateDistance(
|
||||
userLat,
|
||||
userLng,
|
||||
act.checkInLat,
|
||||
act.checkInLong
|
||||
),
|
||||
rating: 0,
|
||||
energyLevel: {
|
||||
...act.activityType.energyLevel,
|
||||
@@ -1637,6 +1711,7 @@ export class UserService {
|
||||
hasMore: skip + limit < newArrivalsCount,
|
||||
activities: await Promise.all(
|
||||
newArrivalsRaw.map(async (a) => ({
|
||||
activityId: a.id,
|
||||
activityTitle: a.activityTitle,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(a.id) ?? 0,
|
||||
@@ -1660,6 +1735,7 @@ export class UserService {
|
||||
hasMore: skip + limit < otherStatesCount,
|
||||
activities: await Promise.all(
|
||||
otherStatesRaw.map(async (a) => ({
|
||||
activityId: a.id,
|
||||
activityTitle: a.activityTitle,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(a.id) ?? 0,
|
||||
@@ -1683,6 +1759,7 @@ export class UserService {
|
||||
hasMore: skip + limit < overseasCount,
|
||||
activities: await Promise.all(
|
||||
overseasRaw.map(async (a) => ({
|
||||
activityId: a.id,
|
||||
activityTitle: a.activityTitle,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(a.id) ?? 0,
|
||||
@@ -1740,6 +1817,30 @@ export class UserService {
|
||||
equipmentIsChargeable: true,
|
||||
cancellationAvailable: true,
|
||||
cancellationAllowedBeforeMins: true,
|
||||
checkInCity: {
|
||||
select: {
|
||||
id: true,
|
||||
cityName: true
|
||||
}
|
||||
},
|
||||
checkOutCity: {
|
||||
select: {
|
||||
id: true,
|
||||
cityName: true
|
||||
}
|
||||
},
|
||||
checkInState: {
|
||||
select: {
|
||||
id: true,
|
||||
stateName: true
|
||||
}
|
||||
},
|
||||
checkOutState: {
|
||||
select: {
|
||||
id: true,
|
||||
stateName: true
|
||||
}
|
||||
},
|
||||
|
||||
activityType: {
|
||||
select: {
|
||||
@@ -1761,6 +1862,8 @@ export class UserService {
|
||||
select: {
|
||||
id: true,
|
||||
exclusiveNotes: true,
|
||||
SafetyInstruction: true,
|
||||
Cancellations: true,
|
||||
dosNotes: true,
|
||||
dontsNotes: true,
|
||||
tipsNotes: true,
|
||||
@@ -1915,6 +2018,9 @@ export class UserService {
|
||||
ActivityVenues: {
|
||||
where: {
|
||||
isActive: true,
|
||||
ScheduleHeader: {
|
||||
some: {}
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -1933,6 +2039,18 @@ export class UserService {
|
||||
mediaType: true,
|
||||
},
|
||||
},
|
||||
ScheduleHeader: {
|
||||
select: {
|
||||
id: true,
|
||||
scheduleType: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
earlyCheckInMins: true,
|
||||
bookingCutOffMins: true,
|
||||
effectiveFromDt: true,
|
||||
effectiveToDt: true,
|
||||
}
|
||||
},
|
||||
ActivityPrices: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -1953,6 +2071,53 @@ export class UserService {
|
||||
},
|
||||
});
|
||||
|
||||
if (!activity) {
|
||||
throw new Error("Activity not found");
|
||||
}
|
||||
|
||||
const userActivityStatus = await tx.userBucketInterested.findFirst({
|
||||
where: {
|
||||
activityXid: activityXid,
|
||||
userXid: userId,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
isBucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isBucket = userActivityStatus?.isBucket === true;
|
||||
const isInterested =
|
||||
userActivityStatus ? userActivityStatus.isBucket === false : false;
|
||||
|
||||
const userLocation = await tx.userAddressDetails.findFirst({
|
||||
where: { userXid: userId },
|
||||
select: {
|
||||
locationLat: true,
|
||||
locationLong: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userLat = userLocation?.locationLat ?? null;
|
||||
const userLng = userLocation?.locationLong ?? null;
|
||||
|
||||
let distance = 0;
|
||||
|
||||
if (
|
||||
userLat &&
|
||||
userLng &&
|
||||
activity?.checkInLat &&
|
||||
activity?.checkInLong
|
||||
) {
|
||||
distance = calculateDistance(
|
||||
userLat,
|
||||
userLng,
|
||||
activity.checkInLat,
|
||||
activity.checkInLong
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
// ================= PRESIGNED URL SECTION =================
|
||||
|
||||
// 1️⃣ Activity Media
|
||||
@@ -2063,9 +2228,10 @@ export class UserService {
|
||||
})
|
||||
: 0;
|
||||
|
||||
const prices = activity.ActivityVenues.flatMap((v) =>
|
||||
v.ActivityPrices.map((p) => p.sellPrice),
|
||||
).filter((p) => p !== null) as number[];
|
||||
const prices =
|
||||
activity?.ActivityVenues?.flatMap((v) =>
|
||||
v.ActivityPrices.map((p) => p.sellPrice)
|
||||
).filter((p) => p !== null) ?? [];
|
||||
|
||||
const cheapestPrice = prices.length > 0 ? Math.min(...prices) : null;
|
||||
|
||||
@@ -2099,10 +2265,11 @@ export class UserService {
|
||||
},
|
||||
});
|
||||
|
||||
const shuffledUsers = interestedUsers.sort(() => 0.5 - Math.random());
|
||||
const randomFive = shuffledUsers.slice(0, 5);
|
||||
const randomFive = interestedUsers
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 5);
|
||||
|
||||
const interestedUserImages: string[] = [];
|
||||
const interestedUserImages: { profileImage: string }[] = [];
|
||||
|
||||
for (const item of randomFive) {
|
||||
const profileImage = item.user.profileImage;
|
||||
@@ -2113,10 +2280,23 @@ export class UserService {
|
||||
: profileImage;
|
||||
|
||||
const presignedUrl = await getPresignedUrl(bucket, key);
|
||||
interestedUserImages.push(presignedUrl);
|
||||
|
||||
interestedUserImages.push({
|
||||
profileImage: presignedUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkInLocation =
|
||||
activity?.checkInCity?.cityName && activity?.checkInState?.stateName
|
||||
? `${activity.checkInCity.cityName}, ${activity.checkInState.stateName}`
|
||||
: null;
|
||||
|
||||
const checkOutLocation =
|
||||
activity?.checkOutCity?.cityName && activity?.checkOutState?.stateName
|
||||
? `${activity.checkOutCity.cityName}, ${activity.checkOutState.stateName}`
|
||||
: null;
|
||||
|
||||
return {
|
||||
activity,
|
||||
interestedCount,
|
||||
@@ -2124,8 +2304,12 @@ export class UserService {
|
||||
cheapestPrice,
|
||||
totalCapacity,
|
||||
rating: 0, // ⭐ Placeholder, implement rating logic as needed
|
||||
distance: 0,
|
||||
interestedUserImages
|
||||
distance: distance || 0,
|
||||
interestedUserImages,
|
||||
isBucket,
|
||||
isInterested,
|
||||
checkInLocation,
|
||||
checkOutLocation
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2521,6 +2705,19 @@ export class UserService {
|
||||
select: {
|
||||
id: true,
|
||||
schoolCompanyName: true,
|
||||
cities: {
|
||||
select: {
|
||||
id: true,
|
||||
cityName: true,
|
||||
states: {
|
||||
select: {
|
||||
id: true,
|
||||
stateName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isSchool: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
@@ -2646,37 +2843,9 @@ export class UserService {
|
||||
) {
|
||||
const data = await this.prisma.$transaction(async (tx) => {
|
||||
|
||||
const networkUsers = await tx.connectDetails.findMany({
|
||||
const userInterests = await tx.userInterests.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
schoolCompanyXid: {
|
||||
in: schoolCompanyXids,
|
||||
},
|
||||
userXid: {
|
||||
not: userId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!networkUsers.length) {
|
||||
return {
|
||||
interests: [],
|
||||
mostHypedActivities: null,
|
||||
newArrivalsActivities: null,
|
||||
otherStatesActivities: null,
|
||||
overSeasActivities: null,
|
||||
};
|
||||
}
|
||||
|
||||
const networkUserIds = [...new Set(networkUsers.map(u => u.userXid))];
|
||||
|
||||
const networkUserInterests = await tx.userInterests.findMany({
|
||||
where: {
|
||||
userXid: { in: networkUserIds },
|
||||
userXid: userId,
|
||||
isActive: true,
|
||||
},
|
||||
distinct: ['interestXid'],
|
||||
@@ -2694,9 +2863,74 @@ export class UserService {
|
||||
}
|
||||
});
|
||||
|
||||
const distinctInterests = networkUserInterests.map(i => i.interestXid);
|
||||
if (!userInterests.length) {
|
||||
return {
|
||||
interests: [],
|
||||
mostHypedActivities: null,
|
||||
newArrivalsActivities: null,
|
||||
otherStatesActivities: null,
|
||||
overSeasActivities: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!distinctInterests.length) {
|
||||
const connectionUsers = await tx.connectDetails.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
schoolCompanyXid: { in: schoolCompanyXids },
|
||||
userXid: { not: userId },
|
||||
},
|
||||
select: {
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connectionUserIds = [
|
||||
...new Set(connectionUsers.map((u) => u.userXid)),
|
||||
];
|
||||
|
||||
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
|
||||
by: ["activityXid"],
|
||||
where: {
|
||||
userXid: { in: connectionUserIds },
|
||||
isActive: true,
|
||||
},
|
||||
_count: {
|
||||
activityXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connectionInterestMap = new Map(
|
||||
connectionInterestByActivity.map((item) => [
|
||||
item.activityXid,
|
||||
item._count.activityXid,
|
||||
])
|
||||
);
|
||||
|
||||
if (!connectionUserIds.length) {
|
||||
return {
|
||||
interests: [],
|
||||
mostHypedActivities: null,
|
||||
newArrivalsActivities: null,
|
||||
otherStatesActivities: null,
|
||||
overSeasActivities: null,
|
||||
};
|
||||
}
|
||||
|
||||
const connectionActivities = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid: { in: connectionUserIds },
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connectionActivityIds = [
|
||||
...new Set(connectionActivities.map((a) => a.activityXid)),
|
||||
];
|
||||
|
||||
if (!connectionActivityIds.length) {
|
||||
return {
|
||||
interests: [],
|
||||
mostHypedActivities: null,
|
||||
@@ -2709,7 +2943,7 @@ export class UserService {
|
||||
const activityTypes = await tx.activityTypes.findMany({
|
||||
where: {
|
||||
interestXid: {
|
||||
in: distinctInterests,
|
||||
in: userInterests.map(i => i.interestXid),
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
@@ -2726,15 +2960,22 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
const activityTypeIds = activityTypes.map((a) => a.id);
|
||||
|
||||
const userAddressDetails = await tx.userAddressDetails.findFirst({
|
||||
where: { userXid: userId },
|
||||
select: {
|
||||
stateXid: true,
|
||||
cityXid: true,
|
||||
countryXid: true,
|
||||
locationLat: true,
|
||||
locationLong: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userLatitude = userAddressDetails?.locationLat ?? null;
|
||||
const userLongitude = userAddressDetails?.locationLong ?? null;
|
||||
|
||||
let effectiveLocation: {
|
||||
countryXid?: number | null;
|
||||
stateXid?: number | null;
|
||||
@@ -2760,41 +3001,24 @@ export class UserService {
|
||||
const effectiveCountryXid = effectiveLocation?.countryXid ?? null;
|
||||
const effectiveStateXid = effectiveLocation?.stateXid ?? null;
|
||||
|
||||
const userConnectionDetails = await tx.connectDetails.findMany({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
schoolCompanyXid: true,
|
||||
}
|
||||
})
|
||||
|
||||
const otherConnectionUsers = await tx.connectDetails.findMany({
|
||||
where: { userXid: { notIn: [userId] }, isActive: true, schoolCompanyXid: { in: userConnectionDetails.map((u) => u.schoolCompanyXid) } },
|
||||
select: {
|
||||
id: true,
|
||||
userXid: true,
|
||||
}
|
||||
})
|
||||
|
||||
const connectionUserIds = otherConnectionUsers.map(u => u.userXid);
|
||||
|
||||
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
|
||||
by: ['activityXid'],
|
||||
const userBucketInterested = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid: { in: connectionUserIds },
|
||||
userXid: userId,
|
||||
isActive: true,
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
activityXid: true,
|
||||
isBucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connectionInterestMap = new Map(
|
||||
connectionInterestByActivity.map(item => [
|
||||
item.activityXid,
|
||||
item._count.activityXid,
|
||||
])
|
||||
);
|
||||
const userBucketActivityIds = userBucketInterested
|
||||
.filter(u => u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const userInterestedActivityIds = userBucketInterested
|
||||
.filter(u => !u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
/* =====================================================
|
||||
1️⃣ FETCH ALL CANDIDATES FOR INTERESTS (SIMPLE SORT)
|
||||
@@ -2802,12 +3026,11 @@ export class UserService {
|
||||
// Reverted to simple ID based sorting for Interest-based activities
|
||||
const activities = await tx.activities.findMany({
|
||||
where: {
|
||||
id: { in: connectionActivityIds }, // 🔥 NEW FILTER
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: {
|
||||
in: activityTypes.map(at => at.id),
|
||||
},
|
||||
activityTypeXid: { in: activityTypeIds },
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
@@ -2852,14 +3075,13 @@ export class UserService {
|
||||
},
|
||||
});
|
||||
|
||||
const activityTypeIds = activityTypes.map(a => a.id);
|
||||
|
||||
const mostHypedTotalCount = await tx.userBucketInterested.groupBy({
|
||||
by: ['activityXid'],
|
||||
where: {
|
||||
isActive: true,
|
||||
isBucket: false,
|
||||
Activities: {
|
||||
id: { in: connectionActivityIds },
|
||||
activityTypeXid: { in: activityTypeIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -2879,6 +3101,7 @@ export class UserService {
|
||||
isActive: true,
|
||||
isBucket: false,
|
||||
Activities: {
|
||||
id: { in: connectionActivityIds },
|
||||
activityTypeXid: { in: activityTypeIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -3013,6 +3236,7 @@ export class UserService {
|
||||
3️⃣ NEW ARRIVALS (RANKED)
|
||||
===================================================== */
|
||||
const newArrivalsWhere = {
|
||||
id: { in: connectionActivityIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -3026,6 +3250,7 @@ export class UserService {
|
||||
4️⃣ OTHER STATES ACTIVITIES (RANKED)
|
||||
===================================================== */
|
||||
const otherStatesWhere: any = {
|
||||
id: { in: connectionActivityIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -3046,6 +3271,7 @@ export class UserService {
|
||||
5️⃣ OVERSEAS ACTIVITIES (RANKED)
|
||||
===================================================== */
|
||||
const overseasWhere: any = {
|
||||
id: { in: connectionActivityIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -3066,6 +3292,12 @@ export class UserService {
|
||||
.map(p => p.sellPrice)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a - b)[0] ?? null;
|
||||
const distance = calculateDistance(
|
||||
userLatitude,
|
||||
userLongitude,
|
||||
activity.checkInLat,
|
||||
activity.checkInLong,
|
||||
);
|
||||
|
||||
return {
|
||||
interestXid: activity.activityType.interestXid,
|
||||
@@ -3073,7 +3305,7 @@ export class UserService {
|
||||
activityTitle: activity.activityTitle,
|
||||
connectionInterestedCount:
|
||||
connectionInterestMap.get(activity.id) ?? 0,
|
||||
distance: 0,
|
||||
distance,
|
||||
rating: 0,
|
||||
activityDurationMins: activity.activityDurationMins,
|
||||
sustainabilityScore: activity.sustainabilityScore,
|
||||
@@ -3092,7 +3324,7 @@ export class UserService {
|
||||
);
|
||||
|
||||
const interestsWithActivities = await Promise.all(
|
||||
networkUserInterests
|
||||
userInterests
|
||||
.sort((a, b) =>
|
||||
a.interest.interestName.localeCompare(b.interest.interestName)
|
||||
)
|
||||
@@ -3122,6 +3354,8 @@ export class UserService {
|
||||
citiesDiscovered: 10,
|
||||
loggedInNetworkCount: 0,
|
||||
citiesInNetworkCount: 0,
|
||||
interestedCount: userInterestedActivityIds.length,
|
||||
bucketCount: userBucketActivityIds.length,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
@@ -3695,4 +3929,56 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
async removeFromBucketInterested(
|
||||
userXid: number,
|
||||
isBucket: boolean,
|
||||
bucketTypeName: string,
|
||||
activityXid: number
|
||||
) {
|
||||
const activityExists = await this.prisma.activities.findFirst({
|
||||
where: { id: activityXid, isActive: true },
|
||||
});
|
||||
|
||||
if (!activityExists) {
|
||||
throw new ApiError(404, 'Activity not found');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.userBucketInterested.findFirst({
|
||||
where: { userXid, activityXid, isActive: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new ApiError(400, 'Activity not found in bucket/interested list');
|
||||
}
|
||||
|
||||
await this.prisma.userBucketInterested.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Get updated counts
|
||||
const [bucketCount, interestedCount] = await Promise.all([
|
||||
this.prisma.userBucketInterested.count({
|
||||
where: {
|
||||
userXid,
|
||||
isBucket: true,
|
||||
isActive: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.userBucketInterested.count({
|
||||
where: {
|
||||
userXid,
|
||||
isBucket: false,
|
||||
isActive: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
bucketCount,
|
||||
interestedCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user