mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
fix: appointment/enquiry modals + team performance fallback
- Appointment form: converted from inline to modal dialog, removed Returning Patient checkbox - Enquiry form: converted from inline to modal dialog - Active call card: removed max-h-[50vh] scroll container, forms render as modals - Team Performance: fallback agent list from call records when Ozonetel unavailable - NPS/Time sections show placeholder when data unavailable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,42 +107,78 @@ export const TeamPerformancePage = () => {
|
||||
setAllAppointments(appts);
|
||||
|
||||
// Build per-agent metrics
|
||||
const agentPerfs: AgentPerf[] = teamAgents.map((agent: any) => {
|
||||
const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelagentid);
|
||||
const agentLeads = leads.filter((l: any) => l.assignedAgent === agent.name);
|
||||
const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === agent.name);
|
||||
const agentAppts = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length; // approximate
|
||||
const totalCalls = agentCalls.length;
|
||||
const inbound = agentCalls.filter((c: any) => c.direction === 'INBOUND').length;
|
||||
const missed = agentCalls.filter((c: any) => c.callStatus === 'MISSED').length;
|
||||
let agentPerfs: AgentPerf[];
|
||||
|
||||
const tb = agent.timeBreakdown;
|
||||
const idleSec = tb ? parseTime(tb.totalIdleTime ?? '0:0:0') : 0;
|
||||
const activeSec = tb ? parseTime(tb.totalBusyTime ?? '0:0:0') : 0;
|
||||
const wrapSec = tb ? parseTime(tb.totalWrapupTime ?? '0:0:0') : 0;
|
||||
const breakSec = tb ? parseTime(tb.totalPauseTime ?? '0:0:0') : 0;
|
||||
if (teamAgents.length > 0) {
|
||||
// Real Ozonetel data available
|
||||
agentPerfs = teamAgents.map((agent: any) => {
|
||||
const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelagentid);
|
||||
const agentLeads = leads.filter((l: any) => l.assignedAgent === agent.name);
|
||||
const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === agent.name);
|
||||
const agentAppts = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length;
|
||||
const totalCalls = agentCalls.length;
|
||||
const inbound = agentCalls.filter((c: any) => c.direction === 'INBOUND').length;
|
||||
const missed = agentCalls.filter((c: any) => c.callStatus === 'MISSED').length;
|
||||
|
||||
return {
|
||||
name: agent.name ?? agent.ozonetelagentid,
|
||||
ozonetelagentid: agent.ozonetelagentid,
|
||||
npsscore: agent.npsscore,
|
||||
maxidleminutes: agent.maxidleminutes,
|
||||
minnpsthreshold: agent.minnpsthreshold,
|
||||
minconversionpercent: agent.minconversionpercent,
|
||||
calls: totalCalls,
|
||||
inbound,
|
||||
missed,
|
||||
followUps: agentFollowUps.length,
|
||||
leads: agentLeads.length,
|
||||
appointments: agentAppts,
|
||||
convPercent: totalCalls > 0 ? Math.round((agentAppts / totalCalls) * 100) : 0,
|
||||
idleMinutes: Math.round(idleSec / 60),
|
||||
activeMinutes: Math.round(activeSec / 60),
|
||||
wrapMinutes: Math.round(wrapSec / 60),
|
||||
breakMinutes: Math.round(breakSec / 60),
|
||||
timeBreakdown: tb,
|
||||
};
|
||||
});
|
||||
const tb = agent.timeBreakdown;
|
||||
const idleSec = tb ? parseTime(tb.totalIdleTime ?? '0:0:0') : 0;
|
||||
const activeSec = tb ? parseTime(tb.totalBusyTime ?? '0:0:0') : 0;
|
||||
const wrapSec = tb ? parseTime(tb.totalWrapupTime ?? '0:0:0') : 0;
|
||||
const breakSec = tb ? parseTime(tb.totalPauseTime ?? '0:0:0') : 0;
|
||||
|
||||
return {
|
||||
name: agent.name ?? agent.ozonetelagentid,
|
||||
ozonetelagentid: agent.ozonetelagentid,
|
||||
npsscore: agent.npsscore,
|
||||
maxidleminutes: agent.maxidleminutes,
|
||||
minnpsthreshold: agent.minnpsthreshold,
|
||||
minconversionpercent: agent.minconversionpercent,
|
||||
calls: totalCalls,
|
||||
inbound,
|
||||
missed,
|
||||
followUps: agentFollowUps.length,
|
||||
leads: agentLeads.length,
|
||||
appointments: agentAppts,
|
||||
convPercent: totalCalls > 0 ? Math.round((agentAppts / totalCalls) * 100) : 0,
|
||||
idleMinutes: Math.round(idleSec / 60),
|
||||
activeMinutes: Math.round(activeSec / 60),
|
||||
wrapMinutes: Math.round(wrapSec / 60),
|
||||
breakMinutes: Math.round(breakSec / 60),
|
||||
timeBreakdown: tb,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Fallback: build agent list from call records
|
||||
const agentNames = [...new Set(calls.map((c: any) => c.agentName).filter(Boolean))] as string[];
|
||||
agentPerfs = agentNames.map((name) => {
|
||||
const agentCalls = calls.filter((c: any) => c.agentName === name);
|
||||
const agentLeads = leads.filter((l: any) => l.assignedAgent === name);
|
||||
const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === name);
|
||||
const completed = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length;
|
||||
const totalCalls = agentCalls.length;
|
||||
|
||||
return {
|
||||
name,
|
||||
ozonetelagentid: name,
|
||||
npsscore: null,
|
||||
maxidleminutes: null,
|
||||
minnpsthreshold: null,
|
||||
minconversionpercent: null,
|
||||
calls: totalCalls,
|
||||
inbound: agentCalls.filter((c: any) => c.direction === 'INBOUND').length,
|
||||
missed: agentCalls.filter((c: any) => c.callStatus === 'MISSED').length,
|
||||
followUps: agentFollowUps.length,
|
||||
leads: agentLeads.length,
|
||||
appointments: completed,
|
||||
convPercent: totalCalls > 0 ? Math.round((completed / totalCalls) * 100) : 0,
|
||||
idleMinutes: 0,
|
||||
activeMinutes: 0,
|
||||
wrapMinutes: 0,
|
||||
breakMinutes: 0,
|
||||
timeBreakdown: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setAgents(agentPerfs);
|
||||
} catch (err) {
|
||||
@@ -329,6 +365,9 @@ export const TeamPerformancePage = () => {
|
||||
<div className="px-6 pt-6">
|
||||
<div className="rounded-xl border border-secondary bg-primary p-4">
|
||||
<h3 className="text-sm font-semibold text-secondary mb-3">Time Breakdown</h3>
|
||||
{teamAvg.active === 0 && teamAvg.idle === 0 && teamAvg.wrap === 0 && teamAvg.break_ === 0 && (
|
||||
<p className="text-xs text-tertiary mb-3">Time utilisation data unavailable — requires Ozonetel agent session data.</p>
|
||||
)}
|
||||
<div className="flex gap-6 mb-4 px-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-3 rounded-sm bg-success-solid" />
|
||||
@@ -378,6 +417,12 @@ export const TeamPerformancePage = () => {
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 rounded-xl border border-secondary bg-primary p-4">
|
||||
<h3 className="text-sm font-semibold text-secondary mb-2">Overall NPS</h3>
|
||||
{agents.every(a => a.npsscore == null) ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-xs text-tertiary">NPS data unavailable — configure NPS scores on agent profiles.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ReactECharts option={npsOption} style={{ height: 150 }} />
|
||||
<div className="space-y-1 mt-2">
|
||||
{agents.filter(a => a.npsscore != null).map(a => (
|
||||
@@ -390,6 +435,8 @@ export const TeamPerformancePage = () => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 rounded-xl border border-secondary bg-primary p-4">
|
||||
<h3 className="text-sm font-semibold text-secondary mb-3">Conversion Metrics</h3>
|
||||
|
||||
Reference in New Issue
Block a user