mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 10:07:22 +00:00
feat: add worklist engine with prioritized missed calls, follow-ups, and leads
This commit is contained in:
267
src/ai/ai-chat.controller.ts
Normal file
267
src/ai/ai-chat.controller.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { Controller, Post, Body, Headers, HttpException, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
type ChatContext = {
|
||||
callerPhone?: string;
|
||||
leadId?: string;
|
||||
leadName?: string;
|
||||
};
|
||||
|
||||
type ChatRequest = {
|
||||
message: string;
|
||||
context?: ChatContext;
|
||||
};
|
||||
|
||||
type ChatResponse = {
|
||||
reply: string;
|
||||
sources: string[];
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
};
|
||||
|
||||
const HOSPITAL_KNOWLEDGE = `
|
||||
## Global Hospital — Clinic Locations
|
||||
|
||||
- **Koramangala**: #45, 80 Feet Road, Koramangala 4th Block, Bengaluru 560034. Open Mon–Sat 8 AM–8 PM, Sun 9 AM–2 PM.
|
||||
- **Whitefield**: Prestige Shantiniketan, ITPL Main Road, Whitefield, Bengaluru 560066. Open Mon–Sat 8 AM–8 PM, Sun closed.
|
||||
- **Indiranagar**: #12, 100 Feet Road, Indiranagar, Bengaluru 560038. Open Mon–Sat 9 AM–7 PM, Sun 10 AM–1 PM.
|
||||
|
||||
## Departments & Doctors
|
||||
|
||||
| Doctor | Department | Clinics | Visiting Hours | Consultation Fee (New / Follow-up) |
|
||||
|---|---|---|---|---|
|
||||
| Dr. Sharma | Cardiology | Koramangala, Whitefield | Mon/Wed/Fri 10 AM–1 PM | ₹800 / ₹500 |
|
||||
| Dr. Patel | Gynecology | Indiranagar, Koramangala | Tue/Thu/Sat 9 AM–12 PM | ₹700 / ₹400 |
|
||||
| Dr. Kumar | Orthopedics | Whitefield | Mon–Fri 2 PM–5 PM | ₹600 / ₹400 |
|
||||
| Dr. Reddy | General Medicine | Koramangala, Indiranagar, Whitefield | Mon–Sat 9 AM–6 PM | ₹500 / ₹300 |
|
||||
| Dr. Singh | ENT | Indiranagar | Mon/Wed/Fri 11 AM–3 PM | ₹600 / ₹400 |
|
||||
|
||||
## Treatment Packages
|
||||
|
||||
- **Master Health Checkup**: ₹2,999 — includes blood work, ECG, X-ray, doctor consultation. Available at all clinics.
|
||||
- **Cardiac Screening**: ₹4,999 — includes ECG, echo, TMT, lipid profile, cardiologist consultation.
|
||||
- **Women's Wellness**: ₹3,499 — includes pap smear, mammography, thyroid, bone density, gynecology consult.
|
||||
- **Orthopedic Assessment**: ₹1,999 — includes X-ray, bone density, physiotherapy assessment, orthopedic consult.
|
||||
|
||||
## Appointment Booking
|
||||
|
||||
- Appointments can be booked via phone, website, or walk-in.
|
||||
- Cancellation policy: free cancellation up to 4 hours before appointment.
|
||||
- Patients should arrive 15 minutes early for paperwork.
|
||||
- First-time patients must bring a valid government ID and any previous medical records.
|
||||
|
||||
## Insurance & Payments
|
||||
|
||||
- Accepted insurance: Star Health, ICICI Lombard, Bajaj Allianz, HDFC Ergo, Max Bupa, New India Assurance.
|
||||
- Payment methods: cash, credit/debit cards, UPI, net banking.
|
||||
- EMI options available for procedures above ₹10,000.
|
||||
`;
|
||||
|
||||
const SYSTEM_PROMPT = `You are an AI assistant for call center agents at Global Hospital, Bengaluru. Your job is to help agents quickly answer questions about doctors, clinics, appointments, treatment packages, and patient context during live calls.
|
||||
|
||||
${HOSPITAL_KNOWLEDGE}
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Be concise and direct — agents are on a live call and need quick answers.
|
||||
- Format responses for easy scanning: use bullet points for lists, bold key info.
|
||||
- If a caller/lead context is provided, incorporate it naturally (e.g., "Since Priya is interested in cardiology...").
|
||||
- Always mention consultation fees when discussing doctor availability.
|
||||
- If asked about something outside the hospital knowledge base, say "I don't have that information — please check with the supervisor."
|
||||
- Do NOT give medical advice, diagnosis, or treatment recommendations.
|
||||
- Do NOT share sensitive internal hospital data (revenue, staff salaries, internal policies).
|
||||
- Do NOT speculate about patient conditions or test results.
|
||||
- Keep responses under 150 words unless the agent asks for detailed information.`;
|
||||
|
||||
@Controller('api/ai')
|
||||
export class AiChatController {
|
||||
private readonly logger = new Logger(AiChatController.name);
|
||||
private client: Anthropic | null = null;
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
const apiKey = config.get<string>('ai.anthropicApiKey');
|
||||
if (apiKey) {
|
||||
this.client = new Anthropic({ apiKey });
|
||||
} else {
|
||||
this.logger.warn('ANTHROPIC_API_KEY not set — AI chat will use keyword fallback');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('chat')
|
||||
async chat(
|
||||
@Body() body: ChatRequest,
|
||||
@Headers('authorization') authHeader: string,
|
||||
): Promise<ChatResponse> {
|
||||
if (!authHeader) {
|
||||
throw new HttpException('Authorization required', 401);
|
||||
}
|
||||
|
||||
if (!body.message || body.message.trim().length === 0) {
|
||||
throw new HttpException('message is required', 400);
|
||||
}
|
||||
|
||||
const message = body.message.trim();
|
||||
const context = body.context;
|
||||
|
||||
// Build context string for the user message
|
||||
let contextPrefix = '';
|
||||
if (context) {
|
||||
const parts: string[] = [];
|
||||
if (context.leadName) parts.push(`Caller: ${context.leadName}`);
|
||||
if (context.callerPhone) parts.push(`Phone: ${context.callerPhone}`);
|
||||
if (context.leadId) parts.push(`Lead ID: ${context.leadId}`);
|
||||
if (parts.length > 0) {
|
||||
contextPrefix = `[Call Context: ${parts.join(', ')}]\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// If no API key, use keyword fallback
|
||||
if (!this.client) {
|
||||
return this.keywordFallback(message, context);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 300,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `${contextPrefix}${message}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
||||
|
||||
this.logger.log(`AI chat responded (${text.length} chars) for: "${message.substring(0, 50)}..."`);
|
||||
|
||||
return {
|
||||
reply: text,
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`AI chat failed: ${error}`);
|
||||
return this.keywordFallback(message, context);
|
||||
}
|
||||
}
|
||||
|
||||
private keywordFallback(message: string, context?: ChatContext): ChatResponse {
|
||||
const lower = message.toLowerCase();
|
||||
|
||||
// Doctor-specific responses
|
||||
if (lower.includes('sharma')) {
|
||||
return {
|
||||
reply: 'Dr. Sharma (Cardiology) is available at Koramangala and Whitefield clinics on Mon/Wed/Fri from 10 AM to 1 PM. Consultation fee: ₹800 for new patients, ₹500 for follow-ups.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('patel')) {
|
||||
return {
|
||||
reply: 'Dr. Patel (Gynecology) is available at Indiranagar and Koramangala clinics on Tue/Thu/Sat from 9 AM to 12 PM. Consultation fee: ₹700 for new patients, ₹400 for follow-ups.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('kumar')) {
|
||||
return {
|
||||
reply: 'Dr. Kumar (Orthopedics) is available at Whitefield clinic Mon-Fri from 2 PM to 5 PM. Consultation fee: ₹600 for new patients, ₹400 for follow-ups.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('reddy')) {
|
||||
return {
|
||||
reply: 'Dr. Reddy (General Medicine) is available at all three clinics (Koramangala, Indiranagar, Whitefield) Mon-Sat from 9 AM to 6 PM. Consultation fee: ₹500 for new patients, ₹300 for follow-ups.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('singh')) {
|
||||
return {
|
||||
reply: 'Dr. Singh (ENT) is available at Indiranagar clinic on Mon/Wed/Fri from 11 AM to 3 PM. Consultation fee: ₹600 for new patients, ₹400 for follow-ups.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
// Topic-based responses
|
||||
if (lower.includes('availability') || lower.includes('visiting') || lower.includes('hours') || lower.includes('timing')) {
|
||||
return {
|
||||
reply: 'Our doctors\' visiting hours:\n- Dr. Sharma (Cardiology): Mon/Wed/Fri 10 AM–1 PM\n- Dr. Patel (Gynecology): Tue/Thu/Sat 9 AM–12 PM\n- Dr. Kumar (Orthopedics): Mon–Fri 2 PM–5 PM\n- Dr. Reddy (General Medicine): Mon–Sat 9 AM–6 PM\n- Dr. Singh (ENT): Mon/Wed/Fri 11 AM–3 PM\n\nWhich doctor would the patient like to see?',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('clinic') || lower.includes('location') || lower.includes('address') || lower.includes('branch')) {
|
||||
return {
|
||||
reply: 'Global Hospital has 3 clinics in Bengaluru:\n- **Koramangala**: 80 Feet Road, Mon–Sat 8 AM–8 PM, Sun 9 AM–2 PM\n- **Whitefield**: Prestige Shantiniketan, Mon–Sat 8 AM–8 PM, Sun closed\n- **Indiranagar**: 100 Feet Road, Mon–Sat 9 AM–7 PM, Sun 10 AM–1 PM',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('package') || lower.includes('checkup') || lower.includes('screening') || lower.includes('wellness')) {
|
||||
return {
|
||||
reply: 'Our treatment packages:\n- **Master Health Checkup**: ₹2,999 (blood work, ECG, X-ray, consultation)\n- **Cardiac Screening**: ₹4,999 (ECG, echo, TMT, lipid profile)\n- **Women\'s Wellness**: ₹3,499 (pap smear, mammography, thyroid)\n- **Orthopedic Assessment**: ₹1,999 (X-ray, bone density, physio)\n\nAll packages available at all clinics.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('insurance') || lower.includes('payment') || lower.includes('emi')) {
|
||||
return {
|
||||
reply: 'We accept: Star Health, ICICI Lombard, Bajaj Allianz, HDFC Ergo, Max Bupa, New India Assurance. Payment via cash, cards, UPI, or net banking. EMI available for procedures above ₹10,000.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('appointment') || lower.includes('book') || lower.includes('cancel')) {
|
||||
return {
|
||||
reply: 'Appointments can be booked via phone, website, or walk-in. Free cancellation up to 4 hours before the appointment. Patients should arrive 15 minutes early. First-time patients need a valid government ID and previous medical records.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
if (lower.includes('fee') || lower.includes('cost') || lower.includes('price') || lower.includes('charge')) {
|
||||
return {
|
||||
reply: 'Consultation fees (New / Follow-up):\n- Dr. Sharma (Cardiology): ₹800 / ₹500\n- Dr. Patel (Gynecology): ₹700 / ₹400\n- Dr. Kumar (Orthopedics): ₹600 / ₹400\n- Dr. Reddy (General Medicine): ₹500 / ₹300\n- Dr. Singh (ENT): ₹600 / ₹400',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
// Patient context response
|
||||
if (lower.includes('patient') || lower.includes('history') || lower.includes('lead')) {
|
||||
if (context?.leadName) {
|
||||
return {
|
||||
reply: `For ${context.leadName} (${context.callerPhone ?? 'no phone on file'}): I can see this is an existing lead in the system. Please check the lead card on your screen for their full history, AI insights, and recent activities.`,
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'medium',
|
||||
};
|
||||
}
|
||||
return {
|
||||
reply: 'To look up a patient\'s history, I need their name or phone number. Please provide the caller\'s details or identify them from the incoming call card.',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'medium',
|
||||
};
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
reply: 'I don\'t have specific information about that. Here\'s what I can help with:\n- Doctor availability and visiting hours\n- Clinic locations and timings\n- Treatment packages and pricing\n- Insurance and payment options\n- Appointment booking process\n\nPlease ask about any of these topics!',
|
||||
sources: ['hospital_db'],
|
||||
confidence: 'low',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiEnrichmentService } from './ai-enrichment.service';
|
||||
import { AiChatController } from './ai-chat.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AiChatController],
|
||||
providers: [AiEnrichmentService],
|
||||
exports: [AiEnrichmentService],
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CallEventsModule } from './call-events/call-events.module';
|
||||
import { OzonetelAgentModule } from './ozonetel/ozonetel-agent.module';
|
||||
import { GraphqlProxyModule } from './graphql-proxy/graphql-proxy.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { WorklistModule } from './worklist/worklist.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -24,6 +25,7 @@ import { HealthModule } from './health/health.module';
|
||||
OzonetelAgentModule,
|
||||
GraphqlProxyModule,
|
||||
HealthModule,
|
||||
WorklistModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class PlatformGraphqlService {
|
||||
}
|
||||
|
||||
// Query using a passed-through auth header (user JWT)
|
||||
private async queryWithAuth<T>(query: string, variables: Record<string, any> | undefined, authHeader: string): Promise<T> {
|
||||
async queryWithAuth<T>(query: string, variables: Record<string, any> | undefined, authHeader: string): Promise<T> {
|
||||
const response = await axios.post(
|
||||
this.graphqlUrl,
|
||||
{ query, variables },
|
||||
|
||||
16
src/worklist/missed-call-webhook.controller.ts
Normal file
16
src/worklist/missed-call-webhook.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Controller, Post, Body, Logger } from '@nestjs/common';
|
||||
|
||||
@Controller('webhooks/ozonetel')
|
||||
export class MissedCallWebhookController {
|
||||
private readonly logger = new Logger(MissedCallWebhookController.name);
|
||||
|
||||
@Post('missed-call')
|
||||
async handleMissedCall(@Body() body: Record<string, any>) {
|
||||
this.logger.log(`Received missed call webhook: ${JSON.stringify(body)}`);
|
||||
|
||||
// TODO: Auto-assignment, duplicate merging, worklist insertion
|
||||
// For now, just acknowledge receipt
|
||||
|
||||
return { received: true };
|
||||
}
|
||||
}
|
||||
45
src/worklist/worklist.controller.ts
Normal file
45
src/worklist/worklist.controller.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Controller, Get, Headers, HttpException, Logger } from '@nestjs/common';
|
||||
import { WorklistService } from './worklist.service';
|
||||
|
||||
@Controller('api/worklist')
|
||||
export class WorklistController {
|
||||
private readonly logger = new Logger(WorklistController.name);
|
||||
|
||||
constructor(private readonly worklist: WorklistService) {}
|
||||
|
||||
@Get()
|
||||
async getWorklist(@Headers('authorization') authHeader: string) {
|
||||
if (!authHeader) {
|
||||
throw new HttpException('Authorization required', 401);
|
||||
}
|
||||
|
||||
// Decode the JWT to extract the agent name
|
||||
// The platform JWT payload contains user info — we extract the name
|
||||
const agentName = this.extractAgentName(authHeader);
|
||||
if (!agentName) {
|
||||
throw new HttpException('Could not determine agent identity from token', 400);
|
||||
}
|
||||
|
||||
this.logger.log(`Fetching worklist for agent: ${agentName}`);
|
||||
|
||||
return this.worklist.getWorklist(agentName, authHeader);
|
||||
}
|
||||
|
||||
private extractAgentName(authHeader: string): string | null {
|
||||
try {
|
||||
const token = authHeader.replace(/^Bearer\s+/i, '');
|
||||
// JWT payload is the second segment, base64url-encoded
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(token.split('.')[1], 'base64url').toString('utf8'),
|
||||
);
|
||||
// The platform JWT includes sub (userId) and workspace info
|
||||
// The agent name comes from firstName + lastName in the token
|
||||
const firstName = payload.firstName ?? payload.given_name ?? '';
|
||||
const lastName = payload.lastName ?? payload.family_name ?? '';
|
||||
const fullName = `${firstName} ${lastName}`.trim();
|
||||
return fullName || payload.email || payload.sub || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/worklist/worklist.module.ts
Normal file
12
src/worklist/worklist.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PlatformModule } from '../platform/platform.module';
|
||||
import { WorklistController } from './worklist.controller';
|
||||
import { WorklistService } from './worklist.service';
|
||||
import { MissedCallWebhookController } from './missed-call-webhook.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PlatformModule],
|
||||
controllers: [WorklistController, MissedCallWebhookController],
|
||||
providers: [WorklistService],
|
||||
})
|
||||
export class WorklistModule {}
|
||||
139
src/worklist/worklist.service.ts
Normal file
139
src/worklist/worklist.service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||
|
||||
export type WorklistResponse = {
|
||||
missedCalls: any[];
|
||||
followUps: any[];
|
||||
marketingLeads: any[];
|
||||
totalPending: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorklistService {
|
||||
private readonly logger = new Logger(WorklistService.name);
|
||||
|
||||
constructor(private readonly platform: PlatformGraphqlService) {}
|
||||
|
||||
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
|
||||
const [missedCalls, followUps, marketingLeads] = await Promise.all([
|
||||
this.getMissedCallsWithToken(agentName, authHeader),
|
||||
this.getPendingFollowUpsWithToken(agentName, authHeader),
|
||||
this.getAssignedLeadsWithToken(agentName, authHeader),
|
||||
]);
|
||||
|
||||
return {
|
||||
missedCalls,
|
||||
followUps,
|
||||
marketingLeads,
|
||||
totalPending: missedCalls.length + followUps.length + marketingLeads.length,
|
||||
};
|
||||
}
|
||||
|
||||
private async getAssignedLeadsWithToken(agentName: string, authHeader: string): Promise<any[]> {
|
||||
try {
|
||||
const data = await this.platform.queryWithAuth<{
|
||||
leads: { edges: { node: any }[] };
|
||||
}>(
|
||||
`query GetAssignedLeads($filter: LeadFilterInput, $first: Int, $orderBy: [LeadOrderByInput]) {
|
||||
leads(filter: $filter, first: $first, orderBy: $orderBy) {
|
||||
edges {
|
||||
node {
|
||||
id createdAt
|
||||
contactName { firstName lastName }
|
||||
contactPhone { number callingCode }
|
||||
contactEmail { address }
|
||||
leadSource leadStatus interestedService
|
||||
assignedAgent campaignId adId
|
||||
contactAttempts spamScore isSpam
|
||||
aiSummary aiSuggestedAction
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
filter: { assignedAgent: { eq: agentName } },
|
||||
first: 20,
|
||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
||||
},
|
||||
authHeader,
|
||||
);
|
||||
|
||||
return data.leads.edges.map((e) => e.node);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to fetch assigned leads: ${err}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getPendingFollowUpsWithToken(agentName: string, authHeader: string): Promise<any[]> {
|
||||
try {
|
||||
const data = await this.platform.queryWithAuth<{
|
||||
followUps: { edges: { node: any }[] };
|
||||
}>(
|
||||
`query GetPendingFollowUps($filter: FollowUpFilterInput, $first: Int) {
|
||||
followUps(filter: $filter, first: $first) {
|
||||
edges {
|
||||
node {
|
||||
id createdAt
|
||||
followUpType followUpStatus
|
||||
scheduledAt completedAt
|
||||
priority assignedAgent
|
||||
patientId callId
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
filter: {
|
||||
assignedAgent: { eq: agentName },
|
||||
followUpStatus: { in: ['PENDING', 'OVERDUE'] },
|
||||
},
|
||||
first: 20,
|
||||
},
|
||||
authHeader,
|
||||
);
|
||||
|
||||
return data.followUps.edges.map((e) => e.node);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to fetch follow-ups: ${err}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getMissedCallsWithToken(agentName: string, authHeader: string): Promise<any[]> {
|
||||
try {
|
||||
const data = await this.platform.queryWithAuth<{
|
||||
calls: { edges: { node: any }[] };
|
||||
}>(
|
||||
`query GetMissedCalls($filter: CallFilterInput, $first: Int, $orderBy: [CallOrderByInput]) {
|
||||
calls(filter: $filter, first: $first, orderBy: $orderBy) {
|
||||
edges {
|
||||
node {
|
||||
id createdAt
|
||||
callDirection callStatus
|
||||
callerNumber { number callingCode }
|
||||
agentName startedAt endedAt
|
||||
durationSeconds disposition
|
||||
callNotes leadId
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
filter: {
|
||||
callStatus: { eq: 'MISSED' },
|
||||
agentName: { eq: agentName },
|
||||
},
|
||||
first: 20,
|
||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
||||
},
|
||||
authHeader,
|
||||
);
|
||||
|
||||
return data.calls.edges.map((e) => e.node);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to fetch missed calls: ${err}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user