From 1edf73fdc819407eb3b303ed396836c6d9b84eb6 Mon Sep 17 00:00:00 2001 From: Gitead Date: Fri, 26 Jun 2026 00:23:43 -0400 Subject: [PATCH] feat: add new frontend components, MH batch worker, and gitignore rules - Add all new Frontend source files (pages, components, hooks, utils) - Add selenium_MHBatchPaymentCheckWorker.py and MHSinglePaymentCheckWorker.py - Add install-steps-5-13.sh setup script - Update .gitignore to exclude runtime/sensitive data (backups, uploads, chat-history, keys, downloads, generated .d.ts files) while keeping folders - Add .gitkeep to preserve empty runtime folders in git Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 21 + apps/Backend/backups/.gitkeep | 0 apps/Backend/chat-history/.gitkeep | 0 apps/Backend/uploads/.gitkeep | 0 .../analytics/appointments-by-day.jsx | 58 + .../src/components/analytics/new-patients.jsx | 47 + .../appointment-procedures-dialog.jsx | 417 +++ .../appointments/add-appointment-modal.jsx | 19 + .../appointments/appointment-form.jsx | 434 +++ .../appointments/patient-status-badge.jsx | 34 + .../components/chart/lab-management-tab.jsx | 258 ++ .../src/components/chart/prescription-tab.jsx | 224 ++ .../src/components/chart/teeth-chart.jsx | 155 ++ .../components/chart/treatment-plan-tab.jsx | 202 ++ .../claims/claim-document-upload-modal.jsx | 104 + .../components/claims/claim-edit-modal.jsx | 271 ++ .../src/components/claims/claim-form.jsx | 2343 +++++++++++++++++ .../components/claims/claim-view-modal.jsx | 272 ++ .../claims/claims-of-patient-table.jsx | 52 + .../components/claims/claims-recent-table.jsx | 481 ++++ .../src/components/claims/claims-ui.jsx | 58 + .../src/components/claims/tooth-ui.jsx | 161 ++ .../components/cloud-storage/bread-crumb.jsx | 111 + .../cloud-storage/file-preview-modal.jsx | 210 ++ .../cloud-storage/files-section.jsx | 463 ++++ .../components/cloud-storage/folder-panel.jsx | 119 + .../cloud-storage/folder-section.jsx | 288 ++ .../cloud-storage/new-folder-modal.jsx | 50 + .../recent-top-level-folder-modal.jsx | 244 ++ .../components/cloud-storage/search-bar.jsx | 293 +++ .../backup-destination-manager.jsx | 179 ++ .../folder-browser-modal.jsx | 75 + .../import-database-section.jsx | 116 + .../network-backup-manager.jsx | 516 ++++ .../documents/file-preview-modal.jsx | 221 ++ .../file-upload/file-upload-zone.jsx | 195 ++ .../file-upload/multiple-file-upload-zone.jsx | 326 +++ .../insurance-status/bcbs-ma-button-modal.jsx | 212 ++ .../insurance-status/cca-button-modal.jsx | 152 ++ .../insurance-status/ddma-buton-modal.jsx | 327 +++ .../deltains-button-modal.jsx | 270 ++ .../dual-pdf-preview-modal.jsx | 157 ++ .../insurance-status/pdf-preview-modal.jsx | 174 ++ .../tufts-sco-button-modal.jsx | 267 ++ .../united-sco-button-modal.jsx | 267 ++ .../insurance/credentials-modal.jsx | 60 + .../src/components/layout/app-layout.jsx | 22 + .../src/components/layout/chatbot.jsx | 1070 ++++++++ .../components/layout/notification-bell.jsx | 196 ++ .../src/components/layout/sidebar.jsx | 302 +++ .../src/components/layout/top-app-bar.jsx | 64 + .../patient-connection/dial-pad.jsx | 200 ++ .../patient-connection/message-thread.jsx | 485 ++++ .../patient-connection/sms-template-diaog.jsx | 157 ++ .../components/patients/add-patient-modal.jsx | 107 + .../patients/patient-financial-modal.jsx | 265 ++ .../components/patients/patient-search.jsx | 181 ++ .../src/components/patients/patient-table.jsx | 1190 +++++++++ .../payments/payment-edit-modal.jsx | 811 ++++++ .../components/payments/payment-ocr-block.jsx | 318 +++ .../payment-upload-documents-block.jsx | 216 ++ .../payments/payments-of-patient-table.jsx | 90 + .../payments/payments-recent-table.jsx | 776 ++++++ .../procedure/procedure-combo-buttons.jsx | 140 + .../reports/collections-by-doctor-report.jsx | 232 ++ .../components/reports/commission-section.jsx | 393 +++ .../src/components/reports/export-button.jsx | 49 + .../reports/pagination-controls.jsx | 37 + .../reports/patients-balances-list.jsx | 77 + .../reports/patients-with-balance-report.jsx | 89 + .../src/components/reports/report-config.jsx | 107 + .../src/components/reports/summary-cards.jsx | 94 + .../components/settings/InsuranceCredForm.jsx | 119 + .../settings/ai-chat-settings-card.jsx | 1258 +++++++++ .../settings/ai-chat-templates-card.jsx | 226 ++ .../components/settings/ai-settings-card.jsx | 235 ++ .../settings/insurance-contact-card.jsx | 197 ++ .../settings/insuranceCredTable.jsx | 155 ++ .../components/settings/npiProviderForm.jsx | 99 + .../components/settings/npiProviderTable.jsx | 162 ++ .../settings/office-contact-card.jsx | 136 + .../components/settings/office-hours-card.jsx | 231 ++ .../settings/procedure-timeslot-card.jsx | 550 ++++ .../settings/program-bridge-table.jsx | 219 ++ .../settings/twilio-settings-card.jsx | 123 + .../src/components/staffs/staff-form.jsx | 75 + .../src/components/staffs/staff-table.jsx | 161 ++ .../src/components/ui/LoadingScreen.jsx | 12 + apps/Frontend/src/components/ui/accordion.jsx | 19 + .../src/components/ui/alert-dialog.jsx | 28 + apps/Frontend/src/components/ui/alert.jsx | 21 + .../src/components/ui/aspect-ratio.jsx | 3 + apps/Frontend/src/components/ui/avatar.jsx | 11 + apps/Frontend/src/components/ui/badge.jsx | 22 + .../Frontend/src/components/ui/breadcrumb.jsx | 27 + apps/Frontend/src/components/ui/button.jsx | 35 + apps/Frontend/src/components/ui/calendar.jsx | 44 + apps/Frontend/src/components/ui/card.jsx | 15 + apps/Frontend/src/components/ui/carousel.jsx | 109 + apps/Frontend/src/components/ui/chart.jsx | 164 ++ apps/Frontend/src/components/ui/checkbox.jsx | 11 + .../src/components/ui/collapsible.jsx | 6 + apps/Frontend/src/components/ui/command.jsx | 36 + .../src/components/ui/confirmationDialog.jsx | 18 + .../src/components/ui/context-menu.jsx | 51 + .../Frontend/src/components/ui/data-table.jsx | 121 + apps/Frontend/src/components/ui/dateInput.jsx | 135 + .../src/components/ui/dateInputField.jsx | 13 + .../src/components/ui/deleteDialog.jsx | 29 + apps/Frontend/src/components/ui/dialog.jsx | 30 + apps/Frontend/src/components/ui/drawer.jsx | 28 + .../src/components/ui/dropdown-menu.jsx | 53 + apps/Frontend/src/components/ui/form.jsx | 68 + .../Frontend/src/components/ui/hover-card.jsx | 9 + apps/Frontend/src/components/ui/input-otp.jsx | 20 + apps/Frontend/src/components/ui/input.jsx | 7 + apps/Frontend/src/components/ui/label.jsx | 8 + apps/Frontend/src/components/ui/menubar.jsx | 64 + .../src/components/ui/navigation-menu.jsx | 33 + .../Frontend/src/components/ui/pagination.jsx | 33 + apps/Frontend/src/components/ui/popover.jsx | 10 + apps/Frontend/src/components/ui/progress.jsx | 9 + .../src/components/ui/radio-group.jsx | 17 + apps/Frontend/src/components/ui/resizable.jsx | 12 + .../src/components/ui/scroll-area.jsx | 18 + apps/Frontend/src/components/ui/select.jsx | 51 + .../components/ui/selenium-task-banner.jsx | 35 + apps/Frontend/src/components/ui/separator.jsx | 6 + apps/Frontend/src/components/ui/sheet.jsx | 45 + apps/Frontend/src/components/ui/sidebar.jsx | 246 ++ apps/Frontend/src/components/ui/skeleton.jsx | 5 + apps/Frontend/src/components/ui/slider.jsx | 11 + apps/Frontend/src/components/ui/stat-card.jsx | 28 + apps/Frontend/src/components/ui/switch.jsx | 8 + apps/Frontend/src/components/ui/table.jsx | 21 + apps/Frontend/src/components/ui/tabs.jsx | 11 + apps/Frontend/src/components/ui/textarea.jsx | 7 + apps/Frontend/src/components/ui/toast.jsx | 34 + apps/Frontend/src/components/ui/toaster.jsx | 18 + .../src/components/ui/toggle-group.jsx | 26 + apps/Frontend/src/components/ui/toggle.jsx | 24 + apps/Frontend/src/components/ui/tooltip.jsx | 10 + apps/Frontend/src/hooks/use-auth.jsx | 144 + apps/Frontend/src/hooks/use-license.js | 14 + apps/Frontend/src/hooks/use-mobile.jsx | 15 + apps/Frontend/src/lib/chatbotFileStore.js | 10 + apps/Frontend/src/lib/protected-route.jsx | 17 + apps/Frontend/src/main.jsx | 5 + apps/Frontend/src/pages/activation-page.jsx | 144 + .../src/pages/ai-input-agent-page.jsx | 120 + apps/Frontend/src/pages/appointments-page.jsx | 1933 ++++++++++++++ apps/Frontend/src/pages/auth-page.jsx | 164 ++ apps/Frontend/src/pages/chart-page.jsx | 99 + apps/Frontend/src/pages/claims-page.jsx | 1221 +++++++++ .../Frontend/src/pages/cloud-storage-page.jsx | 147 ++ apps/Frontend/src/pages/dashboard.jsx | 286 ++ .../src/pages/database-management-page.jsx | 284 ++ .../pages/dental-shopping-login-info-page.jsx | 208 ++ .../pages/dental-shopping-search-tag-page.jsx | 24 + apps/Frontend/src/pages/documents-page.jsx | 550 ++++ .../src/pages/insurance-status-page.jsx | 1164 ++++++++ apps/Frontend/src/pages/job-monitor-page.jsx | 251 ++ apps/Frontend/src/pages/not-found.jsx | 18 + .../src/pages/patient-connection-page.jsx | 298 +++ apps/Frontend/src/pages/patients-page.jsx | 438 +++ apps/Frontend/src/pages/payments-page.jsx | 289 ++ apps/Frontend/src/pages/reports-page.jsx | 53 + apps/Frontend/src/pages/settings-page.jsx | 259 ++ .../src/utils/appointmentTypeUtils.js | 97 + apps/SeleniumService/downloads/.gitkeep | 0 .../selenium_MHBatchPaymentCheckWorker.py | 463 ++++ .../selenium_MHSinglePaymentCheckWorker.py | 218 ++ install-steps-5-13.sh | 84 + 173 files changed, 33469 insertions(+) create mode 100644 apps/Backend/backups/.gitkeep create mode 100644 apps/Backend/chat-history/.gitkeep create mode 100644 apps/Backend/uploads/.gitkeep create mode 100644 apps/Frontend/src/components/analytics/appointments-by-day.jsx create mode 100644 apps/Frontend/src/components/analytics/new-patients.jsx create mode 100644 apps/Frontend/src/components/appointment-procedures/appointment-procedures-dialog.jsx create mode 100644 apps/Frontend/src/components/appointments/add-appointment-modal.jsx create mode 100644 apps/Frontend/src/components/appointments/appointment-form.jsx create mode 100644 apps/Frontend/src/components/appointments/patient-status-badge.jsx create mode 100644 apps/Frontend/src/components/chart/lab-management-tab.jsx create mode 100644 apps/Frontend/src/components/chart/prescription-tab.jsx create mode 100644 apps/Frontend/src/components/chart/teeth-chart.jsx create mode 100644 apps/Frontend/src/components/chart/treatment-plan-tab.jsx create mode 100644 apps/Frontend/src/components/claims/claim-document-upload-modal.jsx create mode 100644 apps/Frontend/src/components/claims/claim-edit-modal.jsx create mode 100644 apps/Frontend/src/components/claims/claim-form.jsx create mode 100644 apps/Frontend/src/components/claims/claim-view-modal.jsx create mode 100644 apps/Frontend/src/components/claims/claims-of-patient-table.jsx create mode 100644 apps/Frontend/src/components/claims/claims-recent-table.jsx create mode 100644 apps/Frontend/src/components/claims/claims-ui.jsx create mode 100644 apps/Frontend/src/components/claims/tooth-ui.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/bread-crumb.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/file-preview-modal.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/files-section.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/folder-panel.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/folder-section.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/new-folder-modal.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/recent-top-level-folder-modal.jsx create mode 100644 apps/Frontend/src/components/cloud-storage/search-bar.jsx create mode 100644 apps/Frontend/src/components/database-management/backup-destination-manager.jsx create mode 100644 apps/Frontend/src/components/database-management/folder-browser-modal.jsx create mode 100644 apps/Frontend/src/components/database-management/import-database-section.jsx create mode 100644 apps/Frontend/src/components/database-management/network-backup-manager.jsx create mode 100644 apps/Frontend/src/components/documents/file-preview-modal.jsx create mode 100644 apps/Frontend/src/components/file-upload/file-upload-zone.jsx create mode 100644 apps/Frontend/src/components/file-upload/multiple-file-upload-zone.jsx create mode 100644 apps/Frontend/src/components/insurance-status/bcbs-ma-button-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/cca-button-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/ddma-buton-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/deltains-button-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/dual-pdf-preview-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/pdf-preview-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/tufts-sco-button-modal.jsx create mode 100644 apps/Frontend/src/components/insurance-status/united-sco-button-modal.jsx create mode 100644 apps/Frontend/src/components/insurance/credentials-modal.jsx create mode 100644 apps/Frontend/src/components/layout/app-layout.jsx create mode 100644 apps/Frontend/src/components/layout/chatbot.jsx create mode 100644 apps/Frontend/src/components/layout/notification-bell.jsx create mode 100644 apps/Frontend/src/components/layout/sidebar.jsx create mode 100644 apps/Frontend/src/components/layout/top-app-bar.jsx create mode 100644 apps/Frontend/src/components/patient-connection/dial-pad.jsx create mode 100644 apps/Frontend/src/components/patient-connection/message-thread.jsx create mode 100644 apps/Frontend/src/components/patient-connection/sms-template-diaog.jsx create mode 100644 apps/Frontend/src/components/patients/add-patient-modal.jsx create mode 100644 apps/Frontend/src/components/patients/patient-financial-modal.jsx create mode 100644 apps/Frontend/src/components/patients/patient-search.jsx create mode 100644 apps/Frontend/src/components/patients/patient-table.jsx create mode 100644 apps/Frontend/src/components/payments/payment-edit-modal.jsx create mode 100644 apps/Frontend/src/components/payments/payment-ocr-block.jsx create mode 100644 apps/Frontend/src/components/payments/payment-upload-documents-block.jsx create mode 100644 apps/Frontend/src/components/payments/payments-of-patient-table.jsx create mode 100644 apps/Frontend/src/components/payments/payments-recent-table.jsx create mode 100644 apps/Frontend/src/components/procedure/procedure-combo-buttons.jsx create mode 100644 apps/Frontend/src/components/reports/collections-by-doctor-report.jsx create mode 100644 apps/Frontend/src/components/reports/commission-section.jsx create mode 100644 apps/Frontend/src/components/reports/export-button.jsx create mode 100644 apps/Frontend/src/components/reports/pagination-controls.jsx create mode 100644 apps/Frontend/src/components/reports/patients-balances-list.jsx create mode 100644 apps/Frontend/src/components/reports/patients-with-balance-report.jsx create mode 100644 apps/Frontend/src/components/reports/report-config.jsx create mode 100644 apps/Frontend/src/components/reports/summary-cards.jsx create mode 100644 apps/Frontend/src/components/settings/InsuranceCredForm.jsx create mode 100644 apps/Frontend/src/components/settings/ai-chat-settings-card.jsx create mode 100644 apps/Frontend/src/components/settings/ai-chat-templates-card.jsx create mode 100644 apps/Frontend/src/components/settings/ai-settings-card.jsx create mode 100644 apps/Frontend/src/components/settings/insurance-contact-card.jsx create mode 100644 apps/Frontend/src/components/settings/insuranceCredTable.jsx create mode 100644 apps/Frontend/src/components/settings/npiProviderForm.jsx create mode 100644 apps/Frontend/src/components/settings/npiProviderTable.jsx create mode 100644 apps/Frontend/src/components/settings/office-contact-card.jsx create mode 100644 apps/Frontend/src/components/settings/office-hours-card.jsx create mode 100644 apps/Frontend/src/components/settings/procedure-timeslot-card.jsx create mode 100644 apps/Frontend/src/components/settings/program-bridge-table.jsx create mode 100644 apps/Frontend/src/components/settings/twilio-settings-card.jsx create mode 100644 apps/Frontend/src/components/staffs/staff-form.jsx create mode 100644 apps/Frontend/src/components/staffs/staff-table.jsx create mode 100644 apps/Frontend/src/components/ui/LoadingScreen.jsx create mode 100644 apps/Frontend/src/components/ui/accordion.jsx create mode 100644 apps/Frontend/src/components/ui/alert-dialog.jsx create mode 100644 apps/Frontend/src/components/ui/alert.jsx create mode 100644 apps/Frontend/src/components/ui/aspect-ratio.jsx create mode 100644 apps/Frontend/src/components/ui/avatar.jsx create mode 100644 apps/Frontend/src/components/ui/badge.jsx create mode 100644 apps/Frontend/src/components/ui/breadcrumb.jsx create mode 100644 apps/Frontend/src/components/ui/button.jsx create mode 100644 apps/Frontend/src/components/ui/calendar.jsx create mode 100644 apps/Frontend/src/components/ui/card.jsx create mode 100644 apps/Frontend/src/components/ui/carousel.jsx create mode 100644 apps/Frontend/src/components/ui/chart.jsx create mode 100644 apps/Frontend/src/components/ui/checkbox.jsx create mode 100644 apps/Frontend/src/components/ui/collapsible.jsx create mode 100644 apps/Frontend/src/components/ui/command.jsx create mode 100644 apps/Frontend/src/components/ui/confirmationDialog.jsx create mode 100644 apps/Frontend/src/components/ui/context-menu.jsx create mode 100644 apps/Frontend/src/components/ui/data-table.jsx create mode 100644 apps/Frontend/src/components/ui/dateInput.jsx create mode 100644 apps/Frontend/src/components/ui/dateInputField.jsx create mode 100644 apps/Frontend/src/components/ui/deleteDialog.jsx create mode 100644 apps/Frontend/src/components/ui/dialog.jsx create mode 100644 apps/Frontend/src/components/ui/drawer.jsx create mode 100644 apps/Frontend/src/components/ui/dropdown-menu.jsx create mode 100644 apps/Frontend/src/components/ui/form.jsx create mode 100644 apps/Frontend/src/components/ui/hover-card.jsx create mode 100644 apps/Frontend/src/components/ui/input-otp.jsx create mode 100644 apps/Frontend/src/components/ui/input.jsx create mode 100644 apps/Frontend/src/components/ui/label.jsx create mode 100644 apps/Frontend/src/components/ui/menubar.jsx create mode 100644 apps/Frontend/src/components/ui/navigation-menu.jsx create mode 100644 apps/Frontend/src/components/ui/pagination.jsx create mode 100644 apps/Frontend/src/components/ui/popover.jsx create mode 100644 apps/Frontend/src/components/ui/progress.jsx create mode 100644 apps/Frontend/src/components/ui/radio-group.jsx create mode 100644 apps/Frontend/src/components/ui/resizable.jsx create mode 100644 apps/Frontend/src/components/ui/scroll-area.jsx create mode 100644 apps/Frontend/src/components/ui/select.jsx create mode 100644 apps/Frontend/src/components/ui/selenium-task-banner.jsx create mode 100644 apps/Frontend/src/components/ui/separator.jsx create mode 100644 apps/Frontend/src/components/ui/sheet.jsx create mode 100644 apps/Frontend/src/components/ui/sidebar.jsx create mode 100644 apps/Frontend/src/components/ui/skeleton.jsx create mode 100644 apps/Frontend/src/components/ui/slider.jsx create mode 100644 apps/Frontend/src/components/ui/stat-card.jsx create mode 100644 apps/Frontend/src/components/ui/switch.jsx create mode 100644 apps/Frontend/src/components/ui/table.jsx create mode 100644 apps/Frontend/src/components/ui/tabs.jsx create mode 100644 apps/Frontend/src/components/ui/textarea.jsx create mode 100644 apps/Frontend/src/components/ui/toast.jsx create mode 100644 apps/Frontend/src/components/ui/toaster.jsx create mode 100644 apps/Frontend/src/components/ui/toggle-group.jsx create mode 100644 apps/Frontend/src/components/ui/toggle.jsx create mode 100644 apps/Frontend/src/components/ui/tooltip.jsx create mode 100644 apps/Frontend/src/hooks/use-auth.jsx create mode 100644 apps/Frontend/src/hooks/use-license.js create mode 100644 apps/Frontend/src/hooks/use-mobile.jsx create mode 100644 apps/Frontend/src/lib/chatbotFileStore.js create mode 100644 apps/Frontend/src/lib/protected-route.jsx create mode 100644 apps/Frontend/src/main.jsx create mode 100644 apps/Frontend/src/pages/activation-page.jsx create mode 100644 apps/Frontend/src/pages/ai-input-agent-page.jsx create mode 100644 apps/Frontend/src/pages/appointments-page.jsx create mode 100644 apps/Frontend/src/pages/auth-page.jsx create mode 100644 apps/Frontend/src/pages/chart-page.jsx create mode 100644 apps/Frontend/src/pages/claims-page.jsx create mode 100644 apps/Frontend/src/pages/cloud-storage-page.jsx create mode 100644 apps/Frontend/src/pages/dashboard.jsx create mode 100644 apps/Frontend/src/pages/database-management-page.jsx create mode 100644 apps/Frontend/src/pages/dental-shopping-login-info-page.jsx create mode 100644 apps/Frontend/src/pages/dental-shopping-search-tag-page.jsx create mode 100644 apps/Frontend/src/pages/documents-page.jsx create mode 100644 apps/Frontend/src/pages/insurance-status-page.jsx create mode 100644 apps/Frontend/src/pages/job-monitor-page.jsx create mode 100644 apps/Frontend/src/pages/not-found.jsx create mode 100644 apps/Frontend/src/pages/patient-connection-page.jsx create mode 100644 apps/Frontend/src/pages/patients-page.jsx create mode 100644 apps/Frontend/src/pages/payments-page.jsx create mode 100644 apps/Frontend/src/pages/reports-page.jsx create mode 100644 apps/Frontend/src/pages/settings-page.jsx create mode 100644 apps/Frontend/src/utils/appointmentTypeUtils.js create mode 100644 apps/SeleniumService/downloads/.gitkeep create mode 100644 apps/SeleniumService/selenium_MHBatchPaymentCheckWorker.py create mode 100644 apps/SeleniumService/selenium_MHSinglePaymentCheckWorker.py create mode 100755 install-steps-5-13.sh diff --git a/.gitignore b/.gitignore index 3c3629e6..8b4102e5 100755 --- a/.gitignore +++ b/.gitignore @@ -1 +1,22 @@ node_modules + +# Build cache +.turbo/ + +# Generated TypeScript declarations +**/*.d.ts +**/*.d.ts.map + +# Runtime / sensitive backend data (keep folders, ignore contents) +apps/Backend/backups/* +apps/Backend/chat-history/* +apps/Backend/uploads/* +apps/Backend/license.json +apps/Backend/network-backup-key.json +apps/SeleniumService/downloads/* + +# Keep the folders themselves +!apps/Backend/backups/.gitkeep +!apps/Backend/chat-history/.gitkeep +!apps/Backend/uploads/.gitkeep +!apps/SeleniumService/downloads/.gitkeep diff --git a/apps/Backend/backups/.gitkeep b/apps/Backend/backups/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/Backend/chat-history/.gitkeep b/apps/Backend/chat-history/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/Backend/uploads/.gitkeep b/apps/Backend/uploads/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/Frontend/src/components/analytics/appointments-by-day.jsx b/apps/Frontend/src/components/analytics/appointments-by-day.jsx new file mode 100644 index 00000000..e1bc2f24 --- /dev/null +++ b/apps/Frontend/src/components/analytics/appointments-by-day.jsx @@ -0,0 +1,58 @@ +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 new file mode 100644 index 00000000..382a623b --- /dev/null +++ b/apps/Frontend/src/components/analytics/new-patients.jsx @@ -0,0 +1,47 @@ +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 new file mode 100644 index 00000000..9d73f3e2 --- /dev/null +++ b/apps/Frontend/src/components/appointment-procedures/appointment-procedures-dialog.jsx @@ -0,0 +1,417 @@ +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 new file mode 100644 index 00000000..21f0b1b7 --- /dev/null +++ b/apps/Frontend/src/components/appointments/add-appointment-modal.jsx @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..478a2440 --- /dev/null +++ b/apps/Frontend/src/components/appointments/appointment-form.jsx @@ -0,0 +1,434 @@ +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 + +