Files
helix-engage/src/providers/data-provider.tsx
saridsa2 196a18fe1a feat(data-provider): paginate entity queries + suppress polling-loading flash
Two related fixes:

1. KPIs were capped at 100. The data-provider's entity queries were hardcoded to first: 100; on Global the supervisor dashboard showed 'Total Calls: 100' this week while the AI assistant (which paginates) reported 182. Converted each query to a cursor-aware builder, added a generic fetchAll(rootField, builder) that loops until hasNextPage=false (capped at 25 pages × 200 as a runaway guard). Page size bumped 100→200 to cut round-trips on active tenants.

2. Every 30s background poll flipped loading=true, flashing a 'Loading...' overlay across supervisor surfaces. hasLoadedRef guards the flag so only the initial fetch triggers the loading state.
2026-04-15 18:56:04 +05:30

174 lines
6.6 KiB
TypeScript

import type { ReactNode } from 'react';
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { apiClient } from '@/lib/api-client';
import {
leadsQuery,
campaignsQuery,
adsQuery,
followUpsQuery,
leadActivitiesQuery,
callsQuery,
appointmentsQuery,
patientsQuery,
} from '@/lib/queries';
import {
transformLeads,
transformCampaigns,
transformAds,
transformFollowUps,
transformLeadActivities,
transformCalls,
transformAppointments,
transformPatients,
} from '@/lib/transforms';
import type { Lead, Campaign, Ad, LeadActivity, FollowUp, WhatsAppTemplate, Agent, Call, LeadIngestionSource, Patient, Appointment } from '@/types/entities';
type DataContextType = {
leads: Lead[];
campaigns: Campaign[];
ads: Ad[];
followUps: FollowUp[];
leadActivities: LeadActivity[];
templates: WhatsAppTemplate[];
agents: Agent[];
calls: Call[];
appointments: Appointment[];
patients: Patient[];
ingestionSources: LeadIngestionSource[];
loading: boolean;
error: string | null;
updateLead: (id: string, updates: Partial<Lead>) => void;
addCall: (call: Call) => void;
refresh: () => void;
};
const DataContext = createContext<DataContextType | undefined>(undefined);
export const useData = (): DataContextType => {
const context = useContext(DataContext);
if (context === undefined) {
throw new Error('useData must be used within a DataProvider');
}
return context;
};
interface DataProviderProps {
children: ReactNode;
}
export const DataProvider = ({ children }: DataProviderProps) => {
const [leads, setLeads] = useState<Lead[]>([]);
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
const [ads, setAds] = useState<Ad[]>([]);
const [followUps, setFollowUps] = useState<FollowUp[]>([]);
const [leadActivities, setLeadActivities] = useState<LeadActivity[]>([]);
const [calls, setCalls] = useState<Call[]>([]);
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const hasLoadedRef = useRef(false);
// These don't have platform entities yet — empty for now
const [templates] = useState<WhatsAppTemplate[]>([]);
const [agents] = useState<Agent[]>([]);
const [ingestionSources] = useState<LeadIngestionSource[]>([]);
const fetchData = useCallback(async () => {
if (!apiClient.isAuthenticated()) {
setLoading(false);
return;
}
// Only flip the global loading flag on the very first fetch. Background
// polls refresh data in place so the UI doesn't flash "Loading..." —
// QA reported this as the supervisor surfaces randomly refreshing.
if (!hasLoadedRef.current) {
setLoading(true);
}
setError(null);
try {
const gql = <T,>(query: string) => apiClient.graphql<T>(query, undefined, { silent: true }).catch(() => null);
// Generic Relay pagination. Keeps paging until hasNextPage=false
// or we hit MAX_PAGES (guard against runaway loops on bad data).
// Returned shape mirrors the original single-page response so
// transformX functions work unchanged — they already read
// `{ <rootField>: { edges } }`.
const MAX_PAGES = 25;
const fetchAll = async (rootField: string, builder: (after?: string) => string): Promise<any | null> => {
const allEdges: any[] = [];
let after: string | undefined = undefined;
for (let page = 0; page < MAX_PAGES; page++) {
const data: any = await gql<any>(builder(after));
if (!data) return null;
const root: any = data[rootField];
if (!root) break;
if (Array.isArray(root.edges)) allEdges.push(...root.edges);
if (!root.pageInfo?.hasNextPage) break;
after = root.pageInfo.endCursor;
if (!after) break;
}
return { [rootField]: { edges: allEdges } };
};
const [leadsData, campaignsData, adsData, followUpsData, activitiesData, callsData, appointmentsData, patientsData] = await Promise.all([
fetchAll('leads', leadsQuery),
fetchAll('campaigns', campaignsQuery),
fetchAll('ads', adsQuery),
fetchAll('followUps', followUpsQuery),
fetchAll('leadActivities', leadActivitiesQuery),
fetchAll('calls', callsQuery),
fetchAll('appointments', appointmentsQuery),
fetchAll('patients', patientsQuery),
]);
if (leadsData) setLeads(transformLeads(leadsData));
if (campaignsData) setCampaigns(transformCampaigns(campaignsData));
if (adsData) setAds(transformAds(adsData));
if (followUpsData) setFollowUps(transformFollowUps(followUpsData));
if (activitiesData) setLeadActivities(transformLeadActivities(activitiesData));
if (callsData) setCalls(transformCalls(callsData));
if (appointmentsData) setAppointments(transformAppointments(appointmentsData));
if (patientsData) setPatients(transformPatients(patientsData));
} catch (err: any) {
setError(err.message ?? 'Failed to load data');
} finally {
hasLoadedRef.current = true;
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
// Poll every 30 seconds for fresh data (calls, leads, appointments)
const interval = setInterval(() => {
console.log('[DATA-PROVIDER] Polling for fresh data');
fetchData();
}, 30_000);
return () => clearInterval(interval);
}, [fetchData]);
const updateLead = (id: string, updates: Partial<Lead>) => {
setLeads((prev) => prev.map((lead) => (lead.id === id ? { ...lead, ...updates } : lead)));
};
const addCall = (call: Call) => {
setCalls((prev) => [call, ...prev]);
};
return (
<DataContext.Provider value={{
leads, campaigns, ads, followUps, leadActivities, templates, agents, calls, appointments, patients, ingestionSources,
loading, error,
updateLead, addCall, refresh: fetchData,
}}>
{children}
</DataContext.Provider>
);
};