mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
- Self-contained NestJS module: types, storage (Redis+JSON), fact providers, action handlers - PriorityConfig CRUD (slider values for task weights, campaign weights, source weights) - Score action handler with SLA multiplier + campaign multiplier formula - Worklist consumer: scores and ranks items before returning - Hospital starter template (7 rules) - REST API: /api/rules/* (CRUD, priority-config, evaluate, templates) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
|
import { WorklistConsumer } from '../rules-engine/consumers/worklist.consumer';
|
|
|
|
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,
|
|
private readonly worklistConsumer: WorklistConsumer,
|
|
) {}
|
|
|
|
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
|
|
const [rawMissedCalls, rawFollowUps, rawMarketingLeads] = await Promise.all([
|
|
this.getMissedCalls(agentName, authHeader),
|
|
this.getPendingFollowUps(agentName, authHeader),
|
|
this.getAssignedLeads(agentName, authHeader),
|
|
]);
|
|
|
|
// Tag each item with a type field for the scoring engine
|
|
const combined = [
|
|
...rawMissedCalls.map((item: any) => ({ ...item, type: 'missed' })),
|
|
...rawFollowUps.map((item: any) => ({ ...item, type: 'follow-up' })),
|
|
...rawMarketingLeads.map((item: any) => ({ ...item, type: 'lead' })),
|
|
];
|
|
|
|
// Score and rank via rules engine
|
|
const scored = await this.worklistConsumer.scoreAndRank(combined);
|
|
|
|
// Split back into the 3 categories
|
|
const missedCalls = scored.filter((item: any) => item.type === 'missed');
|
|
const followUps = scored.filter((item: any) => item.type === 'follow-up');
|
|
const marketingLeads = scored.filter((item: any) => item.type === 'lead');
|
|
|
|
return {
|
|
missedCalls,
|
|
followUps,
|
|
marketingLeads,
|
|
totalPending: missedCalls.length + followUps.length + marketingLeads.length,
|
|
};
|
|
}
|
|
|
|
private async getAssignedLeads(agentName: string, authHeader: string): Promise<any[]> {
|
|
try {
|
|
const data = await this.platform.queryWithAuth<any>(
|
|
`{ leads(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }, orderBy: [{ createdAt: AscNullsLast }]) { edges { node {
|
|
id createdAt
|
|
contactName { firstName lastName }
|
|
contactPhone { primaryPhoneNumber }
|
|
contactEmail { primaryEmail }
|
|
source status interestedService
|
|
assignedAgent campaignId
|
|
contactAttempts spamScore isSpam
|
|
aiSummary aiSuggestedAction
|
|
} } } }`,
|
|
undefined,
|
|
authHeader,
|
|
);
|
|
return data.leads.edges.map((e: any) => e.node);
|
|
} catch (err) {
|
|
this.logger.warn(`Failed to fetch assigned leads: ${err}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private async getPendingFollowUps(agentName: string, authHeader: string): Promise<any[]> {
|
|
try {
|
|
const data = await this.platform.queryWithAuth<any>(
|
|
`{ followUps(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }) { edges { node {
|
|
id name createdAt
|
|
typeCustom status scheduledAt completedAt
|
|
priority assignedAgent
|
|
patientId
|
|
} } } }`,
|
|
undefined,
|
|
authHeader,
|
|
);
|
|
// Filter to PENDING/OVERDUE client-side since platform may not support in-filter on remapped fields
|
|
return data.followUps.edges
|
|
.map((e: any) => e.node)
|
|
.filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE');
|
|
} catch (err) {
|
|
this.logger.warn(`Failed to fetch follow-ups: ${err}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private async getMissedCalls(agentName: string, authHeader: string): Promise<any[]> {
|
|
try {
|
|
// FIFO ordering (AscNullsLast) — oldest first. No agentName filter — missed calls are a shared queue.
|
|
const data = await this.platform.queryWithAuth<any>(
|
|
`{ calls(first: 20, filter: { callStatus: { eq: MISSED }, callbackstatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node {
|
|
id name createdAt
|
|
direction callStatus agentName
|
|
callerNumber { primaryPhoneNumber }
|
|
startedAt endedAt durationSec
|
|
disposition leadId
|
|
callbackstatus callsourcenumber missedcallcount callbackattemptedat
|
|
} } } }`,
|
|
undefined,
|
|
authHeader,
|
|
);
|
|
return data.calls.edges.map((e: any) => e.node);
|
|
} catch (err) {
|
|
this.logger.warn(`Failed to fetch missed calls: ${err}`);
|
|
return [];
|
|
}
|
|
}
|
|
}
|