mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: wire frontend to platform data, migrate to Jotai + Vercel AI SDK
- Replace mock DataProvider with real GraphQL queries through sidecar - Add queries.ts and transforms.ts for platform field name mapping - Migrate SIP state from React Context to Jotai atoms (React 19 compat) - Add singleton SIP manager to survive StrictMode remounts - Remove hardcoded Olivia/Sienna accounts from nav menu - Add password eye toggle, remember me checkbox, forgot password link - Fix worklist hook to transform platform field names - Add seed scripts for clinics, health packages, lab tests - Update test harness for new doctor→clinic relation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
74
src/lib/queries.ts
Normal file
74
src/lib/queries.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// GraphQL queries for platform entities
|
||||
// Platform remaps some SDK field names — queries use platform names
|
||||
|
||||
export const LEADS_QUERY = `{ leads(first: 100, 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 referrerUrl
|
||||
leadScore spamScore isSpam isDuplicate duplicateOfLeadId
|
||||
firstContacted lastContacted contactAttempts convertedAt
|
||||
patientId campaignId
|
||||
aiSummary aiSuggestedAction
|
||||
} } } }`;
|
||||
|
||||
export const CAMPAIGNS_QUERY = `{ campaigns(first: 50, 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
|
||||
} } } }`;
|
||||
|
||||
export const ADS_QUERY = `{ ads(first: 100, orderBy: [{ createdAt: DescNullsLast }]) { edges { node {
|
||||
id name createdAt updatedAt
|
||||
adName externalAdId adStatus adFormat
|
||||
headline adDescription destinationUrl previewUrl
|
||||
impressions clicks conversions
|
||||
spend { amountMicros currencyCode }
|
||||
campaignId
|
||||
} } } }`;
|
||||
|
||||
export const FOLLOW_UPS_QUERY = `{ followUps(first: 50, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
||||
id name createdAt
|
||||
typeCustom status scheduledAt completedAt
|
||||
priority assignedAgent
|
||||
patientId callId
|
||||
} } } }`;
|
||||
|
||||
export const LEAD_ACTIVITIES_QUERY = `{ leadActivities(first: 200, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node {
|
||||
id name createdAt
|
||||
activityType summary occurredAt performedBy
|
||||
previousValue newValue
|
||||
channel durationSeconds outcome
|
||||
leadId
|
||||
} } } }`;
|
||||
|
||||
export const CALLS_QUERY = `{ calls(first: 100, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
|
||||
id name createdAt
|
||||
direction callStatus callerNumber agentName
|
||||
startedAt endedAt durationSec
|
||||
recordingUrl disposition
|
||||
patientId appointmentId leadId
|
||||
} } } }`;
|
||||
|
||||
export const DOCTORS_QUERY = `{ doctors(first: 20) { edges { node {
|
||||
id name fullName { firstName lastName }
|
||||
department specialty qualifications yearsOfExperience
|
||||
visitingHours
|
||||
consultationFeeNew { amountMicros currencyCode }
|
||||
consultationFeeFollowUp { amountMicros currencyCode }
|
||||
active registrationNumber
|
||||
clinic { id name clinicName }
|
||||
} } } }`;
|
||||
|
||||
export const PATIENTS_QUERY = `{ patients(first: 50) { edges { node {
|
||||
id name fullName { firstName lastName }
|
||||
phones { primaryPhoneNumber }
|
||||
emails { primaryEmail }
|
||||
dateOfBirth gender patientType
|
||||
} } } }`;
|
||||
152
src/lib/transforms.ts
Normal file
152
src/lib/transforms.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// Transform platform GraphQL responses → frontend entity types
|
||||
// Platform remaps some field names during sync
|
||||
|
||||
import type { Lead, Campaign, Ad, FollowUp, LeadActivity, Call } from '@/types/entities';
|
||||
|
||||
type PlatformNode = Record<string, any>;
|
||||
|
||||
function extractEdges(data: any, entityName: string): PlatformNode[] {
|
||||
return data?.[entityName]?.edges?.map((e: any) => e.node) ?? [];
|
||||
}
|
||||
|
||||
export function transformLeads(data: any): Lead[] {
|
||||
return extractEdges(data, 'leads').map((n) => ({
|
||||
id: n.id,
|
||||
createdAt: n.createdAt,
|
||||
updatedAt: n.updatedAt,
|
||||
contactName: n.contactName ?? { firstName: '', lastName: '' },
|
||||
contactPhone: n.contactPhone?.primaryPhoneNumber
|
||||
? [{ number: n.contactPhone.primaryPhoneNumber, callingCode: n.contactPhone.primaryPhoneCallingCode ?? '+91' }]
|
||||
: [],
|
||||
contactEmail: n.contactEmail?.primaryEmail
|
||||
? [{ address: n.contactEmail.primaryEmail }]
|
||||
: [],
|
||||
leadSource: n.source,
|
||||
leadStatus: n.status,
|
||||
priority: n.priority ?? 'NORMAL',
|
||||
interestedService: n.interestedService,
|
||||
assignedAgent: n.assignedAgent,
|
||||
utmSource: n.utmSource,
|
||||
utmMedium: n.utmMedium,
|
||||
utmCampaign: n.utmCampaign,
|
||||
utmContent: n.utmContent,
|
||||
utmTerm: n.utmTerm,
|
||||
landingPageUrl: n.landingPage,
|
||||
referrerUrl: n.referrerUrl,
|
||||
leadScore: n.leadScore,
|
||||
spamScore: n.spamScore ?? 0,
|
||||
isSpam: n.isSpam ?? false,
|
||||
isDuplicate: n.isDuplicate ?? false,
|
||||
duplicateOfLeadId: n.duplicateOfLeadId,
|
||||
firstContactedAt: n.firstContacted,
|
||||
lastContactedAt: n.lastContacted,
|
||||
contactAttempts: n.contactAttempts ?? 0,
|
||||
convertedAt: n.convertedAt,
|
||||
patientId: n.patientId,
|
||||
campaignId: n.campaignId,
|
||||
adId: null,
|
||||
aiSummary: n.aiSummary,
|
||||
aiSuggestedAction: n.aiSuggestedAction,
|
||||
}));
|
||||
}
|
||||
|
||||
export function transformCampaigns(data: any): Campaign[] {
|
||||
return extractEdges(data, 'campaigns').map((n) => ({
|
||||
id: n.id,
|
||||
createdAt: n.createdAt,
|
||||
updatedAt: n.updatedAt,
|
||||
campaignName: n.campaignName ?? n.name,
|
||||
campaignType: n.typeCustom,
|
||||
campaignStatus: n.status,
|
||||
platform: n.platform,
|
||||
startDate: n.startDate,
|
||||
endDate: n.endDate,
|
||||
budget: n.budget ? { amountMicros: n.budget.amountMicros, currencyCode: n.budget.currencyCode } : null,
|
||||
amountSpent: n.amountSpent ? { amountMicros: n.amountSpent.amountMicros, currencyCode: n.amountSpent.currencyCode } : null,
|
||||
impressionCount: n.impressions ?? 0,
|
||||
clickCount: n.clicks ?? 0,
|
||||
targetCount: n.targetCount ?? 0,
|
||||
contactedCount: n.contacted ?? 0,
|
||||
convertedCount: n.converted ?? 0,
|
||||
leadCount: n.leadsGenerated ?? 0,
|
||||
externalCampaignId: n.externalCampaignId,
|
||||
platformUrl: n.platformUrl,
|
||||
}));
|
||||
}
|
||||
|
||||
export function transformAds(data: any): Ad[] {
|
||||
return extractEdges(data, 'ads').map((n) => ({
|
||||
id: n.id,
|
||||
createdAt: n.createdAt,
|
||||
updatedAt: n.updatedAt,
|
||||
adName: n.adName ?? n.name,
|
||||
externalAdId: n.externalAdId,
|
||||
adStatus: n.adStatus,
|
||||
adFormat: n.adFormat,
|
||||
headline: n.headline,
|
||||
adDescription: n.adDescription,
|
||||
destinationUrl: n.destinationUrl,
|
||||
previewUrl: n.previewUrl,
|
||||
impressions: n.impressions ?? 0,
|
||||
clicks: n.clicks ?? 0,
|
||||
conversions: n.conversions ?? 0,
|
||||
spend: n.spend ? { amountMicros: n.spend.amountMicros, currencyCode: n.spend.currencyCode } : null,
|
||||
campaignId: n.campaignId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function transformFollowUps(data: any): FollowUp[] {
|
||||
return extractEdges(data, 'followUps').map((n) => ({
|
||||
id: n.id,
|
||||
createdAt: n.createdAt,
|
||||
followUpType: n.typeCustom,
|
||||
followUpStatus: n.status,
|
||||
scheduledAt: n.scheduledAt,
|
||||
completedAt: n.completedAt,
|
||||
priority: n.priority ?? 'NORMAL',
|
||||
assignedAgent: n.assignedAgent,
|
||||
patientId: n.patientId,
|
||||
callId: n.callId,
|
||||
patientName: undefined,
|
||||
patientPhone: undefined,
|
||||
description: n.name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function transformLeadActivities(data: any): LeadActivity[] {
|
||||
return extractEdges(data, 'leadActivities').map((n) => ({
|
||||
id: n.id,
|
||||
createdAt: n.createdAt,
|
||||
activityType: n.activityType,
|
||||
summary: n.summary,
|
||||
occurredAt: n.occurredAt,
|
||||
performedBy: n.performedBy,
|
||||
previousValue: n.previousValue,
|
||||
newValue: n.newValue,
|
||||
channel: n.channel,
|
||||
durationSeconds: n.durationSeconds,
|
||||
outcome: n.outcome,
|
||||
activityNotes: null,
|
||||
leadId: n.leadId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function transformCalls(data: any): Call[] {
|
||||
return extractEdges(data, 'calls').map((n) => ({
|
||||
id: n.id,
|
||||
createdAt: n.createdAt,
|
||||
callDirection: n.direction,
|
||||
callStatus: n.callStatus,
|
||||
callerNumber: n.callerNumber ? [{ number: n.callerNumber, callingCode: '+91' }] : [],
|
||||
agentName: n.agentName,
|
||||
startedAt: n.startedAt,
|
||||
endedAt: n.endedAt,
|
||||
durationSeconds: n.durationSec ?? 0,
|
||||
recordingUrl: n.recordingUrl,
|
||||
disposition: n.disposition,
|
||||
callNotes: undefined,
|
||||
patientId: n.patientId,
|
||||
appointmentId: n.appointmentId,
|
||||
leadId: n.leadId,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user