From be1d7939e009bf19bd283555faee40e1a0c1ce85 Mon Sep 17 00:00:00 2001 From: narayanangourav-sutara Date: Thu, 16 Jul 2026 17:58:25 +0530 Subject: [PATCH 1/2] fix: The existing parent-phone method remains unchanged for the separate Parent WhatsApp Invitation feature. --- .../campaignSetup/useCampaignReach.ts | 21 +++----------- src/services/api/ApiHandler.ts | 12 ++++++++ src/services/api/ServiceApi.ts | 7 +++++ src/services/api/SupabaseApi.ts | 29 +++++++++++++++++++ src/services/database.ts | 3 ++ 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/ops-console/components/campaignSetup/useCampaignReach.ts b/src/ops-console/components/campaignSetup/useCampaignReach.ts index 069be7b6c2..9347e5def7 100644 --- a/src/ops-console/components/campaignSetup/useCampaignReach.ts +++ b/src/ops-console/components/campaignSetup/useCampaignReach.ts @@ -21,7 +21,7 @@ export const useCampaignReach = (selectedSchoolIds: string[]) => { if ( selectedSchoolIds.length === 0 || !api.getParentWhatsappClassesBySchoolId || - !api.getParentWhatsappParentPhonesByClassId + !api.getCampaignParentsInGroupBySchoolIds ) { setCampaignReach(emptyReach); return; @@ -42,26 +42,13 @@ export const useCampaignReach = (selectedSchoolIds: string[]) => { classRow.group_id && String(classRow.group_id).trim() !== '', ); - const memberLists = await Promise.all( - groupedClasses.map(async (classRow) => { - try { - const phones = await api.getParentWhatsappParentPhonesByClassId!( - classRow.id, - ); - return Array.from(new Set(phones)); - } catch { - return []; - } - }), - ); + const memberCount = + await api.getCampaignParentsInGroupBySchoolIds(selectedSchoolIds); if (!mounted) return; setCampaignReach({ groupCount: groupedClasses.length, - memberCount: memberLists.reduce( - (total, members) => total + members.length, - 0, - ), + memberCount, }); } catch (error) { logger.error('Failed to load campaign reach:', error); diff --git a/src/services/api/ApiHandler.ts b/src/services/api/ApiHandler.ts index 3c21d9786b..e009a08a18 100644 --- a/src/services/api/ApiHandler.ts +++ b/src/services/api/ApiHandler.ts @@ -1940,6 +1940,18 @@ export class ApiHandler implements ServiceApi { } return await this.s.getParentWhatsappParentPhonesByClassId(classId); } + + public async getCampaignParentsInGroupBySchoolIds( + schoolIds: string[], + ): Promise { + if (!this.s.getCampaignParentsInGroupBySchoolIds) { + throw new Error( + 'Campaign parents-in-group metrics lookup is not implemented in current API service.', + ); + } + return await this.s.getCampaignParentsInGroupBySchoolIds(schoolIds); + } + public async createAutoProfile( languageDocId: string | undefined, tcVersion: number, diff --git a/src/services/api/ServiceApi.ts b/src/services/api/ServiceApi.ts index 6823cebdd0..50a1d3d22f 100644 --- a/src/services/api/ServiceApi.ts +++ b/src/services/api/ServiceApi.ts @@ -2870,6 +2870,13 @@ export interface ServiceApi { classId: string, ) => Promise; + /** + * Returns the seven-day parents-in-group total for selected campaign schools. + */ + getCampaignParentsInGroupBySchoolIds?: ( + schoolIds: string[], + ) => Promise; + /** * Creates a auto student profile for a parent and returns the student object * @param {string} languageDocId - languageDocId is `Language` doc id diff --git a/src/services/api/SupabaseApi.ts b/src/services/api/SupabaseApi.ts index ed9876016f..22825db942 100644 --- a/src/services/api/SupabaseApi.ts +++ b/src/services/api/SupabaseApi.ts @@ -390,6 +390,8 @@ const CAMPAIGN_LISTING_NATIVE_SORT_COLUMNS: Partial< endDate: 'end_date', }; +const CAMPAIGN_REACH_METRIC_WINDOW = '7d'; + const isCampaignListingRelationSort = ( orderBy: NonNullable, ) => orderBy === 'manager' || orderBy === 'programName'; @@ -4442,6 +4444,33 @@ export class SupabaseApi implements ServiceApi { return Array.from(phoneSet); } + async getCampaignParentsInGroupBySchoolIds( + schoolIds: string[], + ): Promise { + if (!this.supabase || schoolIds.length === 0) return 0; + + const uniqueSchoolIds = Array.from(new Set(schoolIds)); + const { data, error } = await this.supabase + .from(TABLES.SchoolMetrics) + .select('school_id, parents_in_group') + .in('school_id', uniqueSchoolIds) + .eq('metric_window', CAMPAIGN_REACH_METRIC_WINDOW) + .eq('is_deleted', false); + + if (error) { + logger.error( + 'Error fetching campaign parents-in-group school metrics:', + error, + ); + throw error; + } + + return (data ?? []).reduce( + (total, row) => total + (row.parents_in_group ?? 0), + 0, + ); + } + async getUsersByIds(userIds: string[]): Promise[]> { if (!this.supabase || userIds.length === 0) return []; diff --git a/src/services/database.ts b/src/services/database.ts index 8cf3acb1f6..d3b9a4d4c2 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -4259,6 +4259,7 @@ export type Database = { metric_window: string | null; onboarded_students: number | null; partners: string[] | null; + parents_in_group: number | null; parents_reached: number | null; program_id: string | null; program_managers: string[] | null; @@ -4291,6 +4292,7 @@ export type Database = { metric_window?: string | null; onboarded_students?: number | null; partners?: string[] | null; + parents_in_group?: number | null; parents_reached?: number | null; program_id?: string | null; program_managers?: string[] | null; @@ -4323,6 +4325,7 @@ export type Database = { metric_window?: string | null; onboarded_students?: number | null; partners?: string[] | null; + parents_in_group?: number | null; parents_reached?: number | null; program_id?: string | null; program_managers?: string[] | null; From 60f55674c3480e940380c32f66449b986a5dcf08 Mon Sep 17 00:00:00 2001 From: narayanangourav-sutara Date: Thu, 16 Jul 2026 18:06:54 +0530 Subject: [PATCH 2/2] Optimize campaign reach using batched school metrics queries --- .../campaignSetup/useCampaignReach.ts | 17 ++++++----------- .../ParentWhatsappInvitationPageService.ts | 2 +- src/services/api/ApiHandler.ts | 4 ++-- src/services/api/ServiceApi.ts | 4 ++-- src/services/api/SqliteApi.ts | 4 ++-- src/services/api/SupabaseApi.ts | 9 +++++---- 6 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/ops-console/components/campaignSetup/useCampaignReach.ts b/src/ops-console/components/campaignSetup/useCampaignReach.ts index 9347e5def7..ac0eec59cb 100644 --- a/src/ops-console/components/campaignSetup/useCampaignReach.ts +++ b/src/ops-console/components/campaignSetup/useCampaignReach.ts @@ -29,18 +29,13 @@ export const useCampaignReach = (selectedSchoolIds: string[]) => { setLoadingReach(true); try { - const schoolClasses = await Promise.all( - selectedSchoolIds.map((schoolId) => - api.getParentWhatsappClassesBySchoolId!(schoolId), - ), - ); + const schoolClasses = + await api.getParentWhatsappClassesBySchoolId(selectedSchoolIds); - const groupedClasses = schoolClasses - .flat() - .filter( - (classRow) => - classRow.group_id && String(classRow.group_id).trim() !== '', - ); + const groupedClasses = schoolClasses.filter( + (classRow) => + classRow.group_id && String(classRow.group_id).trim() !== '', + ); const memberCount = await api.getCampaignParentsInGroupBySchoolIds(selectedSchoolIds); diff --git a/src/ops-console/pages/Parentwhatsappinvite/ParentWhatsappInvitationPageService.ts b/src/ops-console/pages/Parentwhatsappinvite/ParentWhatsappInvitationPageService.ts index cf73f58d41..f141f818ea 100644 --- a/src/ops-console/pages/Parentwhatsappinvite/ParentWhatsappInvitationPageService.ts +++ b/src/ops-console/pages/Parentwhatsappinvite/ParentWhatsappInvitationPageService.ts @@ -464,7 +464,7 @@ const fetchClassesForSchool = async ( api: ApiHandler, schoolId: string, ): Promise => { - return await api.getParentWhatsappClassesBySchoolId(schoolId); + return await api.getParentWhatsappClassesBySchoolId([schoolId]); }; // Fetches WhatsApp group details via parent WhatsApp dedicated RPC path. diff --git a/src/services/api/ApiHandler.ts b/src/services/api/ApiHandler.ts index e009a08a18..2ea7981b21 100644 --- a/src/services/api/ApiHandler.ts +++ b/src/services/api/ApiHandler.ts @@ -1914,7 +1914,7 @@ export class ApiHandler implements ServiceApi { return await this.s.getClassesBySchoolId(schoolId); } - public async getParentWhatsappClassesBySchoolId(schoolId: string): Promise< + public async getParentWhatsappClassesBySchoolId(schoolIds: string[]): Promise< { id: string; name: string; @@ -1927,7 +1927,7 @@ export class ApiHandler implements ServiceApi { 'Parent WhatsApp class lookup is not implemented in current API service.', ); } - return await this.s.getParentWhatsappClassesBySchoolId(schoolId); + return await this.s.getParentWhatsappClassesBySchoolId(schoolIds); } public async getParentWhatsappParentPhonesByClassId( diff --git a/src/services/api/ServiceApi.ts b/src/services/api/ServiceApi.ts index 50a1d3d22f..fc0fc5aca0 100644 --- a/src/services/api/ServiceApi.ts +++ b/src/services/api/ServiceApi.ts @@ -2855,8 +2855,8 @@ export interface ServiceApi { getClassesBySchoolId(schoolId: string): Promise[]>; - // Parent WhatsApp Invitation: lightweight class lookup for invite workflow. - getParentWhatsappClassesBySchoolId?: (schoolId: string) => Promise< + // Parent WhatsApp Invitation: lightweight class lookup for selected schools. + getParentWhatsappClassesBySchoolId?: (schoolIds: string[]) => Promise< { id: string; name: string; diff --git a/src/services/api/SqliteApi.ts b/src/services/api/SqliteApi.ts index 5c9cf2cea8..fcbb860766 100644 --- a/src/services/api/SqliteApi.ts +++ b/src/services/api/SqliteApi.ts @@ -7316,7 +7316,7 @@ order by } // Parent WhatsApp Invitation: class lookup with group/invite fields. - async getParentWhatsappClassesBySchoolId(schoolId: string): Promise< + async getParentWhatsappClassesBySchoolId(schoolIds: string[]): Promise< { id: string; name: string; @@ -7329,7 +7329,7 @@ order by 'Parent WhatsApp class lookup is not implemented in Supabase API.', ); } - return await this._serverApi.getParentWhatsappClassesBySchoolId(schoolId); + return await this._serverApi.getParentWhatsappClassesBySchoolId(schoolIds); } // Parent WhatsApp Invitation: parent phones from class_user -> user join. diff --git a/src/services/api/SupabaseApi.ts b/src/services/api/SupabaseApi.ts index 22825db942..a6b726265a 100644 --- a/src/services/api/SupabaseApi.ts +++ b/src/services/api/SupabaseApi.ts @@ -4380,7 +4380,7 @@ export class SupabaseApi implements ServiceApi { } // Parent WhatsApp Invitation: class lookup with group and invite fields. - async getParentWhatsappClassesBySchoolId(schoolId: string): Promise< + async getParentWhatsappClassesBySchoolId(schoolIds: string[]): Promise< { id: string; name: string; @@ -4388,17 +4388,18 @@ export class SupabaseApi implements ServiceApi { whatsapp_invite_link?: string | null; }[] > { - if (!this.supabase) return []; + if (!this.supabase || schoolIds.length === 0) return []; + const uniqueSchoolIds = Array.from(new Set(schoolIds)); const { data, error } = await this.supabase .from(TABLES.Class) .select('id, name, group_id, whatsapp_invite_link') - .eq('school_id', schoolId) + .in('school_id', uniqueSchoolIds) .eq('is_deleted', false); if (error) { logger.error( - 'Error in parent WhatsApp class lookup by school ID:', + 'Error in parent WhatsApp class lookup by school IDs:', error, ); throw error;