diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 09208bc..9166857 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -61,7 +61,8 @@ interface HostDocumentInput { export async function generateActivityRefNumber( tx: any, hostXid: number, - activityTypeXid: number + activityTypeXid: number, + hostRefNumber: string ) { // 1️⃣ Get ActivityType with Interest const activityType = await tx.activityTypes.findUnique({ @@ -131,7 +132,7 @@ export async function generateActivityRefNumber( const nextActivityTypeSequence = activityTypeCount + 1; - return `E-${interestCode}${String(interestSequence).padStart( + return `${hostRefNumber}-E-${interestCode}${String(interestSequence).padStart( 3, "0" )}-${String(nextActivityTypeSequence).padStart(2, "0")}`; @@ -2763,6 +2764,18 @@ export class HostService { frequenciesXid: number, ) { return await this.prisma.$transaction(async (tx) => { + + const hostUserDetail = await tx.user.findFirst({ + where: { id: userId, isActive: true}, + select: { + id: true, + userRefNumber: true, + } + }) + + if(!hostUserDetail) { + throw new ApiError(404, 'User not found'); + } const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true }, }); @@ -2770,6 +2783,10 @@ export class HostService { const activityType = await tx.activityTypes.findUnique({ where: { id: activityTypeXid }, + include: { + interests: true, // ✅ correct + energyLevel: true, // ✅ this is correct already + }, }); if (!activityType) throw new ApiError(404, 'Activity type not found'); @@ -2780,7 +2797,7 @@ export class HostService { if (!freq) throw new ApiError(404, 'Frequency not found'); } - const referenceNumber = await generateActivityRefNumber(tx, host.id, activityTypeXid); + const referenceNumber = await generateActivityRefNumber(tx, host.id, activityTypeXid, hostUserDetail.userRefNumber); const created = await tx.activities.create({ data: { diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 49e9c2f..ceb7347 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -193,6 +193,7 @@ async function rankAndPaginateActivities( whereClause: any, page: number, limit: number, + connectionInterestMap ) { const skip = (page - 1) * limit; @@ -330,6 +331,8 @@ async function rankAndPaginateActivities( // activityDurationMins: activity.activityDurationMins, // sustainabilityScore: activity.sustainabilityScore, // cheapestPrice, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, energyLevel: activity.activityType.energyLevel ? { ...activity.activityType.energyLevel, @@ -704,6 +707,45 @@ export class UserService { }; } + 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]; // impossible user id + + 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; /* ===================================================== @@ -890,6 +932,8 @@ export class UserService { mostHypedSorted.map(async (activity) => ({ activityId: activity.id, activityTitle: activity.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, hypeCount: activity.hypeCount, energyLevel: activity.activityType.energyLevel ? { @@ -926,6 +970,7 @@ export class UserService { newArrivalsWhere, page, limit, + connectionInterestMap ); /* ===================================================== @@ -949,7 +994,76 @@ export class UserService { otherStatesWhere, page, limit, + connectionInterestMap ); + // ===================================================== + // 6️⃣ RANDOM ACTIVITIES (5 ONLY - SIMPLE) + // ===================================================== + + const totalActiveCount = await tx.activities.count({ + where: { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + deletedAt: null, + }, + }); + + let randomActivities: any[] = []; + + if (totalActiveCount > 0) { + const takeCount = Math.min(5, totalActiveCount); + + const randomOffsets = new Set(); + 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, + }, + 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 attachPresignedUrl(cover.mediaFileName) + : null, + }; + }), + ); + } /* ===================================================== 5️⃣ OVERSEAS ACTIVITIES (RANKED) @@ -969,6 +1083,7 @@ export class UserService { overseasWhere, page, limit, + connectionInterestMap ); const formattedActivities = await Promise.all( @@ -982,6 +1097,8 @@ export class UserService { return { interestXid: activity.activityType.interestXid, activityId: activity.id, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, activityTitle: activity.activityTitle, activityDurationMins: activity.activityDurationMins, sustainabilityScore: activity.sustainabilityScore, @@ -1024,14 +1141,16 @@ export class UserService { return { userAddressDetails, - experiencesLogged: 25, - citiesDiscovered: 10, + experiencesLogged: 0, + citiesDiscovered: 0, loggedInNetworkCount: 0, citiesInNetworkCount: 0, + rating: 0, pagination: { page, limit, }, + randomActivities, interests: interestsWithActivities, otherStatesActivities: formattedOtherStatesActivities, overSeasActivities: formattedOverSeasActivities, @@ -1112,6 +1231,51 @@ export class UserService { const skip = (page - 1) * limit; + /* ===================================================== + CONNECTION INTEREST MAP +===================================================== */ + + const userConnectionDetails = await tx.connectDetails.findMany({ + where: { userXid: userId, isActive: true }, + select: { schoolCompanyXid: true }, + }); + + const otherConnectionUsers = await tx.connectDetails.findMany({ + where: { + userXid: { not: userId }, + isActive: true, + schoolCompanyXid: { + in: userConnectionDetails.map((u) => u.schoolCompanyXid), + }, + }, + select: { userXid: true }, + }); + + // Prevent empty IN crash + const connectionUserIds = + otherConnectionUsers.length > 0 + ? otherConnectionUsers.map((u) => u.userXid) + : [-1]; + + // Only bucket = true (important!) + const connectionInterestByActivity = + await tx.userBucketInterested.groupBy({ + by: ['activityXid'], + where: { + userXid: { in: connectionUserIds }, + isActive: true, + isBucket: true, + }, + _count: { activityXid: true }, + }); + + const connectionInterestMap = new Map( + connectionInterestByActivity.map((item) => [ + item.activityXid, + item._count.activityXid, + ]), + ); + /* ===================================================== 3️⃣ OTHER INTERESTS (GROUPED WITH ACTIVITIES) ===================================================== */ @@ -1157,6 +1321,8 @@ export class UserService { otherInterestActivities.map(async (a) => ({ interestXid: a.activityType.interestXid, activityId: a.id, + connectionInterestedCount: + connectionInterestMap.get(a.id) ?? 0, activityTitle: a.activityTitle, energyLevel: { ...a.activityType.energyLevel, @@ -1234,6 +1400,8 @@ export class UserService { activityId: act.id, activityTitle: act.activityTitle, hypeCount: g._count.activityXid, + connectionInterestedCount: + connectionInterestMap.get(act.id) ?? 0, energyLevel: { ...act.activityType.energyLevel, presignedUrl: await attachPresignedUrl( @@ -1264,6 +1432,7 @@ export class UserService { take: limit, orderBy: { id: 'desc' }, select: { + id: true, activityTitle: true, activityType: { select: { energyLevel: true } }, ActivitiesMedia: { @@ -1303,6 +1472,7 @@ export class UserService { skip, take: limit, select: { + id: true, activityTitle: true, activityType: { select: { energyLevel: true } }, ActivitiesMedia: { @@ -1316,6 +1486,7 @@ export class UserService { skip, take: limit, select: { + id: true, activityTitle: true, activityType: { select: { energyLevel: true } }, ActivitiesMedia: { @@ -1349,6 +1520,8 @@ export class UserService { activities: await Promise.all( newArrivalsRaw.map(async (a) => ({ activityTitle: a.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(a.id) ?? 0, energyLevel: { ...a.activityType.energyLevel, presignedUrl: await attachPresignedUrl( @@ -1368,6 +1541,8 @@ export class UserService { activities: await Promise.all( otherStatesRaw.map(async (a) => ({ activityTitle: a.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(a.id) ?? 0, energyLevel: { ...a.activityType.energyLevel, presignedUrl: await attachPresignedUrl( @@ -1387,6 +1562,8 @@ export class UserService { activities: await Promise.all( overseasRaw.map(async (a) => ({ activityTitle: a.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(a.id) ?? 0, energyLevel: { ...a.activityType.energyLevel, presignedUrl: await attachPresignedUrl( @@ -1735,13 +1912,47 @@ export class UserService { ); } + // 🔹 Get connection users + const userConnectionDetails = await tx.connectDetails.findMany({ + where: { userXid: userId, isActive: true }, + select: { schoolCompanyXid: true }, + }); + + const schoolCompanyXids = userConnectionDetails.map( + (c) => c.schoolCompanyXid, + ); + + const connectionUsers = await tx.connectDetails.findMany({ + where: { + isActive: true, + schoolCompanyXid: { + in: schoolCompanyXids.length ? schoolCompanyXids : [-1], + }, + userXid: { not: userId }, + }, + select: { userXid: true }, + }); + + const connectionUserIds = connectionUsers.map((u) => u.userXid); + const interestedCount = await tx.userBucketInterested.count({ where: { activityXid, + isBucket: false, isActive: true, }, }); + const connectionInterestedCount = connectionUserIds.length + ? await tx.userBucketInterested.count({ + where: { + activityXid, + userXid: { in: connectionUserIds }, + isActive: true, + }, + }) + : 0; + const prices = activity.ActivityVenues.flatMap((v) => v.ActivityPrices.map((p) => p.sellPrice), ).filter((p) => p !== null) as number[]; @@ -1755,6 +1966,7 @@ export class UserService { return { activity, interestedCount, + connectionInterestedCount, cheapestPrice, totalCapacity, rating: 0, // ⭐ Placeholder, implement rating logic as needed @@ -1898,6 +2110,27 @@ export class UserService { const skip = (page - 1) * limit; + // 0.5️⃣ Get connection users + const userConnectionDetails = await this.prisma.connectDetails.findMany({ + where: { userXid: userId, isActive: true }, + select: { schoolCompanyXid: true }, + }); + + const schoolCompanyXids = userConnectionDetails.map( + (c) => c.schoolCompanyXid, + ); + + const connectionUsers = await this.prisma.connectDetails.findMany({ + where: { + isActive: true, + schoolCompanyXid: { in: schoolCompanyXids.length ? schoolCompanyXids : [-1] }, + userXid: { not: userId }, + }, + select: { userXid: true }, + }); + + const connectionUserIds = connectionUsers.map((u) => u.userXid); + // 0️⃣ Get user's interests and map to activity types const userInterests = await this.prisma.userInterests.findMany({ where: { userXid: userId, isActive: true }, @@ -2012,6 +2245,31 @@ export class UserService { .filter((a) => a.distanceKm <= radiusKm) .sort((a, b) => a.distanceKm - b.distanceKm); + const nearbyActivityIds = withDistance.map((a) => a.id); + + let connectionInterestMap = new Map(); + + if (nearbyActivityIds.length && connectionUserIds.length) { + const connectionInterestCounts = + await this.prisma.userBucketInterested.groupBy({ + by: ['activityXid'], + where: { + activityXid: { in: nearbyActivityIds }, + userXid: { in: connectionUserIds }, + isActive: true, + isBucket: true, // ✅ only real interest + }, + _count: { activityXid: true }, + }); + + connectionInterestMap = new Map( + connectionInterestCounts.map((item) => [ + item.activityXid, + item._count.activityXid, + ]), + ); + } + const totalCount = withDistance.length; const paged = withDistance.slice(skip, skip + limit); @@ -2026,8 +2284,11 @@ export class UserService { return { activityId: activity.id, activityTitle: activity.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, activityDurationMins: activity.activityDurationMins, sustainabilityScore: activity.sustainabilityScore, + rating: 0, distanceKm: activity.distanceKm, cheapestPrice, energyLevel: activity.activityType.energyLevel @@ -2343,6 +2604,42 @@ 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'], + where: { + userXid: { in: connectionUserIds }, + isActive: true, + }, + _count: { + activityXid: true, + }, + }); + + const connectionInterestMap = new Map( + connectionInterestByActivity.map(item => [ + item.activityXid, + item._count.activityXid, + ]) + ); + /* ===================================================== 1️⃣ FETCH ALL CANDIDATES FOR INTERESTS (SIMPLE SORT) ===================================================== */ @@ -2563,7 +2860,7 @@ export class UserService { createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) } }; - const formattedNewArrivalsActivities = await rankAndPaginateActivities(tx, newArrivalsWhere, page, limit); + const formattedNewArrivalsActivities = await rankAndPaginateActivities(tx, newArrivalsWhere, page, limit, connectionInterestMap); /* ===================================================== 4️⃣ OTHER STATES ACTIVITIES (RANKED) @@ -2582,7 +2879,7 @@ export class UserService { otherStatesWhere.checkInStateXid = { not: effectiveStateXid }; } - const formattedOtherStatesActivities = await rankAndPaginateActivities(tx, otherStatesWhere, page, limit); + const formattedOtherStatesActivities = await rankAndPaginateActivities(tx, otherStatesWhere, page, limit, connectionInterestMap); /* ===================================================== @@ -2599,7 +2896,7 @@ export class UserService { overseasWhere.checkInCountryXid = { not: effectiveCountryXid }; } - const formattedOverSeasActivities = await rankAndPaginateActivities(tx, overseasWhere, page, limit); + const formattedOverSeasActivities = await rankAndPaginateActivities(tx, overseasWhere, page, limit, connectionInterestMap); const formattedActivities = await Promise.all( @@ -2614,8 +2911,11 @@ export class UserService { interestXid: activity.activityType.interestXid, activityId: activity.id, activityTitle: activity.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, activityDurationMins: activity.activityDurationMins, sustainabilityScore: activity.sustainabilityScore, + rating: 0, cheapestPrice, energyLevel: activity.activityType.energyLevel ? { @@ -2841,6 +3141,42 @@ 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'], + where: { + userXid: { in: connectionUserIds }, + isActive: true, + }, + _count: { + activityXid: true, + }, + }); + + const connectionInterestMap = new Map( + connectionInterestByActivity.map(item => [ + item.activityXid, + item._count.activityXid, + ]) + ); + /* ======================================================= SWITCH BASED VIEW MORE TYPE ======================================================= */ @@ -2874,7 +3210,7 @@ export class UserService { amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, }; - return await rankAndPaginateActivities(tx, where, page, limit); + return await rankAndPaginateActivities(tx, where, page, limit, connectionInterestMap); } /* ========================================== @@ -2891,7 +3227,7 @@ export class UserService { }, }; - return await rankAndPaginateActivities(tx, where, page, limit); + return await rankAndPaginateActivities(tx, where, page, limit, connectionInterestMap); } /* ========================================== @@ -2913,7 +3249,7 @@ export class UserService { where.checkInStateXid = { not: effectiveStateXid }; } - return await rankAndPaginateActivities(tx, where, page, limit); + return await rankAndPaginateActivities(tx, where, page, limit, connectionInterestMap); } /* ========================================== @@ -2931,7 +3267,7 @@ export class UserService { where.checkInCountryXid = { not: effectiveCountryXid }; } - return await rankAndPaginateActivities(tx, where, page, limit); + return await rankAndPaginateActivities(tx, where, page, limit, connectionInterestMap); } default: @@ -3021,7 +3357,7 @@ export class UserService { id: true, activityTitle: true, ActivitiesMedia: { - where: { + where: { isActive: true, }, select: { @@ -3063,4 +3399,84 @@ export class UserService { }); } + async getFiveRandomActivities() { + return await this.prisma.$transaction(async (tx) => { + + // Step 1: Count eligible activities + const totalCount = await tx.activities.count({ + where: { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + deletedAt: null, + }, + }); + + if (totalCount === 0) return []; + + // Step 2: Generate 5 unique random offsets + const takeCount = Math.min(5, totalCount); + const randomOffsets = new Set(); + + while (randomOffsets.size < takeCount) { + randomOffsets.add(Math.floor(Math.random() * totalCount)); + } + + // Step 3: Fetch activities using skip (efficient for small limit like 5) + const activities = 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, + }, + select: { + id: true, + activityTitle: true, + ActivitiesMedia: { + where: { + isActive: true, + }, + orderBy: { + displayOrder: 'asc', + }, + take: 1, + select: { + mediaFileName: true, + }, + }, + }, + }) + ) + ); + + // Step 4: Attach presigned URLs + const result = await Promise.all( + activities + .filter(Boolean) + .map(async (activity) => { + const media = activity!.ActivitiesMedia?.[0]; + + let presignedUrl = null; + + if (media?.mediaFileName) { + presignedUrl = await attachPresignedUrl(media.mediaFileName); + } + + return { + id: activity!.id, + title: activity!.activityTitle, + coverImage: media?.mediaFileName ?? null, + coverImagePresignedUrl: presignedUrl, + }; + }) + ); + + return result; + }); + } + } \ No newline at end of file