From 4598740efe9b3d66a5deff4432e2c4a89fe0259a Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Thu, 2 Apr 2026 12:14:38 +0530 Subject: [PATCH] feat: inline forms, transfer redesign, patient fixes, UI polish - Appointment/enquiry forms reverted to inline rendering (not modals) - Forms: flat scrollable section with pinned footer, no card wrapper - Appointment form: DatePicker component, date prefilled, removed Returning Patient checkbox - Enquiry form: removed disposition dropdown, lead status defaults to CONTACTED - Transfer dialog: agent picker with live status, doctor list with department, select-then-connect flow - Transfer: removed external number input, moved Cancel/Connect to pinned header row - Button mutual exclusivity: Book Appt / Enquiry / Transfer close each other - Patient name write-back: appointment + enquiry forms update patient fullName after save - Caller cache invalidation: POST /api/caller/invalidate after name update - Follow-up fix (#513): assignedAgent, patientId, date validation in createFollowUp - Patients page: removed status filters + column, added pagination (15/page) - Pending badge removed from call desk header - Table resize handles visible (bg-tertiary pill) - Sim call button: dev-only (import.meta.env.DEV) - CallControlStrip component (reusable, not currently mounted) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/application/table/table.tsx | 2 +- src/components/call-desk/active-call-card.tsx | 64 ++-- src/components/call-desk/appointment-form.tsx | 91 +++--- .../call-desk/call-control-strip.tsx | 61 ++++ src/components/call-desk/enquiry-form.tsx | 99 +++--- src/components/call-desk/transfer-dialog.tsx | 298 +++++++++++++++--- src/pages/call-desk.tsx | 54 +++- src/pages/patients.tsx | 49 +-- src/pages/team-dashboard.tsx | 1 + 9 files changed, 502 insertions(+), 217 deletions(-) create mode 100644 src/components/call-desk/call-control-strip.tsx diff --git a/src/components/application/table/table.tsx b/src/components/application/table/table.tsx index 5a9a1e8..1d1ceb9 100644 --- a/src/components/application/table/table.tsx +++ b/src/components/application/table/table.tsx @@ -212,7 +212,7 @@ const TableHead = ({ className, tooltip, label, children, resizable = true, ...p {resizable && ( )} diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 3841442..640aaed 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -17,6 +17,7 @@ import { TransferDialog } from './transfer-dialog'; import { EnquiryForm } from './enquiry-form'; import { formatPhone } from '@/lib/format'; import { apiClient } from '@/lib/api-client'; +import { useAuth } from '@/providers/auth-provider'; import { cx } from '@/utils/cx'; import { notify } from '@/lib/toast'; import type { Lead, CallDisposition } from '@/types/entities'; @@ -35,6 +36,7 @@ const formatDuration = (seconds: number): string => { }; export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete }: ActiveCallCardProps) => { + const { user } = useAuth(); const { callState, callDuration, callUcid, isMuted, isOnHold, answer, reject, hangup, toggleMute, toggleHold } = useSip(); const setCallState = useSetAtom(sipCallStateAtom); const setCallerNumber = useSetAtom(sipCallerNumberAtom); @@ -203,7 +205,7 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete wasAnsweredRef.current = true; return ( <> -
+
{/* Pinned: caller info + controls */}
@@ -260,23 +262,28 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete + onClick={() => { setAppointmentOpen(!appointmentOpen); setEnquiryOpen(false); setTransferOpen(false); }}>Book Appt - + + onClick={() => { setTransferOpen(!transferOpen); setAppointmentOpen(false); setEnquiryOpen(false); }}>Transfer
- {/* Transfer dialog */} +
+ + {/* Scrollable: expanded forms + transfer */} + {(appointmentOpen || enquiryOpen || transferOpen) && ( +
{transferOpen && callUcid && ( setTransferOpen(false)} onTransferred={() => { setTransferOpen(false); @@ -285,28 +292,31 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete }} /> )} -
+ - - - { - 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 bfdb213..1d8d75b 100644 --- a/src/components/call-desk/appointment-form.tsx +++ b/src/components/call-desk/appointment-form.tsx @@ -1,14 +1,10 @@ import { useState, useEffect } from 'react'; -import { faCalendarPlus, faXmark } from '@fortawesome/pro-duotone-svg-icons'; -import { faIcon } from '@/lib/icon-wrapper'; - -const CalendarPlus02 = faIcon(faCalendarPlus); -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 { Button } from '@/components/base/buttons/button'; -import { ModalOverlay, Modal, Dialog } from '@/components/application/modals/modal'; +import { DatePicker } from '@/components/application/date-picker/date-picker'; +import { parseDate } from '@internationalized/date'; import { apiClient } from '@/lib/api-client'; import { cx } from '@/utils/cx'; import { notify } from '@/lib/toast'; @@ -90,7 +86,7 @@ export const AppointmentForm = ({ const [doctor, setDoctor] = useState(existingAppointment?.doctorId ?? null); const [date, setDate] = useState(() => { if (existingAppointment?.scheduledAt) return existingAppointment.scheduledAt.split('T')[0]; - return ''; + return new Date().toISOString().split('T')[0]; }); const [timeSlot, setTimeSlot] = useState(() => { if (existingAppointment?.scheduledAt) { @@ -249,7 +245,22 @@ export const AppointmentForm = ({ }, ); - // Update lead status if we have a matched lead + // Update patient name if we have a name and a linked patient + if (patientId && patientName.trim()) { + await apiClient.graphql( + `mutation UpdatePatient($id: UUID!, $data: PatientUpdateInput!) { + updatePatient(id: $id, data: $data) { id } + }`, + { + id: patientId, + data: { + fullName: { firstName: patientName.split(' ')[0], lastName: patientName.split(' ').slice(1).join(' ') || '' }, + }, + }, + ).catch((err: unknown) => console.warn('Failed to update patient name:', err)); + } + + // Update lead status + name if we have a matched lead if (leadId) { await apiClient.graphql( `mutation UpdateLead($id: UUID!, $data: LeadUpdateInput!) { @@ -260,10 +271,16 @@ export const AppointmentForm = ({ data: { leadStatus: 'APPOINTMENT_SET', lastContactedAt: new Date().toISOString(), + ...(patientName.trim() ? { contactName: { firstName: patientName.split(' ')[0], lastName: patientName.split(' ').slice(1).join(' ') || '' } } : {}), }, }, ).catch((err: unknown) => console.warn('Failed to update lead:', err)); } + + // Invalidate caller cache so next lookup gets the real name + if (callerNumber) { + apiClient.post('/api/caller/invalidate', { phone: callerNumber }, { silent: true }).catch(() => {}); + } } onSaved?.(); @@ -297,35 +314,12 @@ export const AppointmentForm = ({ } }; - return ( - - - -
- {/* Header with close button */} -
-
-
- -
-
-

- {isEditMode ? 'Edit Appointment' : 'Book Appointment'} -

-

- {isEditMode ? 'Modify or cancel this appointment' : 'Schedule a new patient visit'} -

-
-
- -
+ if (!isOpen) return null; - {/* Form fields */} + return ( +
+ {/* Form fields — scrollable */} +
{/* Patient Info — only for new appointments */} {!isEditMode && ( @@ -416,13 +410,14 @@ export const AppointmentForm = ({ {(item) => } - +
+ Date * + setDate(val ? val.toString() : '')} + granularity="day" + /> +
{/* Time slot grid */} {doctor && date && ( @@ -497,9 +492,10 @@ export const AppointmentForm = ({
)}
+
- {/* Footer buttons */} -
+ {/* Footer — pinned */} +
{isEditMode && (
-
-
-
-
+ ); }; diff --git a/src/components/call-desk/call-control-strip.tsx b/src/components/call-desk/call-control-strip.tsx new file mode 100644 index 0000000..d2967c4 --- /dev/null +++ b/src/components/call-desk/call-control-strip.tsx @@ -0,0 +1,61 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faMicrophone, faMicrophoneSlash, + faPause, faPlay, faPhoneHangup, +} from '@fortawesome/pro-duotone-svg-icons'; +import { useSip } from '@/providers/sip-provider'; +import { cx } from '@/utils/cx'; + +const formatDuration = (seconds: number): string => { + const m = Math.floor(seconds / 60).toString().padStart(2, '0'); + const s = (seconds % 60).toString().padStart(2, '0'); + return `${m}:${s}`; +}; + +export const CallControlStrip = () => { + const { callState, callDuration, isMuted, isOnHold, toggleMute, toggleHold, hangup } = useSip(); + + if (callState !== 'active' && callState !== 'ringing-out') return null; + + return ( +
+
+ + + + + Live Call + {formatDuration(callDuration)} +
+
+ + + +
+
+ ); +}; diff --git a/src/components/call-desk/enquiry-form.tsx b/src/components/call-desk/enquiry-form.tsx index 87765f2..5008102 100644 --- a/src/components/call-desk/enquiry-form.tsx +++ b/src/components/call-desk/enquiry-form.tsx @@ -1,12 +1,9 @@ import { useState, useEffect } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faClipboardQuestion, faXmark } from '@fortawesome/pro-duotone-svg-icons'; 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 { notify } from '@/lib/toast'; @@ -14,19 +11,14 @@ type EnquiryFormProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; callerPhone?: string | null; + leadId?: string | null; + patientId?: string | null; + agentName?: string | null; onSaved?: () => void; }; -const dispositionItems = [ - { id: 'CONVERTED', label: 'Converted' }, - { id: 'FOLLOW_UP', label: 'Follow-up Needed' }, - { id: 'GENERAL_QUERY', label: 'General Query' }, - { id: 'NO_ANSWER', label: 'No Answer' }, - { id: 'INVALID_NUMBER', label: 'Invalid Number' }, - { id: 'CALL_DROPPED', label: 'Call Dropped' }, -]; -export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: EnquiryFormProps) => { +export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, leadId: propLeadId, patientId, agentName, onSaved }: EnquiryFormProps) => { const [patientName, setPatientName] = useState(''); const [source, setSource] = useState('Phone Inquiry'); const [queryAsked, setQueryAsked] = useState(''); @@ -36,7 +28,6 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu const [doctor, setDoctor] = useState(null); const [followUpNeeded, setFollowUpNeeded] = useState(false); const [followUpDate, setFollowUpDate] = useState(''); - const [disposition, setDisposition] = useState(null); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); @@ -65,8 +56,8 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu const doctorItems = filteredDoctors.map(d => ({ id: d.id, label: d.name })); const handleSave = async () => { - if (!patientName.trim() || !queryAsked.trim() || !disposition) { - setError('Please fill in required fields: patient name, query, and disposition.'); + if (!patientName.trim() || !queryAsked.trim()) { + setError('Please fill in required fields: patient name and query.'); return; } @@ -74,9 +65,9 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu setError(null); try { - // Resolve caller — ensures lead+patient pair exists, returns IDs - let leadId: string | null = null; - if (registeredPhone) { + // Use passed leadId or resolve from phone + let leadId: string | null = propLeadId ?? null; + if (!leadId && registeredPhone) { const resolved = await apiClient.post<{ leadId: string; patientId: string }>('/api/caller/resolve', { phone: registeredPhone }, { silent: true }); leadId = resolved.leadId; } @@ -91,7 +82,7 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu name: `Enquiry — ${patientName}`, contactName: { firstName: patientName.split(' ')[0], lastName: patientName.split(' ').slice(1).join(' ') || '' }, source: 'PHONE', - status: disposition === 'CONVERTED' ? 'CONVERTED' : 'NEW', + status: 'CONTACTED', interestedService: queryAsked.substring(0, 100), }, }, @@ -106,15 +97,38 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu contactName: { firstName: patientName.split(' ')[0], lastName: patientName.split(' ').slice(1).join(' ') || '' }, contactPhone: registeredPhone ? { primaryPhoneNumber: registeredPhone } : undefined, source: 'PHONE', - status: disposition === 'CONVERTED' ? 'CONVERTED' : 'NEW', + status: 'CONTACTED', interestedService: queryAsked.substring(0, 100), }, }, ); } + // Update patient name if we have a name and a linked patient + if (patientId && patientName.trim()) { + await apiClient.graphql( + `mutation($id: UUID!, $data: PatientUpdateInput!) { updatePatient(id: $id, data: $data) { id } }`, + { + id: patientId, + data: { + fullName: { firstName: patientName.split(' ')[0], lastName: patientName.split(' ').slice(1).join(' ') || '' }, + }, + }, + ).catch((err: unknown) => console.warn('Failed to update patient name:', err)); + } + + // Invalidate caller cache so next lookup gets the real name + if (callerPhone) { + apiClient.post('/api/caller/invalidate', { phone: callerPhone }, { silent: true }).catch(() => {}); + } + // Create follow-up if needed - if (followUpNeeded && followUpDate) { + if (followUpNeeded) { + if (!followUpDate) { + setError('Please select a follow-up date.'); + setIsSaving(false); + return; + } await apiClient.graphql( `mutation($data: FollowUpCreateInput!) { createFollowUp(data: $data) { id } }`, { @@ -123,7 +137,9 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu typeCustom: 'CALLBACK', status: 'PENDING', priority: 'NORMAL', + assignedAgent: agentName ?? undefined, scheduledAt: new Date(`${followUpDate}T09:00:00`).toISOString(), + patientId: patientId ?? undefined, }, }, { silent: true }, @@ -139,29 +155,12 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu } }; - return ( - - - -
-
-
-
- -
-
-

Log Enquiry

-

Capture caller's question and details

-
-
- -
+ if (!isOpen) return null; + return ( +
+ {/* Form fields — scrollable */} +
@@ -194,25 +193,19 @@ export const EnquiryForm = ({ isOpen, onOpenChange, callerPhone, onSaved }: Enqu )} - - {error && (
{error}
)}
+
-
+ {/* Footer — pinned */} +
-
-
-
-
+ ); }; diff --git a/src/components/call-desk/transfer-dialog.tsx b/src/components/call-desk/transfer-dialog.tsx index c7113fa..3af5116 100644 --- a/src/components/call-desk/transfer-dialog.tsx +++ b/src/components/call-desk/transfer-dialog.tsx @@ -1,49 +1,157 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faXmark } from '@fortawesome/pro-duotone-svg-icons'; +import { faPhone, faUserDoctor, faHeadset, faShieldCheck, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; +import { faIcon } from '@/lib/icon-wrapper'; import { Input } from '@/components/base/input/input'; import { Button } from '@/components/base/buttons/button'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; +import { cx } from '@/utils/cx'; + +const SearchIcon = faIcon(faMagnifyingGlass); + +type TransferTarget = { + id: string; + name: string; + type: 'agent' | 'supervisor' | 'doctor'; + department?: string; + phoneNumber: string; + status?: 'ready' | 'busy' | 'offline' | 'on-call' | 'break'; +}; type TransferDialogProps = { ucid: string; + currentAgentId?: string; onClose: () => void; onTransferred: () => void; }; -export const TransferDialog = ({ ucid, onClose, onTransferred }: TransferDialogProps) => { - const [number, setNumber] = useState(''); - const [transferring, setTransferring] = useState(false); - const [stage, setStage] = useState<'input' | 'connected'>('input'); +const statusConfig: Record = { + ready: { label: 'Ready', dotClass: 'bg-success-solid' }, + 'on-call': { label: 'On Call', dotClass: 'bg-error-solid' }, + 'in-call': { label: 'On Call', dotClass: 'bg-error-solid' }, + busy: { label: 'Busy', dotClass: 'bg-warning-solid' }, + acw: { label: 'Wrapping', dotClass: 'bg-warning-solid' }, + break: { label: 'Break', dotClass: 'bg-tertiary' }, + training: { label: 'Training', dotClass: 'bg-tertiary' }, + offline: { label: 'Offline', dotClass: 'bg-quaternary' }, +}; - const handleConference = async () => { - if (!number.trim()) return; +const typeIcons = { + agent: faHeadset, + supervisor: faShieldCheck, + doctor: faUserDoctor, +}; + +export const TransferDialog = ({ ucid, currentAgentId, onClose, onTransferred }: TransferDialogProps) => { + const [targets, setTargets] = useState([]); + const [search, setSearch] = useState(''); + const [loading, setLoading] = useState(true); + const [transferring, setTransferring] = useState(false); + const [selectedTarget, setSelectedTarget] = useState(null); + const [connectedTarget, setConnectedTarget] = useState(null); + + // Fetch transfer targets + useEffect(() => { + const fetchTargets = async () => { + try { + const [agentsRes, doctorsRes] = await Promise.all([ + apiClient.graphql(`{ agents(first: 20) { edges { node { id name ozonetelagentid sipextension } } } }`), + apiClient.graphql(`{ doctors(first: 20) { edges { node { id name department phone { primaryPhoneNumber } } } } }`), + ]); + + const agents: TransferTarget[] = (agentsRes.agents?.edges ?? []) + .map((e: any) => e.node) + .filter((a: any) => a.ozonetelagentid !== currentAgentId) + .map((a: any) => ({ + id: a.id, + name: a.name, + type: 'agent' as const, + phoneNumber: `0${a.sipextension}`, + status: 'offline' as const, + })); + + const doctors: TransferTarget[] = (doctorsRes.doctors?.edges ?? []) + .map((e: any) => e.node) + .filter((d: any) => d.phone?.primaryPhoneNumber) + .map((d: any) => ({ + id: d.id, + name: d.name, + type: 'doctor' as const, + department: d.department?.replace(/_/g, ' '), + phoneNumber: `0${d.phone.primaryPhoneNumber}`, + })); + + setTargets([...agents, ...doctors]); + } catch (err) { + console.warn('Failed to fetch transfer targets:', err); + } finally { + setLoading(false); + } + }; + fetchTargets(); + }, [currentAgentId]); + + // Subscribe to agent state via SSE for live status + useEffect(() => { + const agentTargets = targets.filter(t => t.type === 'agent'); + if (agentTargets.length === 0) return; + + // Poll agent states from the supervisor endpoint + const fetchStates = async () => { + for (const agent of agentTargets) { + try { + const res = await apiClient.get(`/api/supervisor/agent-state/${agent.phoneNumber.replace(/^0/, '')}`, { silent: true }); + if (res?.state) { + setTargets(prev => prev.map(t => + t.id === agent.id ? { ...t, status: res.state } : t, + )); + } + } catch { /* best effort */ } + } + }; + fetchStates(); + const interval = setInterval(fetchStates, 10000); + return () => clearInterval(interval); + }, [targets.length]); + + const filtered = search.trim() + ? targets.filter(t => t.name.toLowerCase().includes(search.toLowerCase()) || (t.department ?? '').toLowerCase().includes(search.toLowerCase())) + : targets; + + const agents = filtered.filter(t => t.type === 'agent'); + const doctors = filtered.filter(t => t.type === 'doctor'); + + const handleConnect = async () => { + const target = selectedTarget; + if (!target) return; setTransferring(true); try { await apiClient.post('/api/ozonetel/call-control', { action: 'CONFERENCE', ucid, - conferenceNumber: `0${number.replace(/\D/g, '')}`, + conferenceNumber: target.phoneNumber, }); - notify.success('Connected', 'Third party connected. Click Complete to transfer.'); - setStage('connected'); + setConnectedTarget(target); + notify.success('Connected', `Speaking with ${target.name}. Click Complete to transfer.`); } catch { - notify.error('Transfer Failed', 'Could not connect to the target number'); + notify.error('Transfer Failed', `Could not connect to ${target.name}`); } finally { setTransferring(false); } }; + const handleComplete = async () => { + if (!connectedTarget) return; setTransferring(true); try { await apiClient.post('/api/ozonetel/call-control', { action: 'KICK_CALL', ucid, - conferenceNumber: `0${number.replace(/\D/g, '')}`, + conferenceNumber: connectedTarget.phoneNumber, }); - notify.success('Transferred', 'Call transferred successfully'); + notify.success('Transferred', `Call transferred to ${connectedTarget.name}`); onTransferred(); } catch { notify.error('Transfer Failed', 'Could not complete transfer'); @@ -52,40 +160,138 @@ export const TransferDialog = ({ ucid, onClose, onTransferred }: TransferDialogP } }; - return ( -
-
- Transfer Call - + const handleCancel = async () => { + if (!connectedTarget) { onClose(); return; } + // Disconnect the third party, keep the caller + setTransferring(true); + try { + await apiClient.post('/api/ozonetel/call-control', { + action: 'KICK_CALL', + ucid, + conferenceNumber: connectedTarget.phoneNumber, + }); + setConnectedTarget(null); + notify.info('Cancelled', 'Transfer cancelled, caller reconnected'); + } catch { + notify.error('Error', 'Could not disconnect third party'); + } finally { + setTransferring(false); + } + }; + + // Connected state — show target + complete/cancel buttons + if (connectedTarget) { + return ( +
+
+
+ +
+
+

Connected to {connectedTarget.name}

+

Speak privately, then complete the transfer

+
+
+
+ + +
- {stage === 'input' ? ( -
- - + ); + } + + // Target selection + return ( +
+ {/* Search + actions — pinned */} +
+
+
- ) : ( -
- Connected to {number} - -
- )} + + +
+ + {/* Scrollable target list */} +
+ {loading ? ( +

Loading...

+ ) : ( + <> + {/* Agents */} + {agents.length > 0 && ( +
+

Agents

+
+ {agents.map(agent => { + const st = statusConfig[agent.status ?? 'offline'] ?? statusConfig.offline; + const isSelected = selectedTarget?.id === agent.id; + return ( + + ); + })} +
+
+ )} + + {/* Doctors */} + {doctors.length > 0 && ( +
+

Doctors

+
+ {doctors.map(doc => { + const isSelected = selectedTarget?.id === doc.id; + return ( + + ); + })} +
+
+ )} + + + {filtered.length === 0 && !loading && ( +

No matching targets

+ )} + + )} +
+
); }; diff --git a/src/pages/call-desk.tsx b/src/pages/call-desk.tsx index 3ad3b3e..64476b8 100644 --- a/src/pages/call-desk.tsx +++ b/src/pages/call-desk.tsx @@ -1,6 +1,8 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSidebarFlip, faSidebar, faPhone, faXmark, faDeleteLeft } from '@fortawesome/pro-duotone-svg-icons'; +import { faSidebarFlip, faSidebar, faPhone, faXmark, faDeleteLeft, faFlask } from '@fortawesome/pro-duotone-svg-icons'; +import { useSetAtom } from 'jotai'; +import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom, sipCallDurationAtom } from '@/state/sip-state'; import { useAuth } from '@/providers/auth-provider'; import { useData } from '@/providers/data-provider'; import { useWorklist } from '@/hooks/use-worklist'; @@ -10,7 +12,6 @@ import type { WorklistLead } from '@/components/call-desk/worklist-panel'; import { ContextPanel } from '@/components/call-desk/context-panel'; import { ActiveCallCard } from '@/components/call-desk/active-call-card'; -import { Badge } from '@/components/base/badges/badges'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; @@ -19,7 +20,7 @@ export const CallDeskPage = () => { const { user } = useAuth(); const { leadActivities, calls, followUps: dataFollowUps, patients, appointments } = useData(); const { callState, callerNumber, callUcid, dialOutbound } = useSip(); - const { missedCalls, followUps, marketingLeads, totalPending, loading } = useWorklist(); + const { missedCalls, followUps, marketingLeads, loading } = useWorklist(); const [selectedLead, setSelectedLead] = useState(null); const [contextOpen, setContextOpen] = useState(true); const [activeMissedCallId, setActiveMissedCallId] = useState(null); @@ -28,6 +29,29 @@ export const CallDeskPage = () => { const [dialNumber, setDialNumber] = useState(''); const [dialling, setDialling] = useState(false); + // DEV: simulate incoming call + const setSimCallState = useSetAtom(sipCallStateAtom); + const setSimCallerNumber = useSetAtom(sipCallerNumberAtom); + const setSimCallUcid = useSetAtom(sipCallUcidAtom); + const setSimDuration = useSetAtom(sipCallDurationAtom); + const simTimerRef = useRef | null>(null); + + const startSimCall = useCallback(() => { + setSimCallerNumber('+919959966676'); + setSimCallUcid(`SIM-${Date.now()}`); + setSimDuration(0); + setSimCallState('active'); + simTimerRef.current = setInterval(() => setSimDuration((d) => d + 1), 1000); + }, [setSimCallState, setSimCallerNumber, setSimCallUcid, setSimDuration]); + + const endSimCall = useCallback(() => { + if (simTimerRef.current) { clearInterval(simTimerRef.current); simTimerRef.current = null; } + setSimCallState('idle'); + setSimCallerNumber(null); + setSimCallUcid(null); + setSimDuration(0); + }, [setSimCallState, setSimCallerNumber, setSimCallUcid, setSimDuration]); + const handleDial = async () => { const num = dialNumber.replace(/[^0-9]/g, ''); if (num.length < 10) { notify.error('Enter a valid phone number'); return; } @@ -112,6 +136,23 @@ export const CallDeskPage = () => {
+ {import.meta.env.DEV && (!isInCall ? ( + + ) : callUcid?.startsWith('SIM-') && ( + + ))} {!isInCall && (
)} - {totalPending > 0 && ( - {totalPending} pending - )} - {/* Status filter buttons */} -
- {(['all', 'active', 'inactive'] as const).map((status) => ( - - ))} -
{ icon={SearchLg} size="sm" value={searchQuery} - onChange={(value) => setSearchQuery(value)} + onChange={handleSearch} aria-label="Search patients" />
@@ -154,10 +139,9 @@ export const PatientsPage = () => { - - + {(patient) => { const displayName = getPatientDisplayName(patient); const age = computeAge(patient.dateOfBirth); @@ -239,13 +223,6 @@ export const PatientsPage = () => { - {/* Status */} - - - Active - - - {/* Actions */}
@@ -288,6 +265,12 @@ export const PatientsPage = () => { )} + + {totalPages > 1 && ( +
+ +
+ )}
{/* Patient Profile Panel - collapsible with smooth transition */} diff --git a/src/pages/team-dashboard.tsx b/src/pages/team-dashboard.tsx index c9ffd74..ae01aa9 100644 --- a/src/pages/team-dashboard.tsx +++ b/src/pages/team-dashboard.tsx @@ -158,6 +158,7 @@ export const TeamDashboardPage = () => { )}
+
); };