mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
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.
174 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
};
|