feat: appointments v2 + patients redesign + call history agent filter + datepicker placement

Appointments v2:
 - Lean 6-column table (eye icon, patient 2-line, date+time 2-line,
   doctor+dept 2-line, status badge, reminder button)
 - Detail side panel on eye click (read-only: all fields + patient phone
   via PhoneActionCell)
 - Reschedule flow: pencil in panel → modal confirm → dedicated
   ReschedulePanel with department/doctor/date/slot/complaint fields
 - Cancel flow: modal confirm before cancelling
 - WhatsApp reminder button for upcoming booked appointments
 - DatePicker popoverPlacement prop for narrow panels (opens upward)

Patients page redesign:
 - Phone column uses PhoneActionCell (clickable to dial)
 - Email split into own column
 - Actions column replaced by hamburger menu (SMS + WhatsApp)
 - View (eye) button removed — row click opens profile panel

Call History agent filter:
 - Missed calls excluded from agent's personal history
 - Chain name parsing for agent matching
 - "Missed" filter option hidden for agents
 - Subtitle: "134 completed" (no "0 missed")

DatePicker:
 - New popoverPlacement prop forwarded to AriaPopover
 - Default "bottom start", use "top start" in constrained panels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 20:51:57 +05:30
parent df08bcfc19
commit dd8e05b343
5 changed files with 800 additions and 58 deletions

View File

@@ -1,17 +1,16 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { useEffect, useMemo, useRef, useState } from 'react';
// useNavigate removed — row click opens profile panel
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser, faMagnifyingGlass, faEye, faCommentDots, faMessageDots, faSidebarFlip, faSidebar } from '@fortawesome/pro-duotone-svg-icons';
import { faUser, faMagnifyingGlass, faCommentDots, faMessageDots, faEllipsisVertical, faSidebarFlip, faSidebar } from '@fortawesome/pro-duotone-svg-icons';
import { faIcon } from '@/lib/icon-wrapper';
const SearchLg = faIcon(faMagnifyingGlass);
import { Avatar } from '@/components/base/avatar/avatar';
// 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 { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
import { PatientProfilePanel } from '@/components/shared/patient-profile-panel';
import { useData } from '@/providers/data-provider';
import { getInitials } from '@/lib/format';
@@ -55,9 +54,52 @@ const getPatientEmail = (patient: Patient): string => {
return patient.emails?.primaryEmail ?? '';
};
const HamburgerMenu = ({ phone }: { phone: string }) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
return (
<div className="relative" ref={ref}>
<button
onClick={(e) => { e.stopPropagation(); setOpen(!open); }}
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"
title="More actions"
>
<FontAwesomeIcon icon={faEllipsisVertical} className="size-4" />
</button>
{open && (
<div className="absolute top-full right-0 mt-1 w-40 rounded-xl bg-primary shadow-xl ring-1 ring-secondary z-50 overflow-hidden py-1">
<button
onClick={(e) => { e.stopPropagation(); window.open(`sms:+91${phone}`, '_self'); setOpen(false); }}
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-primary hover:bg-primary_hover text-left"
>
<FontAwesomeIcon icon={faCommentDots} className="size-3.5 text-fg-quaternary" />
Send SMS
</button>
<button
onClick={(e) => { e.stopPropagation(); window.open(`https://wa.me/91${phone}`, '_blank'); setOpen(false); }}
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-primary hover:bg-primary_hover text-left"
>
<FontAwesomeIcon icon={faMessageDots} className="size-3.5 text-fg-quaternary" />
WhatsApp
</button>
</div>
)}
</div>
);
};
export const PatientsPage = () => {
const { patients, loading } = useData();
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState('');
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
const [panelOpen, setPanelOpen] = useState(false);
@@ -132,10 +174,11 @@ export const PatientsPage = () => {
<Table>
<Table.Header>
<Table.Head label="PATIENT" isRowHeader />
<Table.Head label="CONTACT" />
<Table.Head label="PHONE" />
<Table.Head label="EMAIL" />
<Table.Head label="GENDER" />
<Table.Head label="AGE" />
<Table.Head label="ACTIONS" />
<Table.Head label="" className="w-12" />
</Table.Header>
<Table.Body items={pagedPatients} dependencies={[selectedPatient?.id]}>
{(patient) => {
@@ -180,18 +223,22 @@ export const PatientsPage = () => {
</div>
</Table.Cell>
{/* Contact */}
{/* Phone — clickable to dial */}
<Table.Cell>
<div className="flex flex-col">
{phone ? (
<span className="text-sm text-secondary">{phone}</span>
) : (
<span className="text-sm text-placeholder">No phone</span>
)}
{email ? (
<span className="text-xs text-tertiary truncate max-w-[200px]">{email}</span>
) : null}
</div>
{phone ? (
<PhoneActionCell phoneNumber={phone} displayNumber={phone} />
) : (
<span className="text-sm text-placeholder">No phone</span>
)}
</Table.Cell>
{/* Email */}
<Table.Cell>
{email ? (
<span className="text-sm text-tertiary truncate max-w-[200px] block">{email}</span>
) : (
<span className="text-sm text-quaternary"></span>
)}
</Table.Cell>
{/* Gender */}
@@ -208,40 +255,11 @@ export const PatientsPage = () => {
</span>
</Table.Cell>
{/* Actions */}
{/* Hamburger — SMS + WhatsApp */}
<Table.Cell>
<div className="flex items-center gap-1">
{phone && (
<>
<ClickToCallButton
phoneNumber={phone}
size="sm"
label=""
/>
<button
onClick={() => window.open(`sms:+91${phone}`, '_self')}
title="SMS"
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-brand-secondary hover:bg-primary_hover transition duration-100 ease-linear"
>
<FontAwesomeIcon icon={faCommentDots} className="size-4" />
</button>
<button
onClick={() => window.open(`https://wa.me/91${phone}`, '_blank')}
title="WhatsApp"
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-[#25D366] hover:bg-primary_hover transition duration-100 ease-linear"
>
<FontAwesomeIcon icon={faMessageDots} className="size-4" />
</button>
</>
)}
<button
onClick={() => navigate(`/patient/${patient.id}`)}
title="View patient"
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"
>
<FontAwesomeIcon icon={faEye} className="size-4" />
</button>
</div>
{phone ? (
<HamburgerMenu phone={phone} />
) : null}
</Table.Cell>
</Table.Row>
);