mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
fix(team-dashboard): agent-table buckets by authoritative agent.id
The Agent Performance table on the team dashboard was bucketing by raw
call.agentName — the field that holds Ozonetel's transfer-chain string
("RamaiahAdmin -> GlobalHealthX") and collides for distinct AgentIDs
that share a Full Name. Result: 7 rows for 3 real agents.
Now buckets by call.agent.id when the CDR enrichment has populated it,
falls back to legacy agentName grouping otherwise. Calls without any
agent info are dropped from the agent rollup (instead of being
collapsed under "Unknown").
Pulls agent { id name ozonetelAgentId } + transferredTo + transferType
on CALLS_QUERY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,14 +26,27 @@ interface AgentTableProps {
|
|||||||
|
|
||||||
export const AgentTable = ({ calls }: AgentTableProps) => {
|
export const AgentTable = ({ calls }: AgentTableProps) => {
|
||||||
const agents = useMemo(() => {
|
const agents = useMemo(() => {
|
||||||
const agentMap = new Map<string, Call[]>();
|
// Bucket by authoritative agent.id when present (from CDR enrichment);
|
||||||
|
// fall back to raw agentName for legacy rows that haven't been
|
||||||
|
// enriched yet. Skips rows with no agent info at all.
|
||||||
|
const agentMap = new Map<string, { displayName: string; calls: Call[] }>();
|
||||||
for (const call of calls) {
|
for (const call of calls) {
|
||||||
const agent = call.agentName ?? 'Unknown';
|
let key: string;
|
||||||
if (!agentMap.has(agent)) agentMap.set(agent, []);
|
let displayName: string;
|
||||||
agentMap.get(agent)!.push(call);
|
if (call.agent?.id) {
|
||||||
|
key = call.agent.id;
|
||||||
|
displayName = call.agent.name ?? call.agent.ozonetelAgentId ?? 'Unknown';
|
||||||
|
} else if (call.agentName) {
|
||||||
|
key = `legacy:${call.agentName}`;
|
||||||
|
displayName = call.agentName;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!agentMap.has(key)) agentMap.set(key, { displayName, calls: [] });
|
||||||
|
agentMap.get(key)!.calls.push(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(agentMap.entries()).map(([name, agentCalls]) => {
|
return Array.from(agentMap.entries()).map(([key, { displayName, calls: agentCalls }]) => {
|
||||||
const inbound = agentCalls.filter((c) => c.callDirection === 'INBOUND').length;
|
const inbound = agentCalls.filter((c) => c.callDirection === 'INBOUND').length;
|
||||||
const outbound = agentCalls.filter((c) => c.callDirection === 'OUTBOUND').length;
|
const outbound = agentCalls.filter((c) => c.callDirection === 'OUTBOUND').length;
|
||||||
const missed = agentCalls.filter((c) => c.callStatus === 'MISSED').length;
|
const missed = agentCalls.filter((c) => c.callStatus === 'MISSED').length;
|
||||||
@@ -43,11 +56,11 @@ export const AgentTable = ({ calls }: AgentTableProps) => {
|
|||||||
const avgHandle = completedCalls.length > 0 ? Math.round(totalDuration / completedCalls.length) : 0;
|
const avgHandle = completedCalls.length > 0 ? Math.round(totalDuration / completedCalls.length) : 0;
|
||||||
const booked = agentCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
|
const booked = agentCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
|
||||||
const conversion = total > 0 ? (booked / total) * 100 : 0;
|
const conversion = total > 0 ? (booked / total) * 100 : 0;
|
||||||
const nameParts = name.split(' ');
|
const nameParts = displayName.split(' ');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: name,
|
id: key,
|
||||||
name,
|
name: displayName,
|
||||||
initials: getInitials(nameParts[0] ?? '', nameParts[1] ?? ''),
|
initials: getInitials(nameParts[0] ?? '', nameParts[1] ?? ''),
|
||||||
inbound, outbound, missed, total, avgHandle, conversion,
|
inbound, outbound, missed, total, avgHandle, conversion,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const CALLS_QUERY = `{ calls(first: 100, orderBy: [{ startedAt: DescNulls
|
|||||||
startedAt endedAt durationSec
|
startedAt endedAt durationSec
|
||||||
recording { primaryLinkUrl } disposition sla
|
recording { primaryLinkUrl } disposition sla
|
||||||
patientId appointmentId leadId
|
patientId appointmentId leadId
|
||||||
|
agentId agent { id name ozonetelAgentId }
|
||||||
|
transferredTo transferType
|
||||||
} } } }`;
|
} } } }`;
|
||||||
|
|
||||||
export const DOCTORS_QUERY = `{ doctors(first: 20) { edges { node {
|
export const DOCTORS_QUERY = `{ doctors(first: 20) { edges { node {
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ export function transformCalls(data: any): Call[] {
|
|||||||
patientId: n.patientId,
|
patientId: n.patientId,
|
||||||
appointmentId: n.appointmentId,
|
appointmentId: n.appointmentId,
|
||||||
leadId: n.leadId,
|
leadId: n.leadId,
|
||||||
|
agentId: n.agentId ?? null,
|
||||||
|
agent: n.agent ?? null,
|
||||||
|
transferredTo: n.transferredTo ?? null,
|
||||||
|
transferType: n.transferType ?? null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,6 +276,12 @@ export type Call = {
|
|||||||
appointmentId: string | null;
|
appointmentId: string | null;
|
||||||
leadId: string | null;
|
leadId: string | null;
|
||||||
sla?: number | null;
|
sla?: number | null;
|
||||||
|
// Authoritative agent link from CDR enrichment. agentName remains the
|
||||||
|
// raw Ozonetel string (may be a transfer chain) for display fallback.
|
||||||
|
agentId?: string | null;
|
||||||
|
agent?: { id: string; name: string | null; ozonetelAgentId: string | null } | null;
|
||||||
|
transferredTo?: string | null;
|
||||||
|
transferType?: string | null;
|
||||||
// Denormalized for display
|
// Denormalized for display
|
||||||
leadName?: string;
|
leadName?: string;
|
||||||
leadPhone?: string;
|
leadPhone?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user