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) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 12:14:38 +05:30
parent 442a581c8a
commit 4598740efe
9 changed files with 502 additions and 217 deletions

View File

@@ -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<WorklistLead | null>(null);
const [contextOpen, setContextOpen] = useState(true);
const [activeMissedCallId, setActiveMissedCallId] = useState<string | null>(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<ReturnType<typeof setInterval> | 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 = () => {
</div>
<div className="flex items-center gap-2">
{import.meta.env.DEV && (!isInCall ? (
<button
onClick={startSimCall}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-warning-secondary text-warning-primary hover:bg-warning-primary hover:text-white transition duration-100 ease-linear"
>
<FontAwesomeIcon icon={faFlask} className="size-3" />
Sim Call
</button>
) : callUcid?.startsWith('SIM-') && (
<button
onClick={endSimCall}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-error-solid text-white hover:bg-error-solid_hover transition duration-100 ease-linear"
>
<FontAwesomeIcon icon={faFlask} className="size-3" />
End Sim
</button>
))}
{!isInCall && (
<div className="relative">
<button
@@ -173,9 +214,6 @@ export const CallDeskPage = () => {
)}
</div>
)}
{totalPending > 0 && (
<Badge size="sm" color="brand" type="pill-color">{totalPending} pending</Badge>
)}
<button
onClick={() => setContextOpen(!contextOpen)}
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
@@ -192,7 +230,7 @@ export const CallDeskPage = () => {
<div className="flex flex-1 flex-col min-h-0 overflow-hidden">
{/* Active call */}
{isInCall && (
<div className="p-5">
<div className="flex flex-col flex-1 min-h-0 overflow-hidden p-5">
<ActiveCallCard lead={activeLeadFull} callerPhone={callerNumber ?? ''} missedCallId={activeMissedCallId} onCallComplete={() => { setActiveMissedCallId(null); setCallDismissed(true); }} />
</div>
)}