diff --git a/src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.ts b/src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.ts index 4cedee4..73c745c 100644 --- a/src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.ts +++ b/src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.ts @@ -47,6 +47,10 @@ export const handler = safeHandler(async ( ? Number(body.energyLevelXid) : undefined, entryTypeXid: Number(body.entryTypeXid), + groupCount: + body.groupCount !== undefined && body.groupCount !== null + ? Number(body.groupCount) + : undefined, page: body.page !== undefined ? Number(body.page) : 1, limit: body.limit !== undefined ? Number(body.limit) : 20, }; @@ -61,6 +65,7 @@ export const handler = safeHandler(async ( (payload.energyLevelXid !== undefined && Number.isNaN(payload.energyLevelXid)) || Number.isNaN(payload.entryTypeXid) || + (payload.groupCount !== undefined && Number.isNaN(payload.groupCount)) || Number.isNaN(payload.page) || Number.isNaN(payload.limit) ) { diff --git a/src/modules/user/handlers/itinerary/saveUserItinerary.ts b/src/modules/user/handlers/itinerary/saveUserItinerary.ts index f58ead4..abab142 100644 --- a/src/modules/user/handlers/itinerary/saveUserItinerary.ts +++ b/src/modules/user/handlers/itinerary/saveUserItinerary.ts @@ -36,7 +36,6 @@ export const handler = safeHandler(async ( } const activities = Array.isArray(body.activities) ? body.activities : []; - const members = Array.isArray(body.members) ? body.members : []; if (!body.startDate || !body.endDate || !body.startTime || !body.endTime) { throw new ApiError( @@ -71,10 +70,6 @@ export const handler = safeHandler(async ( endDate: body.endDate, startTime: body.startTime, endTime: body.endTime, - members: members.map((member: any) => ({ - memberXid: Number(member.memberXid), - memberRole: member.memberRole, - })), activities: activities.map((activity: any) => ({ activityXid: Number(activity.activityXid), venueXid: Number(activity.venueXid), @@ -113,7 +108,6 @@ export const handler = safeHandler(async ( }; if ( - payload.members.some((member) => Number.isNaN(member.memberXid)) || payload.activities.some( (activity) => Number.isNaN(activity.activityXid) || diff --git a/src/modules/user/services/itinerary.service.ts b/src/modules/user/services/itinerary.service.ts index ad68312..4386fd8 100644 --- a/src/modules/user/services/itinerary.service.ts +++ b/src/modules/user/services/itinerary.service.ts @@ -447,10 +447,6 @@ export class ItineraryService { endDate: string; startTime: string; endTime: string; - members?: Array<{ - memberXid: number; - memberRole?: string; - }>; activities: Array<{ activityXid: number; venueXid: number; @@ -503,64 +499,8 @@ export class ItineraryService { const itineraryNo = `ITN-${Date.now()}`; const itineraryTitle = payload.title?.trim() || itineraryNo; - const invitedMembers = Array.from( - new Map( - (payload.members ?? []) - .filter((member) => member.memberXid && member.memberXid !== ownerXid) - .map((member) => [member.memberXid, member]), - ).values(), - ); return this.prisma.$transaction(async (tx) => { - if (invitedMembers.length) { - const ownerConnectionDetails = await tx.connectDetails.findMany({ - where: { - userXid: ownerXid, - isActive: true, - deletedAt: null, - }, - select: { - schoolCompanyXid: true, - }, - }); - - const ownerSchoolCompanyXids = ownerConnectionDetails.map( - (item) => item.schoolCompanyXid, - ); - - if (!ownerSchoolCompanyXids.length) { - throw new ApiError( - 400, - 'You can invite members only when you have school/company connection details.', - ); - } - - const validMembers = await tx.connectDetails.findMany({ - where: { - isActive: true, - deletedAt: null, - schoolCompanyXid: { in: ownerSchoolCompanyXids }, - userXid: { in: invitedMembers.map((member) => member.memberXid) }, - }, - distinct: ['userXid'], - select: { - userXid: true, - }, - }); - - const validMemberIds = new Set(validMembers.map((item) => item.userXid)); - const invalidMembers = invitedMembers.filter( - (member) => !validMemberIds.has(member.memberXid), - ); - - if (invalidMembers.length) { - throw new ApiError( - 400, - 'All invited members must belong to the same school/company network as the owner.', - ); - } - } - const itineraryHeader = await tx.itineraryHeader.create({ data: { itineraryNo, @@ -586,24 +526,6 @@ export class ItineraryService { }, }); - const createdMembers = - invitedMembers.length > 0 - ? await Promise.all( - invitedMembers.map((member) => - tx.itineraryMembers.create({ - data: { - itineraryHeaderXid: itineraryHeader.id, - memberXid: member.memberXid, - memberRole: member.memberRole?.trim() || 'MEMBER', - memberStatus: 'pending', - invitedByXid: ownerXid, - isActive: true, - }, - }), - ), - ) - : []; - const createdActivities = await Promise.all( payload.activities.map(async (activityItem) => { const scheduleHeader = await tx.scheduleHeader.findFirst({ @@ -840,7 +762,7 @@ export class ItineraryService { title: itineraryHeader.title, itineraryStatus: itineraryHeader.itineraryStatus, ownerMemberXid: ownerMember.id, - membersCount: createdMembers.length + 1, + membersCount: 1, activitiesCount: createdActivities.length, members: [ { @@ -849,12 +771,6 @@ export class ItineraryService { memberRole: ownerMember.memberRole, memberStatus: ownerMember.memberStatus, }, - ...createdMembers.map((member) => ({ - id: member.id, - memberXid: member.memberXid, - memberRole: member.memberRole, - memberStatus: member.memberStatus, - })), ], activities: createdActivities, }; @@ -1122,6 +1038,7 @@ export class ItineraryService { endTime: string; energyLevelXid?: number; entryTypeXid: number; + groupCount?: number; page: number; limit: number; }, @@ -1143,8 +1060,39 @@ export class ItineraryService { ); } + if (payload.groupCount !== undefined) { + if (!Number.isInteger(payload.groupCount) || payload.groupCount <= 0) { + throw new ApiError(400, 'groupCount must be a positive integer.'); + } + } + const rangeStartDay = startOfDay(requestedStart); const rangeEndDay = startOfDay(requestedEnd); + const requestedEntryType = await this.prisma.allowedEntryTypes.findFirst({ + where: { + id: payload.entryTypeXid, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + allowedEntryTypeName: true, + }, + }); + + if (!requestedEntryType) { + throw new ApiError(404, 'Selected entry type not found.'); + } + + const isGroupEntryType = + requestedEntryType.allowedEntryTypeName.trim().toLowerCase() === 'group'; + + if (isGroupEntryType && payload.groupCount === undefined) { + throw new ApiError( + 400, + 'groupCount is required when entryTypeXid is for group.', + ); + } const activityEntries = await this.prisma.userBucketInterested.findMany({ where: { @@ -1318,7 +1266,10 @@ export class ItineraryService { where: { isActive: true, deletedAt: null, - maxCapacity: { gt: 0 }, + maxCapacity: + isGroupEntryType && payload.groupCount !== undefined + ? { gte: payload.groupCount } + : { gt: 0 }, }, select: { id: true, @@ -1446,6 +1397,14 @@ export class ItineraryService { return null; } + if ( + isGroupEntryType && + payload.groupCount !== undefined && + slot.maxCapacity < payload.groupCount + ) { + return null; + } + return { scheduleHeaderXid: header.id, slotId: slot.id, @@ -1634,6 +1593,7 @@ export class ItineraryService { endTime: payload.endTime, energyLevelXid: payload.energyLevelXid, entryTypeXid: payload.entryTypeXid, + groupCount: payload.groupCount, page: sanitizedPage, limit: sanitizedLimit, interestTypes: distinctInterests, diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 745606b..588047d 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -1148,7 +1148,9 @@ export class UserService { // 6️⃣ RANDOM ACTIVITIES (5 ONLY - SIMPLE) // ===================================================== - const totalActiveCount = await tx.activities.count({ + let randomActivities: any[] = []; + + const eligibleRandomActivityIds = await tx.activities.findMany({ where: { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, @@ -1157,53 +1159,44 @@ export class UserService { id: { notIn: allUserExcludedActivityIds.length ? allUserExcludedActivityIds - : [-1], // prevent empty notIn issue + : [-1], }, + ActivitiesMedia: { + some: { + isActive: true, + isCoverImage: true, + }, + }, + }, + select: { + id: true, }, }); - let randomActivities: any[] = []; + if (eligibleRandomActivityIds.length > 0) { + const takeCount = Math.min(5, eligibleRandomActivityIds.length); + const selectedIds = eligibleRandomActivityIds + .sort(() => Math.random() - 0.5) + .slice(0, takeCount) + .map((activity) => activity.id); - 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, - id: { - notIn: allUserExcludedActivityIds.length - ? allUserExcludedActivityIds - : [-1], // prevent empty notIn issue - }, - }, + const randomFetched = await tx.activities.findMany({ + where: { + id: { in: selectedIds }, + }, + select: { + id: true, + activityTitle: true, + ActivitiesMedia: { + where: { isActive: true, isCoverImage: true }, + orderBy: { displayOrder: 'asc' }, + take: 1, select: { - id: true, - activityTitle: true, - ActivitiesMedia: { - where: { isActive: true, isCoverImage: true }, - orderBy: { displayOrder: 'asc' }, - take: 1, - select: { - mediaFileName: true, - }, - }, + mediaFileName: true, }, - }), - ), - ); + }, + }, + }); randomActivities = await Promise.all( randomFetched @@ -1817,7 +1810,9 @@ export class UserService { RANDOM ACTIVITIES (5 COVER IMAGES) ===================================================== */ - const totalActiveCount = await tx.activities.count({ + let randomActivities: any[] = []; + + const eligibleRandomActivityIds = await tx.activities.findMany({ where: { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, @@ -1826,50 +1821,43 @@ export class UserService { id: { notIn: safeExcludedIds, }, + ActivitiesMedia: { + some: { + isActive: true, + isCoverImage: true, + }, + }, ...excludeUserInterestCondition, }, + select: { + id: true, + }, }); - let randomActivities: any[] = []; + if (eligibleRandomActivityIds.length > 0) { + const takeCount = Math.min(5, eligibleRandomActivityIds.length); + const selectedIds = eligibleRandomActivityIds + .sort(() => Math.random() - 0.5) + .slice(0, takeCount) + .map((activity) => activity.id); - 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, - id: { - notIn: safeExcludedIds, - }, - ...excludeUserInterestCondition, - }, + const randomFetched = await tx.activities.findMany({ + where: { + id: { in: selectedIds }, + }, + select: { + id: true, + activityTitle: true, + ActivitiesMedia: { + where: { isActive: true, isCoverImage: true }, + orderBy: { displayOrder: 'asc' }, + take: 1, select: { - id: true, - activityTitle: true, - ActivitiesMedia: { - where: { isActive: true, isCoverImage: true }, - orderBy: { displayOrder: 'asc' }, - take: 1, - select: { - mediaFileName: true, - }, - }, + mediaFileName: true, }, - }), - ), - ); + }, + }, + }); randomActivities = await Promise.all( randomFetched @@ -3634,7 +3622,9 @@ export class UserService { RANDOM ACTIVITIES FROM CONNECTION USERS (5 COVER IMAGES) ===================================================== */ - const totalActiveCount = await tx.activities.count({ + let randomActivities: any[] = []; + + const eligibleRandomActivityIds = await tx.activities.findMany({ where: { id: { in: connectionActivityIds }, isActive: true, @@ -3642,47 +3632,42 @@ export class UserService { amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, activityTypeXid: { in: activityTypeIds }, deletedAt: null, + ActivitiesMedia: { + some: { + isActive: true, + isCoverImage: true, + }, + }, + }, + select: { + id: true, }, }); - let randomActivities: any[] = []; + if (eligibleRandomActivityIds.length > 0) { + const takeCount = Math.min(5, eligibleRandomActivityIds.length); + const selectedIds = eligibleRandomActivityIds + .sort(() => Math.random() - 0.5) + .slice(0, takeCount) + .map((activity) => activity.id); - 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: { - id: { in: connectionActivityIds }, - isActive: true, - activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, - amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, - activityTypeXid: { in: activityTypeIds }, - deletedAt: null, - }, + const randomFetched = await tx.activities.findMany({ + where: { + id: { in: selectedIds }, + }, + select: { + id: true, + activityTitle: true, + ActivitiesMedia: { + where: { isActive: true, isCoverImage: true }, + orderBy: { displayOrder: 'asc' }, + take: 1, select: { - id: true, - activityTitle: true, - ActivitiesMedia: { - where: { isActive: true, isCoverImage: true }, - orderBy: { displayOrder: "asc" }, - take: 1, - select: { - mediaFileName: true, - }, - }, + mediaFileName: true, }, - }), - ), - ); + }, + }, + }); randomActivities = await Promise.all( randomFetched @@ -4151,56 +4136,54 @@ export class UserService { async getFiveRandomActivities() { return await this.prisma.$transaction(async (tx) => { - // Step 1: Count eligible activities - const totalCount = await tx.activities.count({ + const eligibleRandomActivityIds = await tx.activities.findMany({ where: { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, deletedAt: null, + ActivitiesMedia: { + some: { + isActive: true, + isCoverImage: true, + }, + }, + }, + select: { + id: true, }, }); - if (totalCount === 0) return []; + if (eligibleRandomActivityIds.length === 0) return []; - // Step 2: Generate 5 unique random offsets - const takeCount = Math.min(5, totalCount); - const randomOffsets = new Set(); + const takeCount = Math.min(5, eligibleRandomActivityIds.length); + const selectedIds = eligibleRandomActivityIds + .sort(() => Math.random() - 0.5) + .slice(0, takeCount) + .map((activity) => activity.id); - 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, + const activities = await tx.activities.findMany({ + where: { + id: { in: selectedIds }, + }, + select: { + id: true, + activityTitle: true, + ActivitiesMedia: { where: { isActive: true, - activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, - amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, - deletedAt: null, + isCoverImage: true, }, + orderBy: { + displayOrder: 'asc', + }, + take: 1, select: { - id: true, - activityTitle: true, - ActivitiesMedia: { - where: { - isActive: true, - }, - orderBy: { - displayOrder: 'asc', - }, - take: 1, - select: { - mediaFileName: true, - }, - }, + mediaFileName: true, }, - }) - ) - ); + }, + }, + }); // Step 4: Attach presigned URLs const result = await Promise.all( @@ -4243,7 +4226,7 @@ export class UserService { } const existing = await this.prisma.userBucketInterested.findFirst({ - where: { userXid, activityXid }, + where: { userXid, activityXid, isActive: true }, }); if (existing) {