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.
102 lines
4.8 KiB
TypeScript
102 lines
4.8 KiB
TypeScript
// GraphQL queries for platform entities
|
|
// Platform remaps SDK field names — all LINKS/PHONES fields need subfield selection.
|
|
//
|
|
// Each entity exports a query *builder* that accepts an optional `after`
|
|
// cursor. The data-provider paginates until `hasNextPage=false` so the
|
|
// dashboard KPIs reflect real totals instead of the first 100 rows. The
|
|
// previous hardcoded `first: 100` caps caused supervisor KPI cards to
|
|
// quietly plateau at 100 on busy tenants.
|
|
//
|
|
// `pageSize` is intentionally large (200) to keep round-trips low. The
|
|
// platform Relay pagination accepts up to 1000 but 200 is a good balance
|
|
// between latency per page and number of pages on active workspaces.
|
|
|
|
const PAGE_SIZE = 200;
|
|
|
|
const cursorArg = (after?: string): string => (after ? `, after: "${after}"` : '');
|
|
|
|
export const leadsQuery = (after?: string) => `{ leads(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ createdAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt updatedAt
|
|
contactName { firstName lastName }
|
|
contactPhone { primaryPhoneNumber primaryPhoneCallingCode }
|
|
contactEmail { primaryEmail }
|
|
source status priority interestedService assignedAgent
|
|
utmSource utmMedium utmCampaign utmContent utmTerm landingPage { primaryLinkUrl } referrerUrl
|
|
leadScore spamScore isSpam isDuplicate
|
|
firstContacted lastContacted contactAttempts convertedAt
|
|
patientId campaignId
|
|
aiSummary aiSuggestedAction
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const campaignsQuery = (after?: string) => `{ campaigns(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ createdAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt updatedAt
|
|
campaignName typeCustom status platform
|
|
startDate endDate
|
|
budget { amountMicros currencyCode }
|
|
amountSpent { amountMicros currencyCode }
|
|
impressions clicks targetCount contacted converted leadsGenerated
|
|
externalCampaignId platformUrl { primaryLinkUrl }
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const adsQuery = (after?: string) => `{ ads(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ createdAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt updatedAt
|
|
adName externalAdId status format
|
|
headline adDescription destinationUrl { primaryLinkUrl } previewUrl { primaryLinkUrl }
|
|
impressions clicks conversions
|
|
spend { amountMicros currencyCode }
|
|
campaignId
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const followUpsQuery = (after?: string) => `{ followUps(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt
|
|
typeCustom status scheduledAt completedAt
|
|
priority assignedAgent
|
|
patientId
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const leadActivitiesQuery = (after?: string) => `{ leadActivities(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt
|
|
activityType summary occurredAt performedBy
|
|
previousValue newValue
|
|
channel durationSec outcome
|
|
leadId
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const callsQuery = (after?: string) => `{ calls(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt
|
|
direction callStatus callerNumber { primaryPhoneNumber } agentName
|
|
startedAt endedAt durationSec
|
|
recording { primaryLinkUrl } disposition sla
|
|
patientId appointmentId leadId
|
|
agentId agent { id name ozonetelAgentId }
|
|
transferredTo transferType
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const appointmentsQuery = (after?: string) => `{ appointments(first: ${PAGE_SIZE}${cursorArg(after)}, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
|
id name createdAt
|
|
scheduledAt durationMin appointmentType status
|
|
doctorName department reasonForVisit
|
|
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
|
doctor { id fullName { firstName lastName } }
|
|
clinicId clinic { id clinicName }
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
export const patientsQuery = (after?: string) => `{ patients(first: ${PAGE_SIZE}${cursorArg(after)}) { edges { node {
|
|
id name fullName { firstName lastName }
|
|
phones { primaryPhoneNumber }
|
|
emails { primaryEmail }
|
|
dateOfBirth gender patientType
|
|
} } pageInfo { hasNextPage endCursor } } }`;
|
|
|
|
// Doctors are a small reference set (< 50 per workspace) — no pagination
|
|
// needed. Left as a plain string for the single consumer that reads it.
|
|
export const DOCTORS_QUERY = `{ doctors(first: 50) { edges { node {
|
|
id name fullName { firstName lastName }
|
|
department specialty qualifications yearsOfExperience
|
|
visitingHours
|
|
consultationFeeNew { amountMicros currencyCode }
|
|
consultationFeeFollowUp { amountMicros currencyCode }
|
|
active registrationNumber
|
|
clinic { id name clinicName }
|
|
} } } }`;
|