feat: appointment card colors by status, MassHealth badge, date nav controls

- Default card color: light (bg-slate-100 / dark text)
- Blue card when procedures selected, gray when claim has a number
- Status badge: green/red from appointment or patient-level MassHealth status
- Solid dot badge (removed ring), overflow-visible to prevent corner clipping
- Date nav: << < [today circle] > >> for week/day jumps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-04-28 10:09:08 -04:00
parent dfa04981c5
commit efe73410e7
5 changed files with 100 additions and 8 deletions

View File

@@ -24,7 +24,7 @@ export function PatientStatusBadge({
<TooltipTrigger asChild>
<span
aria-label={`Patient status: ${label}`}
className={`inline-block rounded-full ring-2 ring-white shadow ${className}`}
className={`inline-block rounded-full shadow ${className}`}
style={{
width: size,
height: size,

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { addDays, startOfToday, addMinutes } from "date-fns";
import { addDays, addWeeks, startOfToday, addMinutes } from "date-fns";
import {
parseLocalDate,
formatLocalDate,
@@ -14,6 +14,8 @@ import {
Plus,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Move,
Trash2,
CreditCard,
@@ -75,6 +77,10 @@ interface ScheduledAppointment {
patientId: number;
patientName: string;
eligibilityStatus: PatientStatus;
patientStatus: PatientStatus;
patientInsuranceProvider: string | null;
hasProcedures: boolean;
hasClaimWithNumber: boolean;
staffId: number;
date: string | Date;
startTime: string | Date;
@@ -83,6 +89,23 @@ interface ScheduledAppointment {
type: string;
}
function appointmentCardColor(apt: ScheduledAppointment): string {
if (apt.hasClaimWithNumber) return "bg-gray-500 text-white";
if (apt.hasProcedures) return "bg-blue-500 text-white";
return "bg-slate-100 text-gray-700 border-slate-300";
}
function resolveAppointmentBadgeStatus(apt: ScheduledAppointment): PatientStatus {
if (apt.eligibilityStatus === "ACTIVE") return "ACTIVE";
if (apt.eligibilityStatus === "INACTIVE") return "INACTIVE";
const isMassHealth = apt.patientInsuranceProvider?.toLowerCase().includes("masshealth");
if (apt.eligibilityStatus === "UNKNOWN" && isMassHealth) {
if (apt.patientStatus === "ACTIVE") return "ACTIVE";
if (apt.patientStatus === "INACTIVE") return "INACTIVE";
}
return apt.eligibilityStatus ?? "UNKNOWN";
}
// Define a unique ID for the appointment context menu
const APPOINTMENT_CONTEXT_MENU_ID = "appointment-context-menu";
@@ -454,6 +477,8 @@ export default function AppointmentsPage() {
: "Unknown Patient";
const eligibilityStatus = (apt as any).eligibilityStatus as PatientStatus;
const patientStatus = (patient as any)?.status as PatientStatus ?? "UNKNOWN";
const patientInsuranceProvider = (patient as any)?.insuranceProvider as string | null ?? null;
const staffId = Number(apt.staffId ?? 1);
@@ -470,6 +495,10 @@ export default function AppointmentsPage() {
...apt,
patientName,
eligibilityStatus,
patientStatus,
patientInsuranceProvider,
hasProcedures: !!(apt as any).hasProcedures,
hasClaimWithNumber: !!(apt as any).hasClaimWithNumber,
staffId,
status: apt.status ?? null,
date: formatLocalDate(apt.date),
@@ -649,7 +678,7 @@ export default function AppointmentsPage() {
return (
<div
ref={drag as unknown as React.RefObject<HTMLDivElement>} // Type assertion to make TypeScript happy
className={`${staff.color} border border-white shadow-md text-white rounded p-1 text-xs h-full overflow-hidden cursor-move relative ${
className={`${appointmentCardColor(appointment)} border shadow-md rounded p-1 text-xs h-full overflow-visible cursor-move relative ${
isDragging ? "opacity-50" : "opacity-100"
}`}
style={{ fontWeight: 500 }}
@@ -668,7 +697,7 @@ export default function AppointmentsPage() {
onContextMenu={(e) => handleContextMenu(e, appointment.id ?? 0)}
>
<PatientStatusBadge
status={appointment.eligibilityStatus ?? "UNKNOWN"}
status={resolveAppointmentBadgeStatus(appointment)}
className="pointer-events-auto" // ensure tooltip works
size={30} // bump size up from 10 → 14
/>
@@ -1247,24 +1276,52 @@ export default function AppointmentsPage() {
<div className="w-full overflow-x-auto bg-white rounded-md shadow">
<div className="p-4 border-b">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
{/* << prev week */}
<Button
variant="outline"
size="icon"
title="Previous week"
onClick={() => setSelectedDate(addWeeks(selectedDate, -1))}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
{/* < prev day */}
<Button
variant="outline"
size="icon"
title="Previous day"
onClick={() => setSelectedDate(addDays(selectedDate, -1))}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<h2 className="text-xl font-semibold">
{/* today circle */}
<Button
variant="outline"
title="Go to today"
onClick={() => setSelectedDate(startOfToday())}
className="rounded-full w-auto px-3 font-semibold text-sm"
>
{formattedSelectedDate}
</h2>
</Button>
{/* > next day */}
<Button
variant="outline"
size="icon"
title="Next day"
onClick={() => setSelectedDate(addDays(selectedDate, 1))}
>
<ChevronRight className="h-4 w-4" />
</Button>
{/* >> next week */}
<Button
variant="outline"
size="icon"
title="Next week"
onClick={() => setSelectedDate(addWeeks(selectedDate, 1))}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
{/* Top button with popover calendar */}