Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions src/ops-console/components/campaignSetup/useCampaignReach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,47 +21,29 @@ export const useCampaignReach = (selectedSchoolIds: string[]) => {
if (
selectedSchoolIds.length === 0 ||
!api.getParentWhatsappClassesBySchoolId ||
!api.getParentWhatsappParentPhonesByClassId
!api.getCampaignParentsInGroupBySchoolIds
) {
setCampaignReach(emptyReach);
return;
}

setLoadingReach(true);
try {
const schoolClasses = await Promise.all(
selectedSchoolIds.map((schoolId) =>
api.getParentWhatsappClassesBySchoolId!(schoolId),
),
);

const groupedClasses = schoolClasses
.flat()
.filter(
(classRow) =>
classRow.group_id && String(classRow.group_id).trim() !== '',
);
const schoolClasses =
await api.getParentWhatsappClassesBySchoolId(selectedSchoolIds);
Comment on lines +32 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since getParentWhatsappClassesBySchoolId is an optional method on the API interface, calling it directly without the non-null assertion operator (!) will cause a TypeScript compilation error under strict null checks. Please restore the ! operator.

Suggested change
const schoolClasses =
await api.getParentWhatsappClassesBySchoolId(selectedSchoolIds);
const schoolClasses =
await api.getParentWhatsappClassesBySchoolId!(selectedSchoolIds);


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 groupedClasses = schoolClasses.filter(
(classRow) =>
classRow.group_id && String(classRow.group_id).trim() !== '',
);

const memberCount =
await api.getCampaignParentsInGroupBySchoolIds(selectedSchoolIds);
Comment on lines +40 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since getCampaignParentsInGroupBySchoolIds is an optional method on the API interface, calling it directly without the non-null assertion operator (!) will cause a TypeScript compilation error under strict null checks. Please add the ! operator.

Suggested change
const memberCount =
await api.getCampaignParentsInGroupBySchoolIds(selectedSchoolIds);
const memberCount =
await api.getCampaignParentsInGroupBySchoolIds!(selectedSchoolIds);

Comment on lines +40 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve reach metrics in SQLite mode

When the app is initialized with APIMode.SQLITE, api is still an ApiHandler, so the earlier method-existence guard passes because ApiHandler defines getCampaignParentsInGroupBySchoolIds; however SqliteApi does not implement this optional delegate, unlike the existing class/parent-phone WhatsApp delegates, so this call throws Campaign parents-in-group metrics lookup is not implemented.... The previous implementation worked in SQLite mode via the existing delegates, but campaign setup/communication now logs an error and leaves the reach widget empty or stale in that environment; please add the SQLite-to-Supabase delegate or keep a fallback path.

Useful? React with 👍 / 👎.


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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ const fetchClassesForSchool = async (
api: ApiHandler,
schoolId: string,
): Promise<DirectClassRow[]> => {
return await api.getParentWhatsappClassesBySchoolId(schoolId);
return await api.getParentWhatsappClassesBySchoolId([schoolId]);
};

// Fetches WhatsApp group details via parent WhatsApp dedicated RPC path.
Expand Down
16 changes: 14 additions & 2 deletions src/services/api/ApiHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -1940,6 +1940,18 @@ export class ApiHandler implements ServiceApi {
}
return await this.s.getParentWhatsappParentPhonesByClassId(classId);
}

public async getCampaignParentsInGroupBySchoolIds(
schoolIds: string[],
): Promise<number> {
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,
Expand Down
11 changes: 9 additions & 2 deletions src/services/api/ServiceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2855,8 +2855,8 @@ export interface ServiceApi {

getClassesBySchoolId(schoolId: string): Promise<TableTypes<'class'>[]>;

// 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;
Expand All @@ -2870,6 +2870,13 @@ export interface ServiceApi {
classId: string,
) => Promise<string[]>;

/**
* Returns the seven-day parents-in-group total for selected campaign schools.
*/
getCampaignParentsInGroupBySchoolIds?: (
schoolIds: string[],
) => Promise<number>;

/**
* Creates a auto student profile for a parent and returns the student object
* @param {string} languageDocId - languageDocId is `Language` doc id
Expand Down
4 changes: 2 additions & 2 deletions src/services/api/SqliteApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The SqliteApi class implements ServiceApi, but it is missing the implementation for the new getCampaignParentsInGroupBySchoolIds method. If SqliteApi is active (e.g., in local or offline environments), calling this method via ApiHandler will throw an error because this.s.getCampaignParentsInGroupBySchoolIds is undefined.

Please implement getCampaignParentsInGroupBySchoolIds in SqliteApi to forward the call to _serverApi (similar to getParentWhatsappClassesBySchoolId).

  }

  async getCampaignParentsInGroupBySchoolIds(
    schoolIds: string[],
  ): Promise<number> {
    if (!this._serverApi || !this._serverApi.getCampaignParentsInGroupBySchoolIds) {
      throw new Error(
        'Campaign parents-in-group metrics lookup is not implemented in Supabase API.',
      );
    }
    return await this._serverApi.getCampaignParentsInGroupBySchoolIds(schoolIds);
  }


// Parent WhatsApp Invitation: parent phones from class_user -> user join.
Expand Down
38 changes: 34 additions & 4 deletions src/services/api/SupabaseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,8 @@ const CAMPAIGN_LISTING_NATIVE_SORT_COLUMNS: Partial<
endDate: 'end_date',
};

const CAMPAIGN_REACH_METRIC_WINDOW = '7d';

const isCampaignListingRelationSort = (
orderBy: NonNullable<CampaignListingParams['orderBy']>,
) => orderBy === 'manager' || orderBy === 'programName';
Expand Down Expand Up @@ -4378,25 +4380,26 @@ 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;
group_id?: string | null;
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;
Expand Down Expand Up @@ -4442,6 +4445,33 @@ export class SupabaseApi implements ServiceApi {
return Array.from(phoneSet);
}

async getCampaignParentsInGroupBySchoolIds(
schoolIds: string[],
): Promise<number> {
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<TableTypes<'user'>[]> {
if (!this.supabase || userIds.length === 0) return [];

Expand Down
3 changes: 3 additions & 0 deletions src/services/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading