diff --git a/apps/Frontend/src/App.jsx b/apps/Frontend/src/App.jsx deleted file mode 100755 index 0a259b16..00000000 --- a/apps/Frontend/src/App.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Switch, Route, Redirect } from "wouter"; -import React, { lazy, Suspense } from "react"; -import { Provider } from "react-redux"; -import { store } from "./redux/store"; -import { queryClient } from "./lib/queryClient"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { Toaster } from "./components/ui/toaster"; -import { TooltipProvider } from "./components/ui/tooltip"; -import { ProtectedRoute } from "./lib/protected-route"; -import { AuthProvider } from "./hooks/use-auth"; -import Dashboard from "./pages/dashboard"; -import LoadingScreen from "./components/ui/LoadingScreen"; -const AuthPage = lazy(() => import("./pages/auth-page")); -const PatientConnectionPage = lazy(() => import("./pages/patient-connection-page")); -const AppointmentsPage = lazy(() => import("./pages/appointments-page")); -const PatientsPage = lazy(() => import("./pages/patients-page")); -const SettingsPage = lazy(() => import("./pages/settings-page")); -const ClaimsPage = lazy(() => import("./pages/claims-page")); -const PaymentsPage = lazy(() => import("./pages/payments-page")); -const InsuranceStatusPage = lazy(() => import("./pages/insurance-status-page")); -const DocumentPage = lazy(() => import("./pages/documents-page")); -const DatabaseManagementPage = lazy(() => import("./pages/database-management-page")); -const ReportsPage = lazy(() => import("./pages/reports-page")); -const CloudStoragePage = lazy(() => import("./pages/cloud-storage-page")); -const JobMonitorPage = lazy(() => import("./pages/job-monitor-page")); -const ChartPage = lazy(() => import("./pages/chart-page")); -const AiInputAgentPage = lazy(() => import("./pages/ai-input-agent-page")); -const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page")); -const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page")); -const ActivationPage = lazy(() => import("./pages/activation-page")); -const NotFound = lazy(() => import("./pages/not-found")); -function Router() { - return ( - }/> - - }/> - }/> - }/> - }/> - }/> - }/> - } adminOnly/> - } adminOnly/> - }/> - }/> - }/> - }/> - } adminOnly/> - }/> - }/> - }/> - }/> - }/> - } adminOnly/> - } adminOnly/> - }/> - }/> - ); -} -function App() { - return ( - - - - - }> - - - - - - ); -} -export default App; diff --git a/apps/Frontend/src/components/analytics/appointments-by-day.jsx b/apps/Frontend/src/components/analytics/appointments-by-day.jsx deleted file mode 100644 index e1bc2f24..00000000 --- a/apps/Frontend/src/components/analytics/appointments-by-day.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, } from "recharts"; -export function AppointmentsByDay({ appointments }) { - const daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; - const countsByDay = daysOfWeek.map((day) => ({ day, count: 0 })); - // Get current date and set time to start of day (midnight) - const now = new Date(); - now.setHours(0, 0, 0, 0); - // Calculate Monday of the current week - const day = now.getDay(); // 0 = Sunday, 1 = Monday, ... - const diffToMonday = day === 0 ? -6 : 1 - day; // adjust if Sunday - const monday = new Date(now); - monday.setDate(now.getDate() + diffToMonday); - // Sunday of the current week - const sunday = new Date(monday); - sunday.setDate(monday.getDate() + 6); - // Filter appointments only from this week (Monday to Sunday) - const appointmentsThisWeek = appointments.filter((appointment) => { - if (!appointment.date) - return false; - const date = new Date(appointment.date); - // Reset time to compare just the date - date.setHours(0, 0, 0, 0); - return date >= monday && date <= sunday; - }); - // Count appointments by day for current week - appointmentsThisWeek.forEach((appointment) => { - const date = new Date(appointment.date); - const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, ... - const dayIndex = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday=0, Sunday=6 - if (countsByDay[dayIndex]) { - countsByDay[dayIndex].count += 1; - } - }); - return ( - - - Appointments by Day - -

- Distribution of appointments throughout the week -

-
- -
- - - - - - [`${value} appointments`, "Count"]} labelFormatter={(value) => `${value}`}/> - - - -
-
-
); -} diff --git a/apps/Frontend/src/components/analytics/new-patients.jsx b/apps/Frontend/src/components/analytics/new-patients.jsx deleted file mode 100644 index 382a623b..00000000 --- a/apps/Frontend/src/components/analytics/new-patients.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; -export function NewPatients({ patients }) { - // Get months for the chart - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - // Process patient data by registration month - const patientsByMonth = months.map(month => ({ name: month, count: 0 })); - // Count new patients by month - patients.forEach(patient => { - const createdDate = new Date(patient.createdAt); - const monthIndex = createdDate.getMonth(); - if (patientsByMonth[monthIndex]) { - patientsByMonth[monthIndex].count += 1; - } - }); - // Add some sample data for visual effect if no patients - if (patients.length === 0) { - // Sample data pattern similar to the screenshot - const sampleData = [17, 12, 22, 16, 15, 17, 22, 28, 20, 16]; - sampleData.forEach((value, index) => { - if (index < patientsByMonth.length) { - if (patientsByMonth[index]) { - patientsByMonth[index].count = value; - } - } - }); - } - return ( - - New Patients -

Monthly trend of new patient registrations

-
- -
- - - - - - [`${value} patients`, "Count"]} labelFormatter={(value) => `${value}`}/> - - - -
-
-
); -} diff --git a/apps/Frontend/src/components/appointment-procedures/appointment-procedures-dialog.jsx b/apps/Frontend/src/components/appointment-procedures/appointment-procedures-dialog.jsx deleted file mode 100644 index 9d73f3e2..00000000 --- a/apps/Frontend/src/components/appointment-procedures/appointment-procedures-dialog.jsx +++ /dev/null @@ -1,417 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { useQuery, useMutation } from "@tanstack/react-query"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Trash2, Plus, Save, X } from "lucide-react"; -import { apiRequest, queryClient } from "@/lib/queryClient"; -import { useToast } from "@/hooks/use-toast"; -import { PROCEDURE_COMBOS } from "@/utils/procedureCombos"; -import { findPriceMismatches, } from "@/utils/procedureCombosMapping"; -import { useLocation } from "wouter"; -import { DeleteConfirmationDialog } from "../ui/deleteDialog"; -import { DirectComboButtons, RegularComboButtons, } from "@/components/procedure/procedure-combo-buttons"; -export function AppointmentProceduresDialog({ open, onOpenChange, appointmentId, patientId, patient, serviceDate, }) { - const { toast } = useToast(); - const [, setLocation] = useLocation(); - // NPI provider state — stored per-appointment on the procedure rows - const [selectedNpiProviderId, setSelectedNpiProviderId] = useState(null); - const emptyRow = () => ({ code: "", label: "", fee: "", tooth: "", surface: "" }); - const [pendingRows, setPendingRows] = useState([emptyRow(), emptyRow(), emptyRow()]); - // reset pending rows when dialog opens - useEffect(() => { - if (open) - setPendingRows([emptyRow(), emptyRow(), emptyRow()]); - }, [open]); - // inline edit state - const [editingId, setEditingId] = useState(null); - const [editRow, setEditRow] = useState({}); - const [clearAllOpen, setClearAllOpen] = useState(false); - // price mismatch dialog - const [priceMismatches, setPriceMismatches] = useState([]); - const pendingAction = useRef(null); - const deriveInsuranceSiteKey = (provider) => { - const p = (provider || "").toLowerCase().trim(); - if (!p) - return ""; - if (p.includes("masshealth") || p === "mh" || p === "mass health") - return "MH"; - if (p.includes("commonwealth care alliance") || p === "cca") - return "CCA"; - if (p.includes("ddma") || p.includes("delta dental ma")) - return "DDMA"; - if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") - return "TuftsSCO"; - if ((p.includes("united") && p.includes("sco")) || p === "unitedsco") - return "UnitedSCO"; - return ""; - }; - const runWithPriceCheck = (procedureCode, fee, action) => { - const siteKey = deriveInsuranceSiteKey(patient?.insuranceProvider); - if (!siteKey || !procedureCode.trim() || !fee) { - action(); - return; - } - const mismatches = findPriceMismatches([{ procedureCode, totalBilled: fee, procedureDate: "" }], siteKey, patient?.dateOfBirth || "", serviceDate ?? new Date().toISOString().slice(0, 10)); - if (mismatches.length === 0) { - action(); - } - else { - pendingAction.current = action; - setPriceMismatches(mismatches); - } - }; - const savePricesToSchedule = async (mismatches) => { - const siteKey = deriveInsuranceSiteKey(patient?.insuranceProvider); - await Promise.all(mismatches.map(m => apiRequest("POST", "/api/fee-schedule/update-price", { - siteKey, - procedureCode: m.procedureCode, - price: m.enteredPrice, - }))); - }; - // ── NPI Providers ────────────────────────────────────────────── - const { data: npiProviders = [] } = useQuery({ - queryKey: ["/api/npiProviders/"], - queryFn: async () => { - const res = await apiRequest("GET", "/api/npiProviders/"); - if (!res.ok) - throw new Error("Failed to fetch NPI providers"); - return res.json(); - }, - enabled: open, - }); - // ── Procedures ───────────────────────────────────────────────── - const { data: procedures = [], isLoading } = useQuery({ - queryKey: ["appointment-procedures", appointmentId], - queryFn: async () => { - const res = await apiRequest("GET", `/api/appointment-procedures/${appointmentId}`); - if (!res.ok) - throw new Error("Failed to load procedures"); - return res.json(); - }, - enabled: open && !!appointmentId, - }); - // Sync NPI provider from saved procedures when they load - useEffect(() => { - if (!procedures.length) - return; - const saved = procedures[0]?.npiProviderId ?? null; - if (saved != null) - setSelectedNpiProviderId(Number(saved)); - }, [procedures]); - // Default NPI provider to Mary Scannell / first when none saved yet - useEffect(() => { - if (selectedNpiProviderId != null || !npiProviders.length) - return; - const mary = npiProviders.find((p) => p.providerName.toLowerCase() === "mary scannell"); - setSelectedNpiProviderId((mary ?? npiProviders[0])?.id ?? null); - }, [npiProviders, selectedNpiProviderId]); - // ── Mutations ────────────────────────────────────────────────── - const setNpiMutation = useMutation({ - mutationFn: async (npiProviderId) => { - const res = await apiRequest("PUT", `/api/appointment-procedures/set-npi-provider/${appointmentId}`, { npiProviderId }); - if (!res.ok) - throw new Error("Failed to update provider"); - }, - onSuccess: () => { - toast({ title: "Rendering provider saved" }); - queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] }); - }, - onError: (err) => { - toast({ title: "Error", description: err.message, variant: "destructive" }); - }, - }); - const bulkAddMutation = useMutation({ - mutationFn: async (rows) => { - const res = await apiRequest("POST", "/api/appointment-procedures/bulk", rows); - if (!res.ok) - throw new Error("Failed to add procedures"); - return res.json(); - }, - onSuccess: () => { - toast({ title: "Procedures saved" }); - setPendingRows([emptyRow(), emptyRow(), emptyRow()]); - queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] }); - }, - }); - const deleteMutation = useMutation({ - mutationFn: async (id) => { - const res = await apiRequest("DELETE", `/api/appointment-procedures/${id}`); - if (!res.ok) - throw new Error("Failed to delete"); - }, - onSuccess: () => { - toast({ title: "Deleted" }); - queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] }); - }, - }); - const clearAllMutation = useMutation({ - mutationFn: async () => { - const res = await apiRequest("DELETE", `/api/appointment-procedures/clear/${appointmentId}`); - if (!res.ok) - throw new Error("Failed to clear procedures"); - }, - onSuccess: () => { - toast({ title: "All procedures cleared" }); - queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] }); - setClearAllOpen(false); - }, - onError: (err) => { - toast({ title: "Error", description: err.message ?? "Failed to clear procedures", variant: "destructive" }); - }, - }); - const updateMutation = useMutation({ - mutationFn: async () => { - if (!editingId) - return; - const res = await apiRequest("PUT", `/api/appointment-procedures/${editingId}`, editRow); - if (!res.ok) - throw new Error("Failed to update"); - return res.json(); - }, - onSuccess: () => { - toast({ title: "Updated" }); - setEditingId(null); - setEditRow({}); - queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] }); - }, - }); - // ── Handlers ─────────────────────────────────────────────────── - const handleAddCombo = (comboKey) => { - const combo = PROCEDURE_COMBOS[comboKey]; - if (!combo) - return; - const rows = combo.codes.map((code, idx) => ({ - appointmentId, - patientId, - npiProviderId: selectedNpiProviderId ?? null, - procedureCode: code, - procedureLabel: combo.label, - fee: 0, - source: "COMBO", - comboKey, - toothNumber: combo.toothNumbers?.[idx] ?? null, - })); - bulkAddMutation.mutate(rows); - }; - const startEdit = (row) => { - if (!row.id) - return; - setEditingId(row.id); - setEditRow({ - procedureCode: row.procedureCode, - procedureLabel: row.procedureLabel, - fee: row.fee, - toothNumber: row.toothNumber, - toothSurface: row.toothSurface, - }); - }; - const cancelEdit = () => { setEditingId(null); setEditRow({}); }; - const handleSavePendingRows = () => { - const rows = pendingRows - .filter((r) => r.code.trim()) - .map((r) => ({ - appointmentId, - patientId, - npiProviderId: selectedNpiProviderId ?? null, - procedureCode: r.code.trim().toUpperCase(), - procedureLabel: r.label || null, - fee: r.fee ? Number(r.fee) : 0, - toothNumber: r.tooth || null, - toothSurface: r.surface || null, - source: "MANUAL", - })); - if (!rows.length) - return; - bulkAddMutation.mutate(rows); - }; - const handleDirectClaim = () => { - setLocation(`/claims?appointmentId=${appointmentId}&mode=direct`); - onOpenChange(false); - }; - const handleManualClaim = () => { - setLocation(`/claims?appointmentId=${appointmentId}&mode=manual`); - onOpenChange(false); - }; - const selectedProvider = npiProviders.find((p) => p.id === selectedNpiProviderId); - // ── UI ───────────────────────────────────────────────────────── - return ( - { if (clearAllOpen) - e.preventDefault(); }} onInteractOutside={(e) => { if (clearAllOpen) - e.preventDefault(); }}> - - - Appointment Procedures - {serviceDate && {serviceDate}} - - - - {/* ── Rendering Provider ─────────────────────────────── */} -
-
- - -
- - {selectedProvider && ( - ✓ {selectedProvider.providerName} - )} -
- - {/* ── Combos ─────────────────────────────────────────── */} -
- - -
- - {/* ── Pending Lines ───────────────────────────────────── */} -
-
Add Procedures
- {/* Column headers */} -
-
Code
Label
Fee
Tooth
Surface
-
- {pendingRows.map((row, i) => (
- setPendingRows((prev) => prev.map((r, idx) => idx === i ? { ...r, code: e.target.value } : r))}/> - setPendingRows((prev) => prev.map((r, idx) => idx === i ? { ...r, label: e.target.value } : r))}/> - setPendingRows((prev) => prev.map((r, idx) => idx === i ? { ...r, fee: e.target.value } : r))}/> - setPendingRows((prev) => prev.map((r, idx) => idx === i ? { ...r, tooth: e.target.value } : r))}/> - setPendingRows((prev) => prev.map((r, idx) => idx === i ? { ...r, surface: e.target.value } : r))}/> - -
))} -
- - -
-
- - {/* ── Procedures List ─────────────────────────────────── */} -
-
-
Saved Procedures ({procedures.length})
- -
- -
-
-
Code
Label
Fee
Tooth
Surface
-
Edit
Delete
-
- - {isLoading &&
Loading...
} - {!isLoading && procedures.length === 0 && (
No procedures added yet
)} - - {procedures.map((p) => (
- {editingId === p.id ? (<> - setEditRow({ ...editRow, procedureCode: e.target.value })}/> - setEditRow({ ...editRow, procedureLabel: e.target.value })}/> - setEditRow({ ...editRow, fee: Number(e.target.value) })}/> - setEditRow({ ...editRow, toothNumber: e.target.value })}/> - setEditRow({ ...editRow, toothSurface: e.target.value })}/> -
- -
-
- -
- ) : (<> -
{p.procedureCode}
-
{p.procedureLabel}
-
{p.fee !== null && p.fee !== undefined ? String(p.fee) : ""}
-
{p.toothNumber}
-
{p.toothSurface}
-
- -
-
- -
- )} -
))} -
-
- - {/* ── Footer ─────────────────────────────────────────── */} -
-
- - -
- -
- - - setClearAllOpen(false)} onConfirm={() => { setClearAllOpen(false); clearAllMutation.mutate(); }}/> - - {/* Price mismatch dialog */} - 0} onOpenChange={open => { if (!open) - setPriceMismatches([]); }}> - - - Save new price to the app? - -
-

The following procedure prices differ from the fee schedule:

-
    - {priceMismatches.map(m => (
  • - {m.procedureCode} - Schedule: ${m.schedulePrice.toFixed(2)} - Entered: ${m.enteredPrice.toFixed(2)} -
  • ))} -
-

Do you want to save the new price(s) to the fee schedule for future use?

-
-
-
- - { - setPriceMismatches([]); - pendingAction.current?.(); - pendingAction.current = null; - }}> - No - - { - await savePricesToSchedule(priceMismatches); - setPriceMismatches([]); - pendingAction.current?.(); - pendingAction.current = null; - }}> - Yes - - -
-
-
); -} diff --git a/apps/Frontend/src/components/appointments/add-appointment-modal.jsx b/apps/Frontend/src/components/appointments/add-appointment-modal.jsx deleted file mode 100644 index 21f0b1b7..00000000 --- a/apps/Frontend/src/components/appointments/add-appointment-modal.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { AppointmentForm } from "./appointment-form"; -export function AddAppointmentModal({ open, onOpenChange, onSubmit, onDelete, isLoading, appointment, prefillData, }) { - return ( - - - - {appointment ? "Edit Appointment" : "Add New Appointment"} - - -
- { - onSubmit(data); - onOpenChange(false); - }} isLoading={isLoading} onDelete={onDelete} onOpenChange={onOpenChange}/> -
-
-
); -} diff --git a/apps/Frontend/src/components/appointments/appointment-form.jsx b/apps/Frontend/src/components/appointments/appointment-form.jsx deleted file mode 100644 index 478a2440..00000000 --- a/apps/Frontend/src/components/appointments/appointment-form.jsx +++ /dev/null @@ -1,434 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { format } from "date-fns"; -import { apiRequest } from "@/lib/queryClient"; -import { APPOINTMENT_TYPES } from "@/utils/appointmentTypeUtils"; -import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Clock } from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; -import { useAuth } from "@/hooks/use-auth"; -import { useDebounce } from "use-debounce"; -import { insertAppointmentSchema, } from "@repo/db/types"; -import { DateInputField } from "@/components/ui/dateInputField"; -import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; -import { toast } from "@/hooks/use-toast"; -export function AppointmentForm({ appointment, prefillData, onSubmit, onDelete, onOpenChange, isLoading = false, }) { - const { user } = useAuth(); - const inputRef = useRef(null); - const [prefillPatient, setPrefillPatient] = useState(null); - const [otherTypeDesc, setOtherTypeDesc] = useState(() => { - const t = appointment?.type ?? ""; - return t.startsWith("other:") ? t.slice(6) : ""; - }); - // Track whether the user explicitly changed the type during this edit session. - // Used to set typeLocked so the auto-sync won't overwrite a deliberate choice. - const originalType = useRef(appointment?.type ?? ""); - const [typeChangedByUser, setTypeChangedByUser] = useState(false); - useEffect(() => { - const timeout = setTimeout(() => { - inputRef.current?.focus(); - }, 50); // small delay ensures content is mounted - return () => clearTimeout(timeout); - }, []); - const { data: staffMembersRaw = [] } = useQuery({ - queryKey: ["/api/staffs/"], - queryFn: async () => { - const res = await apiRequest("GET", "/api/staffs/"); - return res.json(); - }, - enabled: !!user, - }); - const colorMap = { - "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", - })); - // Format the date and times for the form - const defaultValues = appointment - ? { - userId: user?.id, - patientId: appointment.patientId, - title: appointment.title, - date: parseLocalDate(appointment.date), - startTime: appointment.startTime || "09:00", - endTime: appointment.endTime || "09:30", - type: appointment.type?.startsWith("other:") ? "other" : appointment.type, - notes: appointment.notes || "", - status: appointment.status || "scheduled", - staffId: typeof appointment.staffId === "number" - ? appointment.staffId - : undefined, - } - : prefillData - ? { - userId: user?.id, - patientId: prefillData.patientId, - date: prefillData.date ? parseLocalDate(prefillData.date) : new Date(), - title: "", - startTime: prefillData.startTime, - endTime: prefillData.endTime, - type: prefillData.type || "checkup", - status: "scheduled", - notes: "", - staffId: prefillData.staffId, - } - : { - userId: user?.id ?? 0, - date: new Date(), - title: "", - startTime: "09:00", - endTime: "09:30", - type: "checkup", - status: "scheduled", - staffId: staffMembers?.[0]?.id ?? undefined, - }; - const form = useForm({ - resolver: zodResolver(insertAppointmentSchema), - defaultValues, - }); - // ----------------------------- - // PATIENT SEARCH (simple inline search) - // ----------------------------- - const [selectOpen, setSelectOpen] = useState(false); - const [patientSearchTerm, setPatientSearchTerm] = useState(""); - const [debouncedPatientSearch] = useDebounce(patientSearchTerm, 300); - const searchKeyPart = debouncedPatientSearch.trim() || "recent"; - const queryFn = async () => { - const trimmed = debouncedPatientSearch.trim(); - const url = trimmed - ? `/api/patients/search?name=${encodeURIComponent(trimmed)}&limit=50&offset=0` - : `/api/patients/recent?limit=50&offset=0`; - const res = await apiRequest("GET", url); - if (!res.ok) { - const err = await res.json().catch(() => ({ message: "Failed to fetch patients" })); - throw new Error(err.message || "Failed to fetch patients"); - } - const payload = await res.json(); - return Array.isArray(payload) ? payload : (payload.patients ?? []); - }; - const { data: patients = [], isFetching: isFetchingPatients, refetch: refetchPatients, } = useQuery({ - queryKey: ["patients-dropdown", searchKeyPart], - queryFn, - enabled: selectOpen || debouncedPatientSearch.trim().length > 0, - }); - useEffect(() => { - if (selectOpen && patients.length === 0) { - refetchPatients(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectOpen]); - // Prefill form from prefillData prop (new appointment slot click) - useEffect(() => { - if (!prefillData) - return; - form.setValue("staffId", prefillData.staffId); - form.setValue("startTime", prefillData.startTime); - form.setValue("endTime", prefillData.endTime); - form.setValue("date", parseLocalDate(prefillData.date)); - if (prefillData.type) - form.setValue("type", prefillData.type); - if (prefillData.patientId) { - form.setValue("patientId", prefillData.patientId); - (async () => { - try { - const res = await apiRequest("GET", `/api/patients/${prefillData.patientId}`); - if (res.ok) - setPrefillPatient(await res.json()); - } - catch { } - })(); - } - }, [prefillData]); - // When editing an appointment, ensure we prefill the patient so SelectValue can render - useEffect(() => { - if (!appointment?.patientId) - return; - const pid = Number(appointment.patientId); - if (Number.isNaN(pid)) - return; - // set form value immediately so the select has a value - form.setValue("patientId", pid); - // fetch the single patient record and set prefill - (async () => { - try { - const res = await apiRequest("GET", `/api/patients/${pid}`); - if (res.ok) { - const patientRecord = await res.json(); - setPrefillPatient(patientRecord); - } - else { - let msg = `Failed to load patient (status ${res.status})`; - try { - const body = await res.json().catch(() => null); - if (body && body.message) - msg = body.message; - } - catch { } - toast({ - title: "Could not load patient", - description: msg, - variant: "destructive", - }); - } - } - catch (err) { - toast({ - title: "Error fetching patient", - description: err?.message || - "An unknown error occurred while fetching patient details.", - variant: "destructive", - }); - } - })(); - // note: we intentionally do NOT remove prefillPatientd here; it will be cleared when dropdown opens and main list contains the patient - }, [appointment?.patientId]); - const handleSubmit = (data) => { - // Make sure patientId is a number - const patientId = typeof data.patientId === "string" - ? parseInt(data.patientId, 10) - : data.patientId; - // Auto-create title if it's empty - let title = data.title; - if (!title || title.trim() === "") { - // Format: "April 19" - just the date - title = format(data.date, "MMMM d"); - } - const notes = data.notes || ""; - const selectedStaff = staffMembers.find((staff) => staff.id?.toString() === data.staffId) || - staffMembers[0]; - if (!selectedStaff) { - console.error("No staff selected and no available staff in the list"); - return; - } - const formattedDate = formatLocalDate(data.date); - const resolvedType = data.type === "other" && otherTypeDesc.trim() - ? `other:${otherTypeDesc.trim()}` - : data.type; - onSubmit({ - ...data, - userId: Number(user?.id), - title, - notes, - patientId, - date: formattedDate, - startTime: data.startTime, - endTime: data.endTime, - type: resolvedType, - // Lock the type when the user has explicitly changed it on an existing appointment - ...(appointment && typeChangedByUser ? { typeLocked: true } : {}), - }); - }; - return (
-
- { - handleSubmit(data); - }, (errors) => { - console.error("Validation failed:", errors); - })} className="space-y-6"> - ( - Patient - - setPatientSearchTerm(e.target.value)} onClick={(e) => e.stopPropagation()}/> -
- - {/* Prefill patient only if main list does not already include them */} - {prefillPatient && - !patients.some((p) => Number(p.id) === Number(prefillPatient.id)) && ( -
- - {prefillPatient.firstName}{" "} - {prefillPatient.lastName} - - - DOB:{" "} - {prefillPatient.dateOfBirth - ? new Date(prefillPatient.dateOfBirth).toLocaleDateString() - : ""}{" "} - • {prefillPatient.phone ?? ""} - -
-
)} - -
- {isFetchingPatients ? (
- Loading... -
) : patients && patients.length > 0 ? (patients.map((patient) => ( -
- - {patient.firstName} {patient.lastName} - - - DOB:{" "} - {new Date(patient.dateOfBirth).toLocaleDateString()}{" "} - • {patient.phone} - -
-
))) : (
- No patients found -
)} -
- - - - - )}/> - - ( - - Appointment Title{" "} - - (optional) - - - - - - - )}/> - - - -
- ( - Start Time - -
- - -
-
- -
)}/> - - ( - End Time - -
- - -
-
- -
)}/> -
- - ( - Appointment Type - - {field.value === "other" && ( setOtherTypeDesc(e.target.value)} disabled={isLoading} autoFocus/>)} - - )}/> - - ( - Status - - - )}/> - - ( - Doctor/Hygienist - - - )}/> - - ( - Notes - -