@@ -17,9 +17,11 @@ import { extractDateRangeFromColumnFilters } from "@calcom/features/insights/lib
1717import type { DateRange } from "@calcom/features/insights/server/insightsDateUtils" ;
1818import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository" ;
1919import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service" ;
20+ import { SYSTEM_PHONE_FIELDS } from "@calcom/lib/bookings/SystemField" ;
2021import type { PrismaClient } from "@calcom/prisma" ;
2122import { Prisma } from "@calcom/prisma/client" ;
2223import { MembershipRole } from "@calcom/prisma/enums" ;
24+ import { eventTypeBookingFields } from "@calcom/prisma/zod-utils" ;
2325
2426// Utility function to build user hash map with avatar URL fallback
2527export const buildHashMapForUsers = <
@@ -552,7 +554,7 @@ export class InsightsBookingBaseService {
552554 return { data : csvData , total : totalCount } ;
553555 }
554556
555- // 2. Get all bookings with their attendees and seat references
557+ // 2. Get all bookings with their attendees, seat references, and phone data
556558 const bookings = await this . prisma . booking . findMany ( {
557559 where : {
558560 uid : {
@@ -561,10 +563,12 @@ export class InsightsBookingBaseService {
561563 } ,
562564 select : {
563565 uid : true ,
566+ eventTypeId : true ,
564567 attendees : {
565568 select : {
566569 name : true ,
567570 email : true ,
571+ phoneNumber : true ,
568572 noShow : true ,
569573 } ,
570574 } ,
@@ -574,66 +578,124 @@ export class InsightsBookingBaseService {
574578 select : {
575579 name : true ,
576580 email : true ,
581+ phoneNumber : true ,
577582 noShow : true ,
578583 } ,
579584 } ,
580585 } ,
581586 } ,
587+ responses : true ,
588+ eventType : {
589+ select : {
590+ bookingFields : true ,
591+ } ,
592+ } ,
582593 } ,
583594 } ) ;
584595
585- // 3. Create booking map with attendee data (matching original logic)
586- const bookingMap = new Map (
587- bookings . map ( ( booking ) => {
588- const attendeeList =
589- booking . seatsReferences . length > 0
590- ? booking . seatsReferences . map ( ( ref ) => ref . attendee )
591- : booking . attendees ;
592-
593- // List all no-show guests (name and email)
594- const noShowGuests =
595- attendeeList
596- . filter ( ( attendee ) => attendee ?. noShow )
597- . map ( ( attendee ) => ( attendee ? `${ attendee . name } (${ attendee . email } )` : null ) )
598- . filter ( Boolean ) // remove null values
599- . join ( "; " ) || null ;
600- const noShowGuestsCount = attendeeList . filter ( ( attendee ) => attendee ?. noShow ) . length ;
601-
602- const formattedAttendees = attendeeList
603- . map ( ( attendee ) => ( attendee ? `${ attendee . name } (${ attendee . email } )` : null ) )
604- . filter ( Boolean ) ;
605-
606- return [ booking . uid , { attendeeList : formattedAttendees , noShowGuests, noShowGuestsCount } ] ;
607- } )
608- ) ;
596+ // 3. Process bookings: extract phone data and build attendee map
597+ const phoneFieldsCache = new Map < number , { name : string ; label : string } [ ] > ( ) ;
598+ const allPhoneFieldLabels = new Set < string > ( ) ;
599+ let maxAttendees = 0 ;
600+ const finalBookingMap = new Map <
601+ string ,
602+ {
603+ noShowGuests : string | null ;
604+ noShowGuestsCount : number ;
605+ attendeeList : string [ ] ;
606+ attendeePhoneNumbers : ( string | null ) [ ] ;
607+ phoneQuestionResponses : Record < string , string | null > ;
608+ }
609+ > ( ) ;
609610
610- // 4. Calculate max attendees for dynamic columns
611- const maxAttendees = Math . max (
612- ...Array . from ( bookingMap . values ( ) ) . map ( ( data ) => data . attendeeList . length ) ,
613- 0
614- ) ;
611+ const extractPhoneValue = ( value : unknown ) : string | null => {
612+ if ( typeof value === "string" && value . trim ( ) ) return value ;
613+ if ( value && typeof value === "object" && "value" in value ) {
614+ const val = ( value as { value : unknown } ) . value ;
615+ if ( typeof val === "string" && val . trim ( ) ) return val ;
616+ }
617+ return null ;
618+ } ;
619+
620+ for ( const booking of bookings ) {
621+ const eventTypeId = booking . eventTypeId ;
622+ let phoneFields : { name : string ; label : string } [ ] | null = null ;
623+
624+ if ( eventTypeId ) {
625+ if ( phoneFieldsCache . has ( eventTypeId ) ) {
626+ phoneFields = phoneFieldsCache . get ( eventTypeId ) || null ;
627+ } else if ( booking . eventType ?. bookingFields ) {
628+ const parsed = eventTypeBookingFields . safeParse ( booking . eventType . bookingFields ) ;
629+ if ( parsed . success ) {
630+ phoneFields = parsed . data
631+ . filter ( ( field ) => field . type === "phone" && ! SYSTEM_PHONE_FIELDS . has ( field . name ) )
632+ . map ( ( field ) => ( { name : field . name , label : field . label || field . name } ) ) ;
633+ phoneFieldsCache . set ( eventTypeId , phoneFields ) ;
634+ phoneFields . forEach ( ( field ) => allPhoneFieldLabels . add ( field . label ) ) ;
635+ }
636+ }
637+ }
615638
616- // 5. Create final booking map with attendee fields
617- const finalBookingMap = new Map (
618- Array . from ( bookingMap . entries ( ) ) . map ( ( [ uid , data ] ) => {
619- const attendeeFields : Record < string , string | null > = { } ;
639+ const attendeeList =
640+ booking . seatsReferences . length > 0
641+ ? booking . seatsReferences . map ( ( ref ) => ref . attendee )
642+ : booking . attendees ;
620643
621- for ( let i = 1 ; i <= maxAttendees ; i ++ ) {
622- attendeeFields [ `attendee${ i } ` ] = data . attendeeList [ i - 1 ] || null ;
644+ const formattedAttendees : string [ ] = [ ] ;
645+ const noShowAttendees : string [ ] = [ ] ;
646+ const attendeePhoneNumbers : ( string | null ) [ ] = [ ] ;
647+ let noShowGuestsCount = 0 ;
648+
649+ const phoneQuestionResponses : Record < string , string | null > = { } ;
650+ let systemPhoneValue : string | null = null ;
651+
652+ if ( booking . responses && typeof booking . responses === "object" ) {
653+ const responses = booking . responses as Record < string , unknown > ;
654+
655+ systemPhoneValue =
656+ extractPhoneValue ( responses . attendeePhoneNumber ) ||
657+ extractPhoneValue ( responses . smsReminderNumber ) ||
658+ null ;
659+
660+ if ( phoneFields ) {
661+ for ( const field of phoneFields ) {
662+ phoneQuestionResponses [ field . label ] = extractPhoneValue ( responses [ field . name ] ) ;
663+ }
623664 }
665+ }
624666
625- return [
626- uid ,
627- {
628- noShowGuests : data . noShowGuests ,
629- noShowGuestsCount : data . noShowGuestsCount ,
630- ...attendeeFields ,
631- } ,
632- ] ;
633- } )
634- ) ;
667+ const firstPhoneQuestionValue = Object . values ( phoneQuestionResponses ) . find ( ( v ) => v !== null ) || null ;
668+ const phoneFallback = systemPhoneValue || firstPhoneQuestionValue ;
669+
670+ for ( const attendee of attendeeList ) {
671+ if ( attendee ) {
672+ const formatted = `${ attendee . name } (${ attendee . email } )` ;
673+ formattedAttendees . push ( formatted ) ;
674+ attendeePhoneNumbers . push ( attendee . phoneNumber || phoneFallback ) ;
675+ if ( attendee . noShow ) {
676+ noShowAttendees . push ( formatted ) ;
677+ noShowGuestsCount ++ ;
678+ }
679+ }
680+ }
681+
682+ if ( formattedAttendees . length > maxAttendees ) {
683+ maxAttendees = formattedAttendees . length ;
684+ }
635685
636- // 6. Combine booking data with attendee data and add ISO timestamp columns
686+ // List all no-show guests (name and email)
687+ const noShowGuests = noShowAttendees . length > 0 ? noShowAttendees . join ( "; " ) : null ;
688+
689+ finalBookingMap . set ( booking . uid , {
690+ noShowGuests,
691+ noShowGuestsCount,
692+ attendeeList : formattedAttendees ,
693+ attendeePhoneNumbers,
694+ phoneQuestionResponses,
695+ } ) ;
696+ }
697+
698+ // 4. Combine booking data with attendee data and format for CSV
637699 const data = csvData . map ( ( bookingTimeStatus ) => {
638700 const dateAndTime = {
639701 createdAt : bookingTimeStatus . createdAt . toISOString ( ) ,
@@ -647,46 +709,25 @@ export class InsightsBookingBaseService {
647709 endTime_time : dayjs ( bookingTimeStatus . endTime ) . tz ( timeZone ) . format ( TIME_FORMAT ) ,
648710 } ;
649711
650- if ( ! bookingTimeStatus . uid ) {
651- // should not be reached because we filtered above
652- const nullAttendeeFields : Record < string , null > = { } ;
653- for ( let i = 1 ; i <= maxAttendees ; i ++ ) {
654- nullAttendeeFields [ `attendee${ i } ` ] = null ;
655- }
656-
657- return {
658- ...bookingTimeStatus ,
659- ...dateAndTime ,
660- noShowGuests : null ,
661- noShowGuestsCount : 0 ,
662- ...nullAttendeeFields ,
663- } ;
664- }
665-
666- const attendeeData = finalBookingMap . get ( bookingTimeStatus . uid ) ;
667-
668- if ( ! attendeeData ) {
669- const nullAttendeeFields : Record < string , null > = { } ;
670- for ( let i = 1 ; i <= maxAttendees ; i ++ ) {
671- nullAttendeeFields [ `attendee${ i } ` ] = null ;
672- }
673-
674- return {
675- ...bookingTimeStatus ,
676- ...dateAndTime ,
677- noShowGuests : null ,
678- noShowGuestsCount : 0 ,
679- ...nullAttendeeFields ,
680- } ;
681- }
712+ const attendeeData = bookingTimeStatus . uid ? finalBookingMap . get ( bookingTimeStatus . uid ) : null ;
682713
683- return {
714+ const result : Record < string , unknown > = {
684715 ...bookingTimeStatus ,
685716 ...dateAndTime ,
686- noShowGuests : attendeeData . noShowGuests ,
687- noShowGuestsCount : attendeeData . noShowGuestsCount ,
688- ...Object . fromEntries ( Object . entries ( attendeeData ) . filter ( ( [ key ] ) => key . startsWith ( "attendee" ) ) ) ,
717+ noShowGuests : attendeeData ?. noShowGuests || null ,
718+ noShowGuestsCount : attendeeData ?. noShowGuestsCount || 0 ,
689719 } ;
720+
721+ for ( let i = 1 ; i <= maxAttendees ; i ++ ) {
722+ result [ `attendee${ i } ` ] = attendeeData ?. attendeeList [ i - 1 ] || null ;
723+ result [ `attendeePhone${ i } ` ] = attendeeData ?. attendeePhoneNumbers [ i - 1 ] || null ;
724+ }
725+
726+ allPhoneFieldLabels . forEach ( ( label ) => {
727+ result [ label ] = attendeeData ?. phoneQuestionResponses [ label ] || null ;
728+ } ) ;
729+
730+ return result ;
690731 } ) ;
691732
692733 return { data, total : totalCount } ;
0 commit comments