From 442a581c8a0cf8c1391bd4253182674cbe2a5540 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Wed, 1 Apr 2026 16:51:53 +0530 Subject: [PATCH] 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) --- src/components/call-desk/active-call-card.tsx | 43 +++---- src/components/call-desk/appointment-form.tsx | 22 ++-- src/components/call-desk/enquiry-form.tsx | 13 +- src/pages/team-performance.tsx | 115 ++++++++++++------ 4 files changed, 118 insertions(+), 75 deletions(-) diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index f87acc9..3841442 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -287,31 +287,26 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete )} - {/* Scrollable: expanded forms */} - {(appointmentOpen || enquiryOpen) && ( -
- + - { - setEnquiryOpen(false); - setSuggestedDisposition('INFO_PROVIDED'); - notify.success('Enquiry Logged'); - }} - /> -
- )} + { + setEnquiryOpen(false); + setSuggestedDisposition('INFO_PROVIDED'); + notify.success('Enquiry Logged'); + }} + /> {/* Disposition Modal — the ONLY path to end a call */} diff --git a/src/components/call-desk/appointment-form.tsx b/src/components/call-desk/appointment-form.tsx index 6ccef13..bfdb213 100644 --- a/src/components/call-desk/appointment-form.tsx +++ b/src/components/call-desk/appointment-form.tsx @@ -7,8 +7,8 @@ const XClose = faIcon(faXmark); import { Input } from '@/components/base/input/input'; import { Select } from '@/components/base/select/select'; import { TextArea } from '@/components/base/textarea/textarea'; -import { Checkbox } from '@/components/base/checkbox/checkbox'; import { Button } from '@/components/base/buttons/button'; +import { ModalOverlay, Modal, Dialog } from '@/components/application/modals/modal'; import { apiClient } from '@/lib/api-client'; import { cx } from '@/utils/cx'; import { notify } from '@/lib/toast'; @@ -100,7 +100,6 @@ export const AppointmentForm = ({ return null; }); const [chiefComplaint, setChiefComplaint] = useState(existingAppointment?.reasonForVisit ?? ''); - const [isReturning, setIsReturning] = useState(false); const [source, setSource] = useState('Inbound Call'); const [agentNotes, setAgentNotes] = useState(''); @@ -298,10 +297,11 @@ export const AppointmentForm = ({ } }; - if (!isOpen) return null; - return ( -
+ + + +
{/* Header with close button */}
@@ -474,13 +474,6 @@ export const AppointmentForm = ({ <>
- -
-
+
+
+
+
); }; diff --git a/src/components/call-desk/enquiry-form.tsx b/src/components/call-desk/enquiry-form.tsx index 8ca259f..87765f2 100644 --- a/src/components/call-desk/enquiry-form.tsx +++ b/src/components/call-desk/enquiry-form.tsx @@ -6,6 +6,7 @@ import { Select } from '@/components/base/select/select'; import { TextArea } from '@/components/base/textarea/textarea'; import { Checkbox } from '@/components/base/checkbox/checkbox'; import { Button } from '@/components/base/buttons/button'; +import { ModalOverlay, Modal, Dialog } from '@/components/application/modals/modal'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; @@ -138,10 +139,11 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu } }; - if (!isOpen) return null; - return ( -
+ + + +
@@ -208,6 +210,9 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu {isSaving ? 'Saving...' : 'Log Enquiry'}
-
+
+
+
+
); }; diff --git a/src/pages/team-performance.tsx b/src/pages/team-performance.tsx index 7543260..aa618e3 100644 --- a/src/pages/team-performance.tsx +++ b/src/pages/team-performance.tsx @@ -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 = () => {

Time Breakdown

+ {teamAvg.active === 0 && teamAvg.idle === 0 && teamAvg.wrap === 0 && teamAvg.break_ === 0 && ( +

Time utilisation data unavailable — requires Ozonetel agent session data.

+ )}
@@ -378,6 +417,12 @@ export const TeamPerformancePage = () => {

Overall NPS

+ {agents.every(a => a.npsscore == null) ? ( +
+

NPS data unavailable — configure NPS scores on agent profiles.

+
+ ) : ( + <>
{agents.filter(a => a.npsscore != null).map(a => ( @@ -390,6 +435,8 @@ export const TeamPerformancePage = () => {
))}
+ + )}

Conversion Metrics