diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index d74a301..176ff40 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -19,10 +19,11 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema} from "@repo/db/usedSchemas"; import { z } from "zod"; import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; +import { MultipleFileUploadZone } from "../file-upload/multiple-file-upload-zone"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -31,12 +32,36 @@ const PatientSchema = ( }); type Patient = z.infer; + +//creating types out of schema auto generated. +type Appointment = z.infer; + +const insertAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertAppointment = z.infer; + +const updateAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); +type UpdateAppointment = z.infer; + + interface ServiceLine { - procedureCode: string; + procedure_code: string; + procedure_date: string; + oralCavityArea: string; toothNumber: string; - surface: string; - quad: string; - authNo: string; + toothSurface: string; billedAmount: string; } @@ -44,12 +69,21 @@ interface ClaimFormProps { patientId?: number; extractedData?: Partial; onSubmit: (claimData: any) => void; + onHandleAppointmentSubmit: (appointmentData: InsertAppointment | UpdateAppointment) => void; onClose: () => void; } +interface Staff { + id: string; + name: string; + role: "doctor" | "hygienist"; + color: string; +} + export function ClaimForm({ patientId, extractedData, + onHandleAppointmentSubmit, onSubmit, onClose, }: ClaimFormProps) { @@ -82,41 +116,61 @@ export function ClaimForm({ } }, [fetchedPatient]); + //Fetching staff memebers + const [staff, setStaff] = useState(null); + const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } = + useQuery({ + queryKey: ["/api/staffs/"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/staffs/"); + return res.json(); + }, + }); + // Service date state const [serviceDateValue, setServiceDateValue] = useState(new Date()); const [serviceDate, setServiceDate] = useState( - format(new Date(), "MM/dd/yy") + format(new Date(), "MM/dd/yyyy") ); -useEffect(() => { - console.log("🚨 extractedData in effect:", extractedData); - if (extractedData?.serviceDate) { - const parsed = new Date(extractedData.serviceDate); - console.log("✅ Parsed serviceDate from extractedData:", parsed); - setServiceDateValue(parsed); - setServiceDate(format(parsed, "MM/dd/yy")); - } -}, [extractedData]); + const formatServiceDate = (date: Date | undefined): string => { + return date ? format(date, "MM/dd/yyyy") : ""; + }; + + useEffect(() => { + if (extractedData?.serviceDate) { + const parsed = new Date(extractedData.serviceDate); + setServiceDateValue(parsed); + setServiceDate(formatServiceDate(parsed)); + } + }, [extractedData]); + + // used in submit button to send correct date. + function convertToISODate(mmddyyyy: string): string { + const [month, day, year] = mmddyyyy.split("/"); + return `${year}-${month?.padStart(2, "0")}-${day?.padStart(2, "0")}`; +} - // Clinical notes state - const [clinicalNotes, setClinicalNotes] = useState(""); + // Remarks state + const [remarks, setRemarks] = useState(""); - // Doctor selection state - const [doctor, setDoctor] = useState("doctor1"); - - // Service lines state with one empty default line,, - // note: this can be MAXIMUM 10 - const [serviceLines, setServiceLines] = useState([ - { - procedureCode: "", - toothNumber: "", - surface: "", - quad: "", - authNo: "", - billedAmount: "", - }, - ]); + // Service lines state with one empty default line + const [serviceLines, setServiceLines] = useState([]); + useEffect(() => { + if (serviceDate) { + setServiceLines([ + { + procedure_code: "", + procedure_date: serviceDate, + oralCavityArea: "", + toothNumber: "", + toothSurface: "", + billedAmount: "", + }, + ]); + } + }, [serviceDate]); // Update a field in serviceLines at index const updateServiceLine = ( @@ -133,6 +187,11 @@ useEffect(() => { }); }; + const updateProcedureDate = (index: number, date: Date | undefined) => { + const formatted = formatServiceDate(date); + updateServiceLine(index, "procedure_date", formatted); + }; + // Handle patient field changes (to make inputs controlled and editable) const updatePatientField = (field: keyof Patient, value: any) => { setPatient((prev) => (prev ? { ...prev, [field]: value } : null)); @@ -149,19 +208,44 @@ useEffect(() => { // Determine patient date of birth format const formatDOB = (dob: string | undefined) => { if (!dob) return ""; - if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) return dob; // already MM/DD/YYYY - if (/^\d{4}-\d{2}-\d{2}/.test(dob)) { const datePart = dob?.split("T")[0]; // safe optional chaining if (!datePart) return ""; const [year, month, day] = datePart.split("-"); return `${month}/${day}/${year}`; } - return dob; }; + // FILE UPLOAD ZONE + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isUploading, setIsUploading] = useState(false); + + const handleFileUpload = (files: File[]) => { + setIsUploading(true); + + const validFiles = files.filter((file) => file.type === "application/pdf"); + if (validFiles.length > 10) { + toast({ + title: "Too Many Files", + description: "You can only upload up to 10 PDFs.", + variant: "destructive", + }); + setIsUploading(false); + return; + } + + setUploadedFiles(validFiles); + + toast({ + title: "Files Selected", + description: `${validFiles.length} PDF file(s) ready for processing.`, + }); + + setIsUploading(false); + }; + return (
@@ -225,38 +309,16 @@ useEffect(() => { {/* Clinical Notes Entry */}
-
{/* Service Lines */} @@ -265,13 +327,14 @@ useEffect(() => { Service Lines
+ {/* Service Date */}
@@ -303,10 +378,10 @@ useEffect(() => {
Procedure Code + Procedure Date + Oral Cavity Area Tooth Number - Surface - Quadrant - Auth No. + Tooth Surface Billed Amount
@@ -314,47 +389,52 @@ useEffect(() => { {serviceLines.map((line, i) => (
- updateServiceLine(i, "procedureCode", e.target.value) + updateServiceLine(i, "procedure_code", e.target.value) + } + /> + + {/* Date Picker */} + + + + + + updateProcedureDate(i, date)} + /> + + + + + updateServiceLine(i, "oralCavityArea", e.target.value) } /> updateServiceLine(i, "toothNumber", e.target.value) } /> - updateServiceLine(i, "surface", e.target.value) - } - /> - - - updateServiceLine(i, "authNo", e.target.value) + updateServiceLine(i, "toothSurface", e.target.value) } /> { />
))} + -
+
+
- {/* File Upload Section */} -
-

- Please note that file types with 4 or more character - extensions are not allowed, such as .DOCX, .PPTX, or .XLSX -

-
-
- - -
- + {/* File Upload Section */} +
+

+ Only PDF files allowed. You can upload up to 10 files. File + types with 4+ character extensions like .DOCX, .PPTX, or .XLSX + are not allowed. +

+ +
+
+ +
+ + + + {uploadedFiles.length > 0 && ( +
    + {uploadedFiles.map((file, index) => ( +
  • {file.name}
  • + ))} +
+ )}
{/* Insurance Carriers */} @@ -425,7 +522,19 @@ useEffect(() => { Insurance Carriers
- + + ))} + +
+ ) : ( +
+ +
+

Drag and drop PDF files here

+

Or click to browse files

+
+ +

Up to {maxFiles} PDF files, 5MB each

+
+ )} +
+
+ ); +} diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index cd48af5..80634c9 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -75,11 +75,7 @@ export default function ClaimsPage() { const { user } = useAuth(); const [claimFormData, setClaimFormData] = useState({ patientId: null, - carrier: "", - doctorName: "", serviceDate: "", - clinicalNotes: "", - serviceLines: [], }); // Fetch patients @@ -154,6 +150,99 @@ export default function ClaimsPage() { }, }); + // Update appointment mutation + const updateAppointmentMutation = useMutation({ + mutationFn: async ({ + id, + appointment, + }: { + id: number; + appointment: UpdateAppointment; + }) => { + const res = await apiRequest( + "PUT", + `/api/appointments/${id}`, + appointment + ); + return await res.json(); + }, + onSuccess: () => { + toast({ + title: "Success", + description: "Appointment updated successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] }); + queryClient.invalidateQueries({ queryKey: ["/api/patients/"] }); + }, + onError: (error: Error) => { + toast({ + title: "Error", + description: `Failed to update appointment: ${error.message}`, + variant: "destructive", + }); + }, + }); + + const handleAppointmentSubmit = ( + appointmentData: InsertAppointment | UpdateAppointment + ) => { + // Converts local date to exact UTC date with no offset issues + function parseLocalDate(dateInput: Date | string): Date { + if (dateInput instanceof Date) return dateInput; + + const parts = dateInput.split("-"); + if (parts.length !== 3) { + throw new Error(`Invalid date format: ${dateInput}`); + } + + const year = Number(parts[0]); + const month = Number(parts[1]); + const day = Number(parts[2]); + + if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) { + throw new Error(`Invalid date parts in date string: ${dateInput}`); + } + + return new Date(year, month - 1, day); // month is 0-indexed + } + + const rawDate = parseLocalDate(appointmentData.date); + const formattedDate = rawDate.toLocaleDateString("en-CA"); // YYYY-MM-DD format + + // Prepare minimal data to update/create + const minimalData = { + date: rawDate.toLocaleDateString("en-CA"), // "YYYY-MM-DD" format + startTime: appointmentData.startTime || "09:00", + endTime: appointmentData.endTime || "09:30", + staffId: appointmentData.staffId, + }; + + // Find existing appointment for this patient on the same date + const existingAppointment = appointments.find( + (a) => + a.patientId === appointmentData.patientId && + new Date(a.date).toLocaleDateString("en-CA") === formattedDate + ); + + if (existingAppointment && typeof existingAppointment.id === 'number') { + // Update appointment with only date + updateAppointmentMutation.mutate({ + id: existingAppointment.id, + appointment: minimalData, + }); + } else { + // Create new appointment with required fields + defaults + createAppointmentMutation.mutate({ + ...minimalData, + patientId: appointmentData.patientId, + userId:user?.id, + title: "Scheduled Appointment", // default title + type: "checkup", // default type + } as InsertAppointment); + + } + }; + const createClaimMutation = useMutation({ mutationFn: async (claimData: any) => { const res = await apiRequest("POST", "/api/claims/", claimData); @@ -186,7 +275,7 @@ export default function ClaimsPage() { memberId: params.get("memberId") || "", dob: params.get("dob") || "", }; - }, [location]); // <== re-run when route changes + }, [location]); const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen); @@ -209,11 +298,7 @@ export default function ClaimsPage() { setSelectedAppointment(null); setClaimFormData({ patientId: null, - carrier: "", - doctorName: "", serviceDate: "", - clinicalNotes: "", - serviceLines: [], }); }; @@ -225,13 +310,9 @@ export default function ClaimsPage() { setClaimFormData((prev: any) => ({ ...prev, patientId: patient.id, - carrier: patient.insuranceProvider || "", - doctorName: user?.username || "", serviceDate: lastAppointment ? new Date(lastAppointment.date).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10), - clinicalNotes: "", - serviceLines: [], })); }; @@ -279,16 +360,15 @@ export default function ClaimsPage() { createClaimMutation.mutate(claimData); } - const getDisplayProvider = (provider: string) => { - const insuranceMap: Record = { - delta: "Delta Dental", - metlife: "MetLife", - cigna: "Cigna", - aetna: "Aetna", + const getDisplayProvider = (provider: string) => { + const insuranceMap: Record = { + delta: "Delta Dental", + metlife: "MetLife", + cigna: "Cigna", + aetna: "Aetna", + }; + return insuranceMap[provider?.toLowerCase()] || provider; }; - return insuranceMap[provider?.toLowerCase()] || provider; -}; - // Get unique patients with appointments const patientsWithAppointments = appointments.reduce( @@ -391,7 +471,10 @@ export default function ClaimsPage() {

{item.patientName}

- Insurance: {getDisplayProvider(item.insuranceProvider)} + + Insurance:{" "} + {getDisplayProvider(item.insuranceProvider)} + • ID: {item.insuranceId} @@ -435,6 +518,7 @@ export default function ClaimsPage() { onClose={closeClaim} extractedData={claimFormData} onSubmit={handleClaimSubmit} + onHandleAppointmentSubmit={handleAppointmentSubmit} /> )}
diff --git a/apps/Frontend/src/pages/patients-page.tsx b/apps/Frontend/src/pages/patients-page.tsx index 82aa486..a7c3cf4 100644 --- a/apps/Frontend/src/pages/patients-page.tsx +++ b/apps/Frontend/src/pages/patients-page.tsx @@ -87,11 +87,6 @@ export default function PatientsPage() { const [uploadedFile, setUploadedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); const [isExtracting, setIsExtracting] = useState(false); - const [formData, setFormData] = useState({ - PatientName: "", - PatientMemberId: "", - PatientDob: "", - }); const { mutate: extractPdf } = useExtractPdfData(); const [location, navigate] = useLocation(); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 45efce1..56d4e81 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -81,33 +81,37 @@ model Staff { phone String? createdAt DateTime @default(now()) appointments Appointment[] + claims Claim[] @relation("ClaimStaff") } model Claim { - id Int @id @default(autoincrement()) - patientId Int - appointmentId Int - clinicalNotes String - serviceDate DateTime - doctorName String - carrier String // e.g., "Delta MA" - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - patient Patient @relation(fields: [patientId], references: [id]) - appointment Appointment @relation(fields: [appointmentId], references: [id]) - serviceLines ServiceLine[] - User User? @relation(fields: [userId], references: [id]) - userId Int? + id Int @id @default(autoincrement()) + patientId Int + appointmentId Int + userId Int + staffId Int + remarks String + serviceDate DateTime + insuranceProvider String // e.g., "Delta MA" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + status String @default("pending") // "pending", "completed", "cancelled", "no-show" + + patient Patient @relation(fields: [patientId], references: [id]) + appointment Appointment @relation(fields: [appointmentId], references: [id]) + user User? @relation(fields: [userId], references: [id]) + staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id]) + serviceLines ServiceLine[] } model ServiceLine { - id Int @id @default(autoincrement()) - claimId Int - procedureCode String - toothNumber String? - surface String? - quadrant String? - authNumber String? - billedAmount Float - claim Claim @relation(fields: [claimId], references: [id]) + id Int @id @default(autoincrement()) + claimId Int + procedureCode String + procedureDate DateTime @db.Date + oralCavityArea String? + toothNumber String? + toothSurface String? + billedAmount Float + claim Claim @relation(fields: [claimId], references: [id]) }