diff --git a/src/modules/user/handlers/itinerary/afterBookingFromCalendar.ts b/src/modules/user/handlers/itinerary/afterBookingFromCalendar.ts index 1ecb490..ae754de 100644 --- a/src/modules/user/handlers/itinerary/afterBookingFromCalendar.ts +++ b/src/modules/user/handlers/itinerary/afterBookingFromCalendar.ts @@ -43,6 +43,7 @@ export const handler = safeHandler(async ( } const result = await itineraryService.getActivityDetailsAfterBooking( + userId, itineraryHeaderXid, ); diff --git a/src/modules/user/services/itinerary.service.ts b/src/modules/user/services/itinerary.service.ts index f81e173..f55d661 100644 --- a/src/modules/user/services/itinerary.service.ts +++ b/src/modules/user/services/itinerary.service.ts @@ -269,6 +269,28 @@ const formatDateKey = (date: Date) => { const addMinutes = (date: Date, minutes: number) => new Date(date.getTime() + minutes * 60 * 1000); +const formatTicketTime = (value?: string | null) => { + if (!value) { + return null; + } + + return value.trim().toUpperCase().replace(/\s+(AM|PM)$/i, '$1'); +}; + +const formatTicketTimeRange = ( + startTime?: string | null, + endTime?: string | null, +) => { + const formattedStartTime = formatTicketTime(startTime); + const formattedEndTime = formatTicketTime(endTime); + + if (formattedStartTime && formattedEndTime) { + return `${formattedStartTime} - ${formattedEndTime}`; + } + + return formattedStartTime ?? formattedEndTime ?? null; +}; + const getDateRange = (fromDate: Date, toDate: Date) => { const dates: Date[] = []; const cursor = startOfDay(fromDate); @@ -4512,13 +4534,29 @@ export class ItineraryService { }; } - async getActivityDetailsAfterBooking(itineraryHeaderXid: number) { - // Fetch the itinerary header with complete details + async getActivityDetailsAfterBooking( + userXid: number, + itineraryHeaderXid: number, + ) { const itineraryHeader = await this.prisma.itineraryHeader.findFirst({ where: { id: itineraryHeaderXid, isActive: true, deletedAt: null, + OR: [ + { + ownerXid: userXid, + }, + { + ItineraryMembers: { + some: { + memberXid: userXid, + isActive: true, + deletedAt: null, + }, + }, + }, + ], }, select: { id: true, @@ -4539,6 +4577,220 @@ export class ItineraryService { mobileNumber: true, }, }, + ItineraryMembers: { + where: { + memberXid: userXid, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + memberXid: true, + memberRole: true, + memberStatus: true, + member: { + select: { + id: true, + firstName: true, + lastName: true, + emailAddress: true, + mobileNumber: true, + profileImage: true, + }, + }, + }, + }, + ItineraryActivities: { + where: { + activityXid: { + not: null, + }, + isActive: true, + deletedAt: null, + }, + orderBy: [ + { + occurenceDate: 'asc', + }, + { + startTime: 'asc', + }, + { + displayOrder: 'asc', + }, + ], + select: { + id: true, + displayOrder: true, + occurenceDate: true, + startTime: true, + endTime: true, + paxCount: true, + totalAmount: true, + bookingStatus: true, + venue: { + select: { + id: true, + venueName: true, + venueLabel: true, + }, + }, + ItineraryDetails: { + where: { + itineraryKind: 'ACTIVITY', + isActive: true, + deletedAt: null, + itineraryMember: { + is: { + memberXid: userXid, + isActive: true, + deletedAt: null, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + select: { + id: true, + itineraryMemberXid: true, + offlineCode: true, + description1: true, + description2: true, + activityStatus: true, + itineraryStatus: true, + isPaid: true, + paidOn: true, + createdAt: true, + }, + }, + itineraryActivitySelections: { + where: { + isActive: true, + deletedAt: null, + itineraryMember: { + is: { + memberXid: userXid, + isActive: true, + deletedAt: null, + }, + }, + }, + select: { + id: true, + itineraryMemberXid: true, + isFoodOpted: true, + isTrainerOpted: true, + isInActivityNavigationOpted: true, + activityNavigationMode: { + select: { + id: true, + navigationModeName: true, + }, + }, + selectedFoodTypes: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + activityFoodTypeXid: true, + activityFoodType: { + select: { + id: true, + foodTypeXid: true, + foodType: { + select: { + id: true, + foodTypeName: true, + }, + }, + }, + }, + }, + }, + selectedEquipments: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + activityEquipmentXid: true, + activityEquipment: { + select: { + id: true, + equipmentName: true, + }, + }, + }, + }, + }, + }, + activity: { + select: { + id: true, + activityTitle: true, + activityDescription: true, + checkInAddress: true, + checkInLat: true, + checkInLong: true, + activityDurationMins: true, + foodAvailable: true, + trainerAvailable: true, + equipmentAvailable: true, + pickUpDropAvailable: true, + inActivityAvailable: true, + ActivitiesMedia: { + where: { + isActive: true, + deletedAt: null, + }, + orderBy: { + displayOrder: 'asc', + }, + select: { + id: true, + mediaType: true, + mediaFileName: true, + isCoverImage: true, + displayOrder: true, + }, + }, + ActivityPickUpDetails: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + isPickUp: true, + locationLat: true, + locationLong: true, + locationAddress: true, + }, + }, + ActivityOtherDetails: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + exclusiveNotes: true, + SafetyInstruction: true, + Cancellations: true, + dosNotes: true, + dontsNotes: true, + tipsNotes: true, + termsAndCondition: true, + }, + take: 1, + }, + }, + }, + }, + }, }, }); @@ -4546,122 +4798,186 @@ export class ItineraryService { throw new ApiError(404, 'Itinerary not found'); } - // Find all itinerary activities linked to this itinerary header - const itineraryActivities = await this.prisma.itineraryActivities.findMany({ - where: { - itineraryHeaderXid, - activityXid: { - not: null, - }, - isActive: true, - deletedAt: null, - }, - orderBy: [ - { - occurenceDate: 'asc', - }, - { - startTime: 'asc', - }, - ], - select: { - id: true, - occurenceDate: true, - startTime: true, - endTime: true, - activity: { - select: { - id: true, - activityTitle: true, - activityDescription: true, - checkInAddress: true, - checkInLat: true, - checkInLong: true, - activityDurationMins: true, - ActivitiesMedia: { - where: { - isActive: true, - deletedAt: null, - }, - orderBy: { - displayOrder: 'asc', - }, - select: { - id: true, - mediaType: true, - mediaFileName: true, - isCoverImage: true, - displayOrder: true, - }, - }, - ActivityOtherDetails: { - where: { - isActive: true, - deletedAt: null, - }, - select: { - exclusiveNotes: true, - SafetyInstruction: true, - Cancellations: true, - dosNotes: true, - dontsNotes: true, - tipsNotes: true, - termsAndCondition: true, - }, - take: 1, - }, - }, - }, - }, - }); + const itineraryMember = itineraryHeader.ItineraryMembers[0] ?? null; - if (itineraryActivities.length === 0) { + if (itineraryHeader.ItineraryActivities.length === 0) { throw new ApiError(404, 'No activities found for this itinerary'); } - // Process all activities const activities = await Promise.all( - itineraryActivities.map(async (itineraryActivity) => { - // Generate check-in code - const checkInCode = generateCheckInCode(); - - // Attach presigned URLs to media files + itineraryHeader.ItineraryActivities.map(async (itineraryActivity) => { + const itineraryDetail = itineraryActivity.ItineraryDetails[0] ?? null; + const activitySelection = + itineraryActivity.itineraryActivitySelections[0] ?? null; + const selectedFoodTypes = ( + activitySelection?.selectedFoodTypes ?? [] + ).map( + (selectedFoodType) => + selectedFoodType.activityFoodType.foodType.foodTypeName, + ); + const selectedEquipments = ( + activitySelection?.selectedEquipments ?? [] + ).map( + (selectedEquipment) => + selectedEquipment.activityEquipment.equipmentName, + ); + const selectedNavigationMode = + activitySelection?.activityNavigationMode?.navigationModeName ?? null; const mediaWithUrls = await Promise.all( - (itineraryActivity.activity?.ActivitiesMedia || []).map( + (itineraryActivity.activity?.ActivitiesMedia ?? []).map( async (media) => ({ ...media, mediaUrl: await attachPresignedUrl(media.mediaFileName), }), ), ); + const coverImage = + mediaWithUrls.find((media) => media.isCoverImage) ?? + mediaWithUrls[0] ?? + null; + const pickUpDetail = + itineraryActivity.activity?.ActivityPickUpDetails.find( + (detail) => detail.isPickUp && detail.locationAddress, + ) ?? + itineraryActivity.activity?.ActivityPickUpDetails.find( + (detail) => detail.locationAddress, + ) ?? + null; + const dropDetail = + itineraryActivity.activity?.ActivityPickUpDetails.find( + (detail) => !detail.isPickUp && detail.locationAddress, + ) ?? null; + const bookingDate = formatDateKey(itineraryActivity.occurenceDate); + const ticketTime = formatTicketTimeRange( + itineraryActivity.startTime, + itineraryActivity.endTime, + ); + const venueName = + itineraryActivity.venue?.venueLabel ?? + itineraryActivity.venue?.venueName ?? + null; + const foodIncluded = selectedFoodTypes.length > 0 ? 'Yes' : 'No'; + const equipmentIncluded = selectedEquipments.length > 0 ? 'Yes' : 'No'; + const pickAndDropIncluded = itineraryActivity.activity?.pickUpDropAvailable + ? 'Yes' + : 'No'; + const inActivityNavigation = + selectedNavigationMode ?? + (activitySelection?.isInActivityNavigationOpted ? 'Yes' : 'No'); - // Build activity details return { itineraryActivityId: itineraryActivity.id, - activityId: itineraryActivity.activity?.id, - activityTitle: itineraryActivity.activity?.activityTitle, - activityDescription: itineraryActivity.activity?.activityDescription, + bookingId: itineraryHeader.itineraryNo, + activityId: itineraryActivity.activity?.id ?? null, + activityTitle: itineraryActivity.activity?.activityTitle ?? null, + activityDescription: + itineraryActivity.activity?.activityDescription ?? null, occurenceDate: itineraryActivity.occurenceDate, startTime: itineraryActivity.startTime, endTime: itineraryActivity.endTime, - duration: itineraryActivity.activity?.activityDurationMins, - checkInCode, - checkInAddress: itineraryActivity.activity?.checkInAddress, - checkInLat: itineraryActivity.activity?.checkInLat, - checkInLong: itineraryActivity.activity?.checkInLong, + duration: itineraryActivity.activity?.activityDurationMins ?? null, + checkInCode: itineraryDetail?.offlineCode ?? null, + qrCodeValue: itineraryDetail?.offlineCode ?? null, + checkInAddress: itineraryActivity.activity?.checkInAddress ?? null, + checkInLat: itineraryActivity.activity?.checkInLat ?? null, + checkInLong: itineraryActivity.activity?.checkInLong ?? null, + bookingStatus: + itineraryDetail?.activityStatus ?? + itineraryActivity.bookingStatus ?? + null, + description1: itineraryDetail?.description1 ?? null, + description2: itineraryDetail?.description2 ?? null, + itineraryStatus: itineraryDetail?.itineraryStatus ?? null, + isPaid: itineraryDetail?.isPaid ?? false, + paidOn: itineraryDetail?.paidOn ?? null, + paxCount: itineraryActivity.paxCount ?? null, + totalAmount: itineraryActivity.totalAmount ?? null, + venue: { + id: itineraryActivity.venue?.id ?? null, + venueName: itineraryActivity.venue?.venueName ?? null, + venueLabel: itineraryActivity.venue?.venueLabel ?? null, + displayName: venueName, + }, images: mediaWithUrls.map((media) => ({ id: media.id, type: media.mediaType, + fileName: media.mediaFileName, url: media.mediaUrl, isCover: media.isCoverImage, order: media.displayOrder, })), - coverImage: mediaWithUrls - .filter((m) => m.isCoverImage) - .map((media) => ({ - id: media.id, - url: media.mediaUrl, - })), + coverImage: coverImage + ? { + id: coverImage.id, + fileName: coverImage.mediaFileName, + url: coverImage.mediaUrl, + } + : null, + bookingInformation: { + activityName: itineraryActivity.activity?.activityTitle ?? null, + date: bookingDate, + venue: venueName, + venueName: itineraryActivity.venue?.venueName ?? null, + venueLabel: itineraryActivity.venue?.venueLabel ?? null, + time: ticketTime, + startTime: formatTicketTime(itineraryActivity.startTime), + endTime: formatTicketTime(itineraryActivity.endTime), + }, + bookingIncluded: { + food: foodIncluded, + selectedFoodTypes, + equipment: equipmentIncluded, + selectedEquipments, + pickAndDrop: pickAndDropIncluded, + pickupLocation: + pickUpDetail?.locationAddress ?? + itineraryActivity.activity?.checkInAddress ?? + null, + dropLocation: dropDetail?.locationAddress ?? null, + inActivityNavigation: inActivityNavigation ?? 'No', + trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No', + }, + ticketCard: { + qrCodeValue: itineraryDetail?.offlineCode ?? null, + checkInCode: itineraryDetail?.offlineCode ?? null, + activityTitle: itineraryActivity.activity?.activityTitle ?? null, + date: bookingDate, + venue: venueName, + time: ticketTime, + food: foodIncluded, + equipments: equipmentIncluded, + pickAndDrop: pickAndDropIncluded, + inActivityNavigation: inActivityNavigation ?? 'No', + }, + userSelections: { + isFoodOpted: activitySelection?.isFoodOpted ?? false, + selectedFoodTypes, + isTrainerOpted: activitySelection?.isTrainerOpted ?? false, + isInActivityNavigationOpted: + activitySelection?.isInActivityNavigationOpted ?? false, + selectedNavigationMode: selectedNavigationMode, + selectedEquipments, + }, + pickupDetails: { + pickUpDropAvailable: + itineraryActivity.activity?.pickUpDropAvailable ?? false, + pickUpLocation: pickUpDetail + ? { + id: pickUpDetail.id, + locationAddress: pickUpDetail.locationAddress, + locationLat: pickUpDetail.locationLat, + locationLong: pickUpDetail.locationLong, + } + : null, + dropLocation: dropDetail + ? { + id: dropDetail.id, + locationAddress: dropDetail.locationAddress, + locationLat: dropDetail.locationLat, + locationLong: dropDetail.locationLong, + } + : null, + }, activityInfo: { exclusiveNotes: itineraryActivity.activity?.ActivityOtherDetails[0] @@ -4709,7 +5025,24 @@ export class ItineraryService { email: itineraryHeader.owner?.emailAddress, phone: itineraryHeader.owner?.mobileNumber, }, + viewer: itineraryMember + ? { + itineraryMemberXid: itineraryMember.id, + memberXid: itineraryMember.memberXid, + memberRole: itineraryMember.memberRole, + memberStatus: itineraryMember.memberStatus, + firstName: itineraryMember.member.firstName, + lastName: itineraryMember.member.lastName, + email: itineraryMember.member.emailAddress, + phone: itineraryMember.member.mobileNumber, + profileImage: itineraryMember.member.profileImage, + profileImagePresignedUrl: await attachPresignedUrl( + itineraryMember.member.profileImage, + ), + } + : null, totalActivities: activities.length, + ticketCards: activities.map((activity) => activity.ticketCard), activities, }, };