mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Badge } from '@/components/base/badges/badges';
|
||||
// Button removed — actions are icon-only now
|
||||
import { Input } from '@/components/base/input/input';
|
||||
import { Table, TableCard } from '@/components/application/table/table';
|
||||
import { PaginationPageDefault } from '@/components/application/pagination/pagination';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
|
||||
import { PatientProfilePanel } from '@/components/shared/patient-profile-panel';
|
||||
@@ -59,13 +60,13 @@ export const PatientsPage = () => {
|
||||
const { patients, loading } = useData();
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
|
||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
const filteredPatients = useMemo(() => {
|
||||
return patients.filter((patient) => {
|
||||
// Search filter
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
const name = getPatientDisplayName(patient).toLowerCase();
|
||||
@@ -75,13 +76,13 @@ export const PatientsPage = () => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Status filter — treat all patients as active for now since we don't have a status field
|
||||
if (statusFilter === 'inactive') return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [patients, searchQuery, statusFilter]);
|
||||
}, [patients, searchQuery]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredPatients.length / PAGE_SIZE));
|
||||
const pagedPatients = filteredPatients.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
||||
const handleSearch = (val: string) => { setSearchQuery(val); setCurrentPage(1); };
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
@@ -103,22 +104,6 @@ export const PatientsPage = () => {
|
||||
>
|
||||
<FontAwesomeIcon icon={panelOpen ? faSidebarFlip : faSidebar} className="size-4" />
|
||||
</button>
|
||||
{/* Status filter buttons */}
|
||||
<div className="flex rounded-lg border border-secondary overflow-hidden">
|
||||
{(['all', 'active', 'inactive'] as const).map((status) => (
|
||||
<button
|
||||
key={status}
|
||||
onClick={() => setStatusFilter(status)}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition duration-100 ease-linear capitalize ${
|
||||
statusFilter === status
|
||||
? 'bg-active text-brand-secondary'
|
||||
: 'bg-primary text-tertiary hover:bg-primary_hover'
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-56">
|
||||
<Input
|
||||
@@ -126,7 +111,7 @@ export const PatientsPage = () => {
|
||||
icon={SearchLg}
|
||||
size="sm"
|
||||
value={searchQuery}
|
||||
onChange={(value) => setSearchQuery(value)}
|
||||
onChange={handleSearch}
|
||||
aria-label="Search patients"
|
||||
/>
|
||||
</div>
|
||||
@@ -154,10 +139,9 @@ export const PatientsPage = () => {
|
||||
<Table.Head label="TYPE" />
|
||||
<Table.Head label="GENDER" />
|
||||
<Table.Head label="AGE" />
|
||||
<Table.Head label="STATUS" />
|
||||
<Table.Head label="ACTIONS" />
|
||||
</Table.Header>
|
||||
<Table.Body items={filteredPatients}>
|
||||
<Table.Body items={pagedPatients}>
|
||||
{(patient) => {
|
||||
const displayName = getPatientDisplayName(patient);
|
||||
const age = computeAge(patient.dateOfBirth);
|
||||
@@ -239,13 +223,6 @@ export const PatientsPage = () => {
|
||||
</span>
|
||||
</Table.Cell>
|
||||
|
||||
{/* Status */}
|
||||
<Table.Cell>
|
||||
<Badge size="sm" color="success" type="pill-color">
|
||||
Active
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
|
||||
{/* Actions */}
|
||||
<Table.Cell>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -288,6 +265,12 @@ export const PatientsPage = () => {
|
||||
</Table>
|
||||
)}
|
||||
</TableCard.Root>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="shrink-0 border-t border-secondary px-6 py-3">
|
||||
<PaginationPageDefault page={currentPage} total={totalPages} onPageChange={setCurrentPage} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Patient Profile Panel - collapsible with smooth transition */}
|
||||
|
||||
@@ -158,6 +158,7 @@ export const TeamDashboardPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user