Files
DentalManagement2025/apps/Frontend/src/pages/appointments-page.tsx

926 lines
31 KiB
TypeScript

import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { format, addDays, startOfToday, addMinutes } from "date-fns";
import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar";
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
import { ClaimModal } from "@/components/claims/claim-modal";
import { Button } from "@/components/ui/button";
import {
Calendar as CalendarIcon,
Plus,
ChevronLeft,
ChevronRight,
RefreshCw,
Move,
Trash2,
FileText,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { z } from "zod";
import { Calendar } from "@/components/ui/calendar";
import {
AppointmentUncheckedCreateInputObjectSchema,
PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/shared/schemas";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useAuth } from "@/hooks/use-auth";
import {
Card,
CardContent,
CardHeader,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Menu, Item, useContextMenu } from "react-contexify";
import "react-contexify/ReactContexify.css";
import { useLocation } from "wouter";
//creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
// Define types for scheduling
interface TimeSlot {
time: string;
displayTime: string;
}
interface Staff {
id: string;
name: string;
role: "doctor" | "hygienist";
color: string;
}
interface ScheduledAppointment {
id?: number;
patientId: number;
patientName: string;
staffId: string;
date: string | Date;
startTime: string | Date;
endTime: string | Date;
status: string | null;
type: string;
}
// Define a unique ID for the appointment context menu
const APPOINTMENT_CONTEXT_MENU_ID = "appointment-context-menu";
export default function AppointmentsPage() {
const { toast } = useToast();
const { user } = useAuth();
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isClaimModalOpen, setIsClaimModalOpen] = useState(false);
const [claimAppointmentId, setClaimAppointmentId] = useState<number | null>(
null
);
const [claimPatientId, setClaimPatientId] = useState<number | null>(null);
const [editingAppointment, setEditingAppointment] = useState<
Appointment | undefined
>(undefined);
const [selectedDate, setSelectedDate] = useState<Date>(startOfToday());
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [location] = useLocation();
// Create context menu hook
const { show } = useContextMenu({
id: APPOINTMENT_CONTEXT_MENU_ID,
});
//Fetching staff memebers
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
useQuery<Staff[]>({
queryKey: ["/api/staffs/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/staffs/");
return res.json();
},
enabled: !!user,
});
const colorMap: Record<string, string> = {
"Dr. Kai Gao": "bg-blue-600",
"Dr. Jane Smith": "bg-emerald-600",
};
const staffMembers = staffMembersRaw.map((staff) => ({
...staff,
color: colorMap[staff.name] || "bg-gray-400",
}));
// Generate time slots from 8:00 AM to 6:00 PM in 30-minute increments
const timeSlots: TimeSlot[] = [];
for (let hour = 8; hour <= 18; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const hour12 = hour > 12 ? hour - 12 : hour;
const period = hour >= 12 ? "PM" : "AM";
const timeStr = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
const displayTime = `${hour12}:${minute.toString().padStart(2, "0")} ${period}`;
timeSlots.push({ time: timeStr, displayTime });
}
}
// Fetch appointments
const {
data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments,
refetch: refetchAppointments,
} = useQuery<Appointment[]>({
queryKey: ["/api/appointments/all"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/appointments/all");
return res.json();
},
enabled: !!user,
});
// Fetch patients (needed for the dropdowns)
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
Patient[]
>({
queryKey: ["/api/patients/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/patients/");
return res.json();
},
enabled: !!user,
});
// Handle creating a new appointment at a specific time slot and for a specific staff member
const handleCreateAppointmentAtSlot = (
timeSlot: TimeSlot,
staffId: string
) => {
// Calculate end time (30 minutes after start time)
const startHour = parseInt(timeSlot.time.split(":")[0] as string);
const startMinute = parseInt(timeSlot.time.split(":")[1] as string);
const startDate = new Date(selectedDate);
startDate.setHours(startHour, startMinute, 0);
const endDate = addMinutes(startDate, 30);
const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
// Find staff member
const staff = staffMembers.find((s) => s.id === staffId);
// Pre-fill appointment form with default values
const newAppointment = {
date: format(selectedDate, "yyyy-MM-dd"),
startTime: timeSlot.time, // This is in "HH:MM" format
endTime: endTime,
type: staff?.role === "doctor" ? "checkup" : "cleaning",
status: "scheduled",
title: `Appointment with ${staff?.name}`, // Add staff name in title for easier display
notes: `Appointment with ${staff?.name}`, // Store staff info in notes for processing
staff: staffId, // This matches the 'staff' field in the appointment form schema
};
// For new appointments, set editingAppointment to undefined
// This will ensure we go to the "create" branch in handleAppointmentSubmit
setEditingAppointment(undefined);
// But still pass the pre-filled data to the modal
setIsAddModalOpen(true);
// Store the prefilled values in state or sessionStorage to access in the modal
// Clear any existing data first to ensure we're not using old data
sessionStorage.removeItem("newAppointmentData");
sessionStorage.setItem(
"newAppointmentData",
JSON.stringify(newAppointment)
);
};
// Check for newPatient parameter in URL
useEffect(() => {
if (patients.length === 0 || !user) return;
// Parse URL search params to check for newPatient
const params = new URLSearchParams(window.location.search);
const newPatientId = params.get("newPatient");
if (newPatientId) {
const patientId = parseInt(newPatientId);
// Find the patient in our list
const patient = (patients as Patient[]).find((p) => p.id === patientId);
if (patient) {
toast({
title: "Patient Added",
description: `${patient.firstName} ${patient.lastName} was added successfully. You can now schedule an appointment.`,
});
// Select first available staff member
const staffId = staffMembers[0]!.id;
// Find first time slot today (9:00 AM is a common starting time)
const defaultTimeSlot =
timeSlots.find((slot) => slot.time === "09:00") || timeSlots[0];
// Open appointment modal with prefilled patient
handleCreateAppointmentAtSlot(defaultTimeSlot!, staffId);
// Pre-select the patient in the appointment form
const patientData = {
patientId: patient.id,
};
// Store info in session storage for the modal to pick up
const existingData = sessionStorage.getItem("newAppointmentData");
if (existingData) {
const parsedData = JSON.parse(existingData);
sessionStorage.setItem(
"newAppointmentData",
JSON.stringify({
...parsedData,
...patientData,
})
);
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [patients, user, location]);
// Create appointment mutation
const createAppointmentMutation = useMutation({
mutationFn: async (appointment: InsertAppointment) => {
const res = await apiRequest("POST", "/api/appointments/", appointment);
return await res.json();
},
onSuccess: () => {
toast({
title: "Success",
description: "Appointment created successfully.",
});
// Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
setIsAddModalOpen(false);
},
onError: (error: Error) => {
toast({
title: "Error",
description: `Failed to create appointment: ${error.message}`,
variant: "destructive",
});
},
});
// Update appointment mutation
const updateAppointmentMutation = useMutation({
mutationFn: async ({
id,
appointment,
}: {
id: number;
appointment: UpdateAppointment;
}) => {
const res = await apiRequest(
"PUT",
`/api/appointments/${id}`,
appointment
);
return await res.json();
},
onSuccess: () => {
toast({
title: "Success",
description: "Appointment updated successfully.",
});
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
setEditingAppointment(undefined);
setIsAddModalOpen(false);
},
onError: (error: Error) => {
toast({
title: "Error",
description: `Failed to update appointment: ${error.message}`,
variant: "destructive",
});
},
});
// Delete appointment mutation
const deleteAppointmentMutation = useMutation({
mutationFn: async (id: number) => {
await apiRequest("DELETE", `/api/appointments/${id}`);
},
onSuccess: () => {
toast({
title: "Success",
description: "Appointment deleted successfully.",
});
// Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
},
onError: (error: Error) => {
toast({
title: "Error",
description: `Failed to delete appointment: ${error.message}`,
variant: "destructive",
});
},
});
// Handle appointment submission (create or update)
const handleAppointmentSubmit = (
appointmentData: InsertAppointment | UpdateAppointment
) => {
// Make sure the date is for the selected date
const updatedData = {
...appointmentData,
date: format(selectedDate, "yyyy-MM-dd"),
};
// Check if we're editing an existing appointment with a valid ID
if (
editingAppointment &&
"id" in editingAppointment &&
typeof editingAppointment.id === "number"
) {
updateAppointmentMutation.mutate({
id: editingAppointment.id,
appointment: updatedData as unknown as UpdateAppointment,
});
} else {
// This is a new appointment
if (user) {
createAppointmentMutation.mutate({
...(updatedData as unknown as InsertAppointment),
userId: user.id,
});
}
}
};
// Handle edit appointment
const handleEditAppointment = (appointment: Appointment) => {
setEditingAppointment(appointment);
setIsAddModalOpen(true);
};
// Handle delete appointment
const handleDeleteAppointment = (id: number) => {
if (confirm("Are you sure you want to delete this appointment?")) {
deleteAppointmentMutation.mutate(id);
}
};
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
// Get formatted date string for display
const formattedDate = format(selectedDate, "yyyy-MM-dd");
const selectedDateAppointments = appointments.filter((apt) => {
// Ensure apt.date is in 'yyyy-MM-dd' format before comparison
const appointmentDate = format(new Date(apt.date), "yyyy-MM-dd");
return appointmentDate === formattedDate;
});
// Process appointments for the scheduler view
const processedAppointments: ScheduledAppointment[] =
selectedDateAppointments.map((apt) => {
// Find patient name
const patient = patients.find((p) => p.id === apt.patientId);
const patientName = patient
? `${patient.firstName} ${patient.lastName}`
: "Unknown Patient";
// Try to determine the staff from the notes or title
let staffId = "doctor1"; // Default to first doctor if we can't determine
// Check notes first
if (apt.notes) {
// Look for "Appointment with Dr. X" or similar patterns
for (const staff of staffMembers) {
if (apt.notes.includes(staff.name)) {
staffId = staff.id;
break;
}
}
}
// If no match in notes, check title
if (staffId === "doctor1" && apt.title) {
for (const staff of staffMembers) {
if (apt.title.includes(staff.name)) {
staffId = staff.id;
break;
}
}
}
const processed = {
...apt,
patientName,
staffId,
status: apt.status ?? null, // Default to null if status is undefined
date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d
};
return processed;
});
// Check if appointment exists at a specific time slot and staff
const getAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => {
if (processedAppointments.length === 0) {
return undefined;
}
// In appointments for a given time slot, we'll just display the first one
// In a real application, you might want to show multiple or stack them
const appointmentsAtSlot = processedAppointments.filter((apt) => {
// Fix time format comparison - the database adds ":00" seconds to the time
const dbTime =
typeof apt.startTime === "string" ? apt.startTime.substring(0, 5) : "";
const timeMatches = dbTime === timeSlot.time;
const staffMatches = apt.staffId === staffId;
return timeMatches && staffMatches;
});
return appointmentsAtSlot.length > 0 ? appointmentsAtSlot[0] : undefined;
};
const isLoading =
isLoadingAppointments ||
isLoadingPatients ||
createAppointmentMutation.isPending ||
updateAppointmentMutation.isPending ||
deleteAppointmentMutation.isPending;
// Define drag item types
const ItemTypes = {
APPOINTMENT: "appointment",
};
// Handle moving an appointment to a new time slot and staff
const handleMoveAppointment = (
appointmentId: number,
newTimeSlot: TimeSlot,
newStaffId: string
) => {
const appointment = appointments.find((a) => a.id === appointmentId);
if (!appointment) return;
// Calculate new end time (30 minutes from start)
const startHour = parseInt(newTimeSlot.time.split(":")[0] as string);
const startMinute = parseInt(newTimeSlot.time.split(":")[1] as string);
const startDate = new Date(selectedDate);
startDate.setHours(startHour, startMinute, 0);
const endDate = addMinutes(startDate, 30);
const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
// Find staff member
const staff = staffMembers.find((s) => s.id === newStaffId);
// Update appointment data
const { id, createdAt, ...sanitizedAppointment } = appointment;
const updatedAppointment: UpdateAppointment = {
...sanitizedAppointment,
startTime: newTimeSlot.time, // Already in HH:MM format
endTime: endTime, // Already in HH:MM format
notes: `Appointment with ${staff?.name}`,
staffId: newStaffId, // Update staffId
};
// Call update mutation
updateAppointmentMutation.mutate({
id: appointmentId,
appointment: updatedAppointment,
});
};
// Function to display context menu
const handleContextMenu = (e: React.MouseEvent, appointmentId: number) => {
// Prevent the default browser context menu
e.preventDefault();
// Show our custom context menu with appointment ID as data
show({
event: e,
props: {
appointmentId,
},
});
};
// Create a draggable appointment component
function DraggableAppointment({
appointment,
staff,
}: {
appointment: ScheduledAppointment;
staff: Staff;
}) {
const [{ isDragging }, drag] = useDrag(() => ({
type: ItemTypes.APPOINTMENT,
item: { id: appointment.id },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
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 ${
isDragging ? "opacity-50" : "opacity-100"
}`}
style={{ fontWeight: 500 }}
onClick={(e) => {
// Only allow edit on click if we're not dragging
if (!isDragging) {
const fullAppointment = appointments.find(
(a) => a.id === appointment.id
);
if (fullAppointment) {
e.stopPropagation();
handleEditAppointment(fullAppointment);
}
}
}}
onContextMenu={(e) => handleContextMenu(e, appointment.id ?? 0)}
>
<div className="font-bold truncate flex items-center gap-1">
<Move className="h-3 w-3" />
{appointment.patientName}
</div>
<div className="truncate">{appointment.type}</div>
</div>
);
}
// Create a drop target for appointments
function DroppableTimeSlot({
timeSlot,
staffId,
appointment,
staff,
}: {
timeSlot: TimeSlot;
staffId: string;
appointment: ScheduledAppointment | undefined;
staff: Staff;
}) {
const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: ItemTypes.APPOINTMENT,
drop: (item: { id: number }) => {
handleMoveAppointment(item.id, timeSlot, staffId);
},
canDrop: (item: { id: number }) => {
// Prevent dropping if there's already an appointment here
return !appointment;
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}));
return (
<td
ref={drop as unknown as React.RefObject<HTMLTableCellElement>}
key={`${timeSlot.time}-${staffId}`}
className={`px-1 py-1 border relative h-14 ${isOver && canDrop ? "bg-green-100" : ""}`}
>
{appointment ? (
<DraggableAppointment appointment={appointment} staff={staff} />
) : (
<button
className={`w-full h-full ${isOver && canDrop ? "bg-green-100" : "text-gray-400 hover:bg-gray-100"} rounded flex items-center justify-center`}
onClick={() => handleCreateAppointmentAtSlot(timeSlot, staffId)}
>
<Plus className="h-4 w-4" />
</button>
)}
</td>
);
}
return (
<div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar
isMobileOpen={isMobileMenuOpen}
setIsMobileOpen={setIsMobileMenuOpen}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
<main className="flex-1 overflow-y-auto p-4">
<div className="container mx-auto">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Appointment Schedule
</h1>
<p className="text-muted-foreground">
View and manage the dental practice schedule
</p>
</div>
<Button
onClick={() => {
setEditingAppointment(undefined);
setIsAddModalOpen(true);
}}
className="gap-1"
disabled={isLoading}
>
<Plus className="h-4 w-4" />
New Appointment
</Button>
</div>
{/* Context Menu */}
<Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade">
<Item
onClick={({ props }) => {
const fullAppointment = appointments.find(
(a) => a.id === props.appointmentId
);
if (fullAppointment) {
handleEditAppointment(fullAppointment);
}
}}
>
<span className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4" />
Edit Appointment
</span>
</Item>
<Item
onClick={({ props }) => {
const fullAppointment = appointments.find(
(a) => a.id === props.appointmentId
);
if (fullAppointment) {
// Set the appointment and patient IDs for the claim modal
setClaimAppointmentId(fullAppointment.id ?? null);
setClaimPatientId(fullAppointment.patientId);
// Find the patient name for the toast notification
const patient = patients.find(
(p) => p.id === fullAppointment.patientId
);
const patientName = patient
? `${patient.firstName} ${patient.lastName}`
: `Patient #${fullAppointment.patientId}`;
// Show a toast notification
toast({
title: "Claim Services Initiated",
description: `Started insurance claim process for ${patientName}`,
});
// Open the claim modal
setIsClaimModalOpen(true);
}
}}
>
<span className="flex items-center gap-2 text-blue-600">
<FileText className="h-4 w-4" />
Claim Services
</span>
</Item>
<Item
onClick={({ props }) =>
handleDeleteAppointment(props.appointmentId)
}
>
<span className="flex items-center gap-2 text-red-600">
<Trash2 className="h-4 w-4" />
Delete Appointment
</span>
</Item>
</Menu>
{/* Main Content - Split into Schedule and Calendar */}
<div className="flex flex-col lg:flex-row gap-6">
{/* Left side - Schedule Grid */}
<div className="w-full lg:w-3/4 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">
<Button
variant="outline"
size="icon"
onClick={() =>
setSelectedDate(addDays(selectedDate, -1))
}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<h2 className="text-xl font-semibold">{formattedDate}</h2>
<Button
variant="outline"
size="icon"
onClick={() =>
setSelectedDate(addDays(selectedDate, 1))
}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* Schedule Grid with Drag and Drop */}
<DndProvider backend={HTML5Backend}>
<div className="overflow-x-auto">
<table className="w-full border-collapse min-w-[800px]">
<thead>
<tr>
<th className="p-2 border bg-gray-50 w-[100px]">
Time
</th>
{staffMembers.map((staff) => (
<th
key={staff.id}
className={`p-2 border bg-gray-50 ${staff.role === "doctor" ? "font-bold" : ""}`}
>
{staff.name}
<div className="text-xs text-gray-500">
{staff.role}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{timeSlots.map((timeSlot) => (
<tr key={timeSlot.time}>
<td className="border px-2 py-1 text-xs text-gray-600 font-medium">
{timeSlot.displayTime}
</td>
{staffMembers.map((staff) => (
<DroppableTimeSlot
key={`${timeSlot.time}-${staff.id}`}
timeSlot={timeSlot}
staffId={staff.id}
appointment={getAppointmentAtSlot(
timeSlot,
staff.id
)}
staff={staff}
/>
))}
</tr>
))}
</tbody>
</table>
</div>
</DndProvider>
</div>
{/* Right side - Calendar and Stats */}
<div className="w-full lg:w-1/4 space-y-6">
{/* Calendar Card */}
<Card>
<CardHeader className="pb-2">
<CardTitle>Calendar</CardTitle>
<CardDescription>
Select a date to view or schedule appointments
</CardDescription>
</CardHeader>
<CardContent>
<Calendar
mode="single"
selected={selectedDate}
onSelect={(date) => {
if (date) setSelectedDate(date);
}}
className="rounded-md border"
/>
</CardContent>
</Card>
{/* Statistics Card */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between">
<span>Appointments</span>
<Button
variant="ghost"
size="icon"
onClick={() => refetchAppointments()}
>
<RefreshCw className="h-4 w-4" />
</Button>
</CardTitle>
<CardDescription>
Statistics for {formattedDate}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
Total appointments:
</span>
<span className="font-semibold">
{selectedDateAppointments.length}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
With doctors:
</span>
<span className="font-semibold">
{
processedAppointments.filter(
(apt) =>
staffMembers.find((s) => s.id === apt.staffId)
?.role === "doctor"
).length
}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
With hygienists:
</span>
<span className="font-semibold">
{
processedAppointments.filter(
(apt) =>
staffMembers.find((s) => s.id === apt.staffId)
?.role === "hygienist"
).length
}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</main>
</div>
{/* Add/Edit Appointment Modal */}
<AddAppointmentModal
open={isAddModalOpen}
onOpenChange={setIsAddModalOpen}
onSubmit={handleAppointmentSubmit}
isLoading={
createAppointmentMutation.isPending ||
updateAppointmentMutation.isPending
}
appointment={editingAppointment}
patients={patients}
/>
{/* Claim Services Modal */}
{claimPatientId && claimAppointmentId && (
<ClaimModal
open={isClaimModalOpen}
onClose={() => {
setIsClaimModalOpen(false);
setClaimPatientId(null);
setClaimAppointmentId(null);
}}
patientId={claimPatientId}
appointmentId={claimAppointmentId}
/>
)}
</div>
);
}