From a279a3e7c1af1a692093ead5cdfdaff453613ee6 Mon Sep 17 00:00:00 2001 From: Gitead Date: Thu, 23 Apr 2026 23:30:08 -0400 Subject: [PATCH] fix: resolve appointment form validation and socket connection issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix status enum in browser.ts (uppercase → lowercase values) - Fix date validation (z.string → z.coerce.date) so form accepts Date objects - Add missing type/userId/title fields to browser.ts appointment schema - Replace nested PatientSearch Select with simple inline Input to fix patient selection - Fix mutationFn to throw on non-ok responses so backend errors surface correctly - Connect socket.io directly to backend (port 5000) instead of Vite proxy to fix AggregateError on startup Co-Authored-By: Claude Sonnet 4.6 --- .../appointments/appointment-form.tsx | 104 ++------ apps/Frontend/src/lib/socket.ts | 11 +- apps/Frontend/src/pages/appointments-page.tsx | 4 +- packages/db/usedSchemas/browser.js | 252 ++++++++++++++++++ packages/db/usedSchemas/browser.ts | 7 +- 5 files changed, 287 insertions(+), 91 deletions(-) create mode 100644 packages/db/usedSchemas/browser.js diff --git a/apps/Frontend/src/components/appointments/appointment-form.tsx b/apps/Frontend/src/components/appointments/appointment-form.tsx index 5975f087..2bc56e88 100755 --- a/apps/Frontend/src/components/appointments/appointment-form.tsx +++ b/apps/Frontend/src/components/appointments/appointment-form.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { format } from "date-fns"; @@ -35,10 +35,6 @@ import { } from "@repo/db/types"; import { DateInputField } from "@/components/ui/dateInputField"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; -import { - PatientSearch, - SearchCriteria, -} from "@/components/patients/patient-search"; import { toast } from "@/hooks/use-toast"; interface AppointmentFormProps { @@ -152,75 +148,26 @@ export function AppointmentForm({ }); // ----------------------------- - // PATIENT SEARCH (reuse PatientSearch) + // PATIENT SEARCH (simple inline search) // ----------------------------- const [selectOpen, setSelectOpen] = useState(false); + const [patientSearchTerm, setPatientSearchTerm] = useState(""); + const [debouncedPatientSearch] = useDebounce(patientSearchTerm, 300); - // search criteria state (reused from patient page) - const [searchCriteria, setSearchCriteria] = useState( - null - ); - const [isSearchActive, setIsSearchActive] = useState(false); + const searchKeyPart = debouncedPatientSearch.trim() || "recent"; - // debounce search criteria so we don't hammer the backend - const [debouncedSearchCriteria] = useDebounce(searchCriteria, 300); - - const limit = 50; // dropdown size - const offset = 0; // always first page for dropdown - - // compute key used in patient page: recent or trimmed term - const searchKeyPart = useMemo( - () => debouncedSearchCriteria?.searchTerm?.trim() || "recent", - [debouncedSearchCriteria] - ); - - // Query function mirrors PatientTable logic (so backend contract is identical) const queryFn = async (): Promise => { - const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim(); - const isSearch = !!trimmedTerm && trimmedTerm.length > 0; - const rawSearchBy = debouncedSearchCriteria?.searchBy || "name"; - const validSearchKeys = [ - "name", - "phone", - "insuranceId", - "gender", - "dob", - "all", - ]; - const searchKey = validSearchKeys.includes(rawSearchBy) - ? rawSearchBy - : "name"; - - let url: string; - if (isSearch) { - const searchParams = new URLSearchParams({ - limit: String(limit), - offset: String(offset), - }); - - if (searchKey === "all") { - searchParams.set("term", trimmedTerm!); - } else { - searchParams.set(searchKey, trimmedTerm!); - } - - url = `/api/patients/search?${searchParams.toString()}`; - } else { - url = `/api/patients/recent?limit=${limit}&offset=${offset}`; - } + 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" })); + 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(); - // Expect payload to be { patients: Patient[], totalCount: number } or just an array. - // Normalize: if payload.patients exists, return it; otherwise assume array of patients. return Array.isArray(payload) ? payload : (payload.patients ?? []); }; @@ -231,12 +178,11 @@ export function AppointmentForm({ } = useQuery({ queryKey: ["patients-dropdown", searchKeyPart], queryFn, - enabled: selectOpen || !!debouncedSearchCriteria?.searchTerm, + enabled: selectOpen || debouncedPatientSearch.trim().length > 0, }); - // If select opened and no patients loaded, fetch useEffect(() => { - if (selectOpen && (!patients || patients.length === 0)) { + if (selectOpen && patients.length === 0) { refetchPatients(); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -418,11 +364,7 @@ export function AppointmentForm({ onOpenChange={(open: boolean) => { setSelectOpen(open); if (!open) { - // reset transient search state when the dropdown closes - setSearchCriteria(null); - setIsSearchActive(false); - - // Remove transient prefill if the main cached list contains it now + setPatientSearchTerm(""); if ( prefillPatient && patients && @@ -433,7 +375,6 @@ export function AppointmentForm({ setPrefillPatient(null); } } else { - // when opened, ensure initial results if (!patients || patients.length === 0) refetchPatients(); } }} @@ -458,21 +399,12 @@ export function AppointmentForm({ - {/* Reuse full PatientSearch UI inside dropdown — callbacks update the query */}
e.stopPropagation()}> - { - setSearchCriteria(criteria); - setIsSearchActive(true); - }} - onClearSearch={() => { - setSearchCriteria({ - searchTerm: "", - searchBy: "name", - }); - setIsSearchActive(false); - }} - isSearchActive={isSearchActive} + setPatientSearchTerm(e.target.value)} + onClick={(e) => e.stopPropagation()} />
diff --git a/apps/Frontend/src/lib/socket.ts b/apps/Frontend/src/lib/socket.ts index 9a458805..b11b98ae 100644 --- a/apps/Frontend/src/lib/socket.ts +++ b/apps/Frontend/src/lib/socket.ts @@ -6,13 +6,16 @@ */ import { io, Socket } from "socket.io-client"; +// Connect directly to backend to avoid Vite's WS proxy failing on upgrade, +// which causes an unhandled AggregateError from engine.io's Promise.any() probe. const SOCKET_URL = - import.meta.env.VITE_API_BASE_URL_BACKEND || - (typeof window !== "undefined" ? window.location.origin : ""); + import.meta.env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000"; export const socket: Socket = io(SOCKET_URL, { withCredentials: true, autoConnect: true, + reconnectionAttempts: 5, + reconnectionDelay: 2000, }); socket.on("connect", () => { @@ -22,3 +25,7 @@ socket.on("connect", () => { socket.on("disconnect", () => { console.log("[socket] disconnected"); }); + +socket.on("connect_error", (err) => { + console.warn("[socket] connection error:", err.message); +}); diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index b6815c9a..e04e1c7d 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -274,7 +274,9 @@ export default function AppointmentsPage() { "/api/appointments/upsert", appointment ); - return await res.json(); + const body = await res.json(); + if (!res.ok) throw new Error(body?.message || "Failed to create appointment"); + return body; }, onSuccess: (appointment) => { toast({ diff --git a/packages/db/usedSchemas/browser.js b/packages/db/usedSchemas/browser.js new file mode 100644 index 00000000..d3de3de4 --- /dev/null +++ b/packages/db/usedSchemas/browser.js @@ -0,0 +1,252 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CommunicationUncheckedCreateInputObjectSchema = exports.CloudFileUncheckedCreateInputObjectSchema = exports.CloudFolderUncheckedCreateInputObjectSchema = exports.BackupDestinationUncheckedCreateInputObjectSchema = exports.DatabaseBackupUncheckedCreateInputObjectSchema = exports.NotificationUncheckedCreateInputObjectSchema = exports.ServiceLineTransactionCreateInputObjectSchema = exports.PaymentUncheckedCreateInputObjectSchema = exports.PdfGroupUncheckedCreateInputObjectSchema = exports.PdfFileUncheckedCreateInputObjectSchema = exports.InsuranceCredentialUncheckedCreateInputObjectSchema = exports.ClaimUncheckedCreateInputObjectSchema = exports.NpiProviderUncheckedCreateInputObjectSchema = exports.AppointmentProcedureUncheckedCreateInputObjectSchema = exports.AppointmentUncheckedCreateInputObjectSchema = exports.StaffUncheckedCreateInputObjectSchema = exports.PatientUncheckedCreateInputObjectSchema = exports.UserUncheckedCreateInputObjectSchema = exports.CommunicationStatusSchema = exports.CommunicationDirectionSchema = exports.CommunicationChannelSchema = exports.PdfTitleKeySchema = exports.ProcedureSourceSchema = exports.NotificationTypesSchema = exports.PaymentStatusSchema = exports.PaymentMethodSchema = exports.ClaimStatusSchema = exports.PatientStatusSchema = void 0; +const z = __importStar(require("zod")); +// ─── Enums ──────────────────────────────────────────────────────────────────── +exports.PatientStatusSchema = z.enum(["ACTIVE", "INACTIVE", "UNKNOWN"]); +exports.ClaimStatusSchema = z.enum([ + "PENDING", + "APPROVED", + "CANCELLED", + "REVIEW", + "VOID", +]); +exports.PaymentMethodSchema = z.enum([ + "EFT", + "CHECK", + "CASH", + "CARD", + "OTHER", +]); +exports.PaymentStatusSchema = z.enum([ + "PENDING", + "PARTIALLY_PAID", + "PAID", + "OVERPAID", + "DENIED", + "VOID", +]); +exports.NotificationTypesSchema = z.enum([ + "BACKUP", + "CLAIM", + "PAYMENT", + "ETC", +]); +exports.ProcedureSourceSchema = z.enum(["COMBO", "MANUAL"]); +exports.PdfTitleKeySchema = z.enum([ + "INSURANCE_CLAIM", + "INSURANCE_CLAIM_PREAUTH", + "ELIGIBILITY_STATUS", + "CLAIM_STATUS", + "OTHER", +]); +exports.CommunicationChannelSchema = z.enum(["sms", "voice"]); +exports.CommunicationDirectionSchema = z.enum(["outbound", "inbound"]); +exports.CommunicationStatusSchema = z.enum([ + "queued", + "sent", + "delivered", + "failed", + "completed", + "busy", + "no_answer", +]); +// ─── Object Schemas ─────────────────────────────────────────────────────────── +exports.UserUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + username: z.string(), + password: z.string(), +}); +exports.PatientUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + firstName: z.string(), + lastName: z.string(), + dateOfBirth: z.string(), + phone: z.string().optional(), + email: z.string().optional(), + address: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + zipCode: z.string().optional(), + status: exports.PatientStatusSchema.optional(), +}); +exports.StaffUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + firstName: z.string(), + lastName: z.string(), + role: z.string(), + phone: z.string().optional(), + email: z.string().optional(), + userId: z.number().int().optional(), +}); +exports.AppointmentUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + patientId: z.number().int(), + userId: z.number().int().optional(), + staffId: z.number().int().optional(), + title: z.string().optional(), + date: z.coerce.date(), + startTime: z.string(), + endTime: z.string(), + type: z.string(), + status: z + .enum(["scheduled", "confirmed", "completed", "cancelled", "no-show"]) + .optional(), + notes: z.string().optional(), +}); +exports.AppointmentProcedureUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + appointmentId: z.number().int(), + patientId: z.number().int(), + procedureCode: z.string(), + procedureLabel: z.string().optional().nullable(), + fee: z.number().optional().nullable(), + category: z.string().optional().nullable(), + toothNumber: z.string().optional().nullable(), + toothSurface: z.string().optional().nullable(), + oralCavityArea: z.string().optional().nullable(), + source: exports.ProcedureSourceSchema.optional(), + comboKey: z.string().optional().nullable(), + createdAt: z.coerce.date().optional(), +}); +exports.NpiProviderUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + npiNumber: z.string(), + providerName: z.string(), + createdAt: z.coerce.date().optional(), +}); +exports.ClaimUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + patientId: z.number().int(), + appointmentId: z.number().int().optional(), + status: exports.ClaimStatusSchema.optional(), + submittedAt: z.string().optional(), + paidAt: z.string().optional(), + amount: z.number().optional(), + insuranceId: z.string().optional(), +}); +exports.InsuranceCredentialUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + siteKey: z.string(), + username: z.string(), + password: z.string(), +}); +exports.PdfFileUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + filename: z.string(), + pdfData: z.instanceof(Uint8Array), + uploadedAt: z.coerce.date().optional(), + groupId: z.number().int(), +}); +exports.PdfGroupUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + title: z.string(), + titleKey: exports.PdfTitleKeySchema.optional(), + createdAt: z.coerce.date().optional(), + patientId: z.number().int(), +}); +exports.PaymentUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + patientId: z.number().int(), + appointmentId: z.number().int().optional(), + amount: z.number(), + method: exports.PaymentMethodSchema.optional(), + status: exports.PaymentStatusSchema.optional(), + paymentDate: z.string().optional(), + updatedById: z.number().int().optional(), +}); +exports.ServiceLineTransactionCreateInputObjectSchema = z.object({ + transactionId: z.string().optional().nullable(), + paidAmount: z.number(), + adjustedAmount: z.number().optional(), + method: exports.PaymentMethodSchema, + receivedDate: z.coerce.date(), + payerName: z.string().optional().nullable(), + notes: z.string().optional().nullable(), + createdAt: z.coerce.date().optional(), +}); +exports.NotificationUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + type: exports.NotificationTypesSchema, + message: z.string(), + createdAt: z.coerce.date().optional(), + read: z.boolean().optional(), +}); +exports.DatabaseBackupUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + createdAt: z.coerce.date().optional(), +}); +exports.BackupDestinationUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + path: z.string(), + isActive: z.boolean().optional(), + createdAt: z.coerce.date().optional(), +}); +exports.CloudFolderUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + name: z.string(), + parentId: z.number().int().optional().nullable(), + createdAt: z.coerce.date().optional(), +}); +exports.CloudFileUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + userId: z.number().int(), + name: z.string(), + mimeType: z.string().optional().nullable(), + fileSize: z.bigint(), + folderId: z.number().int().optional().nullable(), + isComplete: z.boolean().optional(), + totalChunks: z.number().int().optional().nullable(), + createdAt: z.coerce.date().optional(), +}); +exports.CommunicationUncheckedCreateInputObjectSchema = z.object({ + id: z.number().int().optional(), + patientId: z.number().int(), + userId: z.number().int().optional().nullable(), + channel: exports.CommunicationChannelSchema, + direction: exports.CommunicationDirectionSchema, + status: exports.CommunicationStatusSchema, + body: z.string().optional().nullable(), + callDuration: z.number().int().optional().nullable(), + twilioSid: z.string().optional().nullable(), + createdAt: z.coerce.date().optional(), +}); diff --git a/packages/db/usedSchemas/browser.ts b/packages/db/usedSchemas/browser.ts index d3a16a7b..7a6a7d4a 100644 --- a/packages/db/usedSchemas/browser.ts +++ b/packages/db/usedSchemas/browser.ts @@ -107,12 +107,15 @@ export const StaffUncheckedCreateInputObjectSchema = z.object({ export const AppointmentUncheckedCreateInputObjectSchema = z.object({ id: z.number().int().optional(), patientId: z.number().int(), + userId: z.number().int().optional(), staffId: z.number().int().optional(), - date: z.string(), + title: z.string().optional(), + date: z.coerce.date(), startTime: z.string(), endTime: z.string(), + type: z.string(), status: z - .enum(["SCHEDULED", "COMPLETED", "CANCELLED", "NO_SHOW"]) + .enum(["scheduled", "confirmed", "completed", "cancelled", "no-show"]) .optional(), notes: z.string().optional(), });