claim page , auto fill field working
This commit is contained in:
@@ -14,92 +14,132 @@ import { Label } from "@/components/ui/label";
|
||||
import { X, Calendar as CalendarIcon } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||
import { z } from "zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
interface ServiceLine {
|
||||
procedureCode: string;
|
||||
toothNumber: string;
|
||||
surface: string;
|
||||
quad: string;
|
||||
authNo: string;
|
||||
billedAmount: string;
|
||||
}
|
||||
|
||||
interface ClaimFormProps {
|
||||
patientId: number;
|
||||
appointmentId: number;
|
||||
patientName: string;
|
||||
patientId?: number;
|
||||
extractedData?: Partial<Patient>;
|
||||
onSubmit: (claimData: any) => void;
|
||||
onClose: () => void;
|
||||
patientData?: Patient;
|
||||
}
|
||||
|
||||
export function ClaimForm({
|
||||
patientId,
|
||||
appointmentId,
|
||||
patientName,
|
||||
extractedData,
|
||||
onSubmit,
|
||||
onClose,
|
||||
patientData
|
||||
}: ClaimFormProps) {
|
||||
const { toast } = useToast();
|
||||
const [patient, setPatient] = useState<Patient | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
|
||||
const [serviceDate, setServiceDate] = useState<string>(format(new Date(), 'MM/dd/yy'));
|
||||
const [clinicalNotes, setClinicalNotes] = useState<string>('');
|
||||
|
||||
// Fetch patient data if not provided
|
||||
useEffect(() => {
|
||||
if (patientData) {
|
||||
setPatient(patientData);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchPatient = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/patients/${patientId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch patient data");
|
||||
}
|
||||
const data = await response.json();
|
||||
setPatient(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching patient:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load patient information",
|
||||
variant: "destructive",
|
||||
// Query patient if patientId provided
|
||||
const { data: fetchedPatient, isLoading, error } = useQuery<Patient>({
|
||||
queryKey: ["/api/patients/", patientId],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", `/api/patients/${patientId}`);
|
||||
if (!res.ok) throw new Error("Failed to fetch patient");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!patientId,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
// Patient state - initialize from extractedData or null (new patient)
|
||||
const [patient, setPatient] = useState<Patient | null>(
|
||||
extractedData ? ({ ...extractedData } as Patient) : null
|
||||
);
|
||||
|
||||
// Sync fetched patient when available
|
||||
useEffect(() => {
|
||||
if (fetchedPatient) {
|
||||
setPatient(fetchedPatient);
|
||||
}
|
||||
}, [fetchedPatient]);
|
||||
|
||||
|
||||
// Service date state
|
||||
const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
|
||||
const [serviceDate, setServiceDate] = useState<string>(format(new Date(), "MM/dd/yy"));
|
||||
|
||||
// Clinical notes state
|
||||
const [clinicalNotes, setClinicalNotes] = useState<string>("");
|
||||
|
||||
// Doctor selection state
|
||||
const [doctor, setDoctor] = useState("doctor1");
|
||||
|
||||
// Service lines state with one empty default line
|
||||
const [serviceLines, setServiceLines] = useState<ServiceLine[]>([
|
||||
{
|
||||
procedureCode: "",
|
||||
toothNumber: "",
|
||||
surface: "",
|
||||
quad: "",
|
||||
authNo: "",
|
||||
billedAmount: "",
|
||||
},
|
||||
]);
|
||||
|
||||
// Update a field in serviceLines at index
|
||||
const updateServiceLine = (
|
||||
index: number,
|
||||
field: keyof ServiceLine,
|
||||
value: string
|
||||
) => {
|
||||
setServiceLines((prev) => {
|
||||
const updated = [...prev];
|
||||
if (updated[index]) {
|
||||
updated[index][field] = value;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
if (patientId) {
|
||||
fetchPatient();
|
||||
}
|
||||
}, [patientId, patientData, toast]);
|
||||
|
||||
// Handle patient field changes (to make inputs controlled and editable)
|
||||
const updatePatientField = (field: keyof Patient, value: any) => {
|
||||
setPatient((prev) => (prev ? { ...prev, [field]: value } : null));
|
||||
};
|
||||
|
||||
|
||||
// Update service date when calendar date changes
|
||||
const onServiceDateChange = (date: Date | undefined) => {
|
||||
if (date) {
|
||||
setServiceDateValue(date);
|
||||
setServiceDate(format(date, 'MM/dd/yy'));
|
||||
setServiceDate(format(date, "MM/dd/yy"));
|
||||
}
|
||||
};
|
||||
|
||||
// Determine patient date of birth format
|
||||
const formatDOB = (dob: string | undefined) => {
|
||||
if (!dob) return '';
|
||||
if (!dob) return "";
|
||||
|
||||
// If already in MM/DD/YYYY format, return as is
|
||||
if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) {
|
||||
return dob;
|
||||
}
|
||||
if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) return dob; // already MM/DD/YYYY
|
||||
|
||||
// If in YYYY-MM-DD format, convert to MM/DD/YYYY
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dob)) {
|
||||
const [year, month, day] = dob.split('-');
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -110,7 +150,9 @@ export function ClaimForm({
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
||||
<Card className="w-full max-w-5xl max-h-[90vh] overflow-y-auto bg-white">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b">
|
||||
<CardTitle className="text-xl font-bold">Insurance Claim Form</CardTitle>
|
||||
<CardTitle className="text-xl font-bold">
|
||||
Insurance Claim Form
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -123,8 +165,9 @@ export function ClaimForm({
|
||||
<Label htmlFor="memberId">Member ID</Label>
|
||||
<Input
|
||||
id="memberId"
|
||||
value={patient?.insuranceId || ''}
|
||||
disabled={loading}
|
||||
value={patient?.insuranceId || ""}
|
||||
onChange={(e) => updatePatientField("insuranceId", e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -132,32 +175,35 @@ export function ClaimForm({
|
||||
<Input
|
||||
id="dateOfBirth"
|
||||
value={formatDOB(patient?.dateOfBirth)}
|
||||
disabled={loading}
|
||||
onChange={(e) => updatePatientField("dateOfBirth", e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={patient?.firstName || ''}
|
||||
disabled={loading}
|
||||
value={patient?.firstName || ""}
|
||||
onChange={(e) => updatePatientField("firstName", e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={patient?.lastName || ''}
|
||||
disabled={loading}
|
||||
value={patient?.lastName || ""}
|
||||
onChange={(e) => updatePatientField("lastName", e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Clinical Notes Entry */}
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Label htmlFor="clinicalNotes" className="whitespace-nowrap">Clinical Notes:</Label>
|
||||
<Label htmlFor="clinicalNotes" className="whitespace-nowrap">
|
||||
Clinical Notes:
|
||||
</Label>
|
||||
<Input
|
||||
id="clinicalNotes"
|
||||
className="flex-grow"
|
||||
@@ -180,7 +226,7 @@ export function ClaimForm({
|
||||
toast({
|
||||
title: "Empty Input",
|
||||
description: "Please enter clinical notes to extract",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -191,7 +237,9 @@ export function ClaimForm({
|
||||
|
||||
{/* Service Lines */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2 text-center">Service Lines</h3>
|
||||
<h3 className="text-xl font-semibold mb-2 text-center">
|
||||
Service Lines
|
||||
</h3>
|
||||
<div className="flex justify-end items-center mb-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="flex items-center">Service Date</Label>
|
||||
@@ -214,8 +262,10 @@ export function ClaimForm({
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Label className="flex items-center ml-2">Treating Doctor</Label>
|
||||
<Select defaultValue="doctor1">
|
||||
<Label className="flex items-center ml-2">
|
||||
Treating Doctor
|
||||
</Label>
|
||||
<Select value={doctor} onValueChange={setDoctor}>
|
||||
<SelectTrigger className="w-36">
|
||||
<SelectValue placeholder="Select Doctor" />
|
||||
</SelectTrigger>
|
||||
@@ -227,50 +277,45 @@ export function ClaimForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-6 gap-4 mb-2">
|
||||
<div>
|
||||
<Label htmlFor="procedureCode1">Procedure Code</Label>
|
||||
<Input id="procedureCode1" placeholder="e.g. D0120" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="toothNumber1">Tooth Number</Label>
|
||||
<Input id="toothNumber1" placeholder="e.g. 14" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="surface1">Surface</Label>
|
||||
<Input id="surface1" placeholder="e.g. MOD" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="quad1">Quad</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="UR">Upper Right</SelectItem>
|
||||
<SelectItem value="UL">Upper Left</SelectItem>
|
||||
<SelectItem value="LR">Lower Right</SelectItem>
|
||||
<SelectItem value="LL">Lower Left</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="authNo1">Auth No.</Label>
|
||||
<Input id="authNo1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="billedAmount1">Billed Amount</Label>
|
||||
<Input id="billedAmount1" placeholder="$0.00" />
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4 mb-2 font-medium text-sm text-gray-700">
|
||||
<span>Procedure Code</span>
|
||||
<span>Tooth Number</span>
|
||||
<span>Surface</span>
|
||||
<span>Quadrant</span>
|
||||
<span>Auth No.</span>
|
||||
<span>Billed Amount</span>
|
||||
</div>
|
||||
|
||||
{/* Add more service lines - simplified for clarity */}
|
||||
{[2, 3, 4, 5].map(i => (
|
||||
{/* Dynamic Rows */}
|
||||
{serviceLines.map((line, i) => (
|
||||
<div key={i} className="grid grid-cols-6 gap-4 mb-2">
|
||||
<Input placeholder="Procedure Code" />
|
||||
<Input placeholder="Tooth Number" />
|
||||
<Input placeholder="Surface" />
|
||||
<Select>
|
||||
<Input
|
||||
placeholder="e.g. D0120"
|
||||
value={line.procedureCode}
|
||||
onChange={(e) =>
|
||||
updateServiceLine(i, "procedureCode", e.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="e.g. 14"
|
||||
value={line.toothNumber}
|
||||
onChange={(e) =>
|
||||
updateServiceLine(i, "toothNumber", e.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="e.g. MOD"
|
||||
value={line.surface}
|
||||
onChange={(e) =>
|
||||
updateServiceLine(i, "surface", e.target.value)
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={line.quad}
|
||||
onValueChange={(value) =>
|
||||
updateServiceLine(i, "quad", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
@@ -281,10 +326,40 @@ export function ClaimForm({
|
||||
<SelectItem value="LL">Lower Left</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Auth No." />
|
||||
<Input placeholder="$0.00" />
|
||||
<Input
|
||||
placeholder="e.g. 123456"
|
||||
value={line.authNo}
|
||||
onChange={(e) =>
|
||||
updateServiceLine(i, "authNo", e.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="$0.00"
|
||||
value={line.billedAmount}
|
||||
onChange={(e) =>
|
||||
updateServiceLine(i, "billedAmount", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setServiceLines([
|
||||
...serviceLines,
|
||||
{
|
||||
procedureCode: "",
|
||||
toothNumber: "",
|
||||
surface: "",
|
||||
quad: "",
|
||||
authNo: "",
|
||||
billedAmount: "",
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
+ Add Service Line
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button variant="outline">Child Prophy Codes</Button>
|
||||
@@ -295,7 +370,10 @@ export function ClaimForm({
|
||||
|
||||
{/* File Upload Section */}
|
||||
<div className="mt-4 bg-gray-100 p-3 rounded-md">
|
||||
<p className="text-sm text-gray-500 mb-2">Please note that file types with 4 or more character extensions are not allowed, such as .DOCX, .PPTX, or .XLSX</p>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Please note that file types with 4 or more character
|
||||
extensions are not allowed, such as .DOCX, .PPTX, or .XLSX
|
||||
</p>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Select Field:</Label>
|
||||
@@ -304,7 +382,9 @@ export function ClaimForm({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="supportData">Support Data for Claim</SelectItem>
|
||||
<SelectItem value="supportData">
|
||||
Support Data for Claim
|
||||
</SelectItem>
|
||||
<SelectItem value="xrays">X-Ray Images</SelectItem>
|
||||
<SelectItem value="photos">Clinical Photos</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -317,15 +397,21 @@ export function ClaimForm({
|
||||
|
||||
{/* Insurance Carriers */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4 text-center">Insurance Carriers</h3>
|
||||
<h3 className="text-xl font-semibold mb-4 text-center">
|
||||
Insurance Carriers
|
||||
</h3>
|
||||
<div className="flex justify-between">
|
||||
<Button className="w-32" variant="outline">Delta MA</Button>
|
||||
<Button className="w-32" variant="outline">MH</Button>
|
||||
<Button className="w-32" variant="outline">Others</Button>
|
||||
<Button className="w-32" variant="outline">
|
||||
Delta MA
|
||||
</Button>
|
||||
<Button className="w-32" variant="outline">
|
||||
MH
|
||||
</Button>
|
||||
<Button className="w-32" variant="outline">
|
||||
Others
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { ClaimForm } from "@/components/claims/claim-form";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||
import { Plus, FileCheck, CheckCircle, Clock, AlertCircle } from "lucide-react";
|
||||
import {
|
||||
PatientUncheckedCreateInputObjectSchema,
|
||||
AppointmentUncheckedCreateInputObjectSchema,
|
||||
} from "@repo/db/usedSchemas";
|
||||
import { FileCheck, CheckCircle, Clock, AlertCircle } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { z } from "zod";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
//creating types out of schema auto generated.
|
||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||
@@ -60,15 +64,33 @@ const updatePatientSchema = (
|
||||
|
||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||
|
||||
function getQueryParams() {
|
||||
const search = window.location.search;
|
||||
const params = new URLSearchParams(search);
|
||||
return {
|
||||
name: params.get("name") || "",
|
||||
memberId: params.get("memberId") || "",
|
||||
dob: params.get("dob") || "",
|
||||
};
|
||||
}
|
||||
|
||||
export default function ClaimsPage() {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
||||
const [selectedPatient, setSelectedPatient] = useState<number | null>(null);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<number | null>(null);
|
||||
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const [claimFormData, setClaimFormData] = useState<any>({
|
||||
patientId: null,
|
||||
carrier: "",
|
||||
doctorName: "",
|
||||
serviceDate: "",
|
||||
clinicalNotes: "",
|
||||
serviceLines: [],
|
||||
});
|
||||
|
||||
// Fetch patients
|
||||
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||
@@ -95,6 +117,75 @@ export default function ClaimsPage() {
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Add patient mutation
|
||||
const addPatientMutation = useMutation({
|
||||
mutationFn: async (patient: InsertPatient) => {
|
||||
const res = await apiRequest("POST", "/api/patients/", patient);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: (newPatient) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient added successfully!",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to add patient: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Create appointment mutation
|
||||
const createAppointmentMutation = useMutation({
|
||||
mutationFn: async (appointment: InsertAppointment) => {
|
||||
const res = await apiRequest("POST", "/api/appointments/", appointment);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Appointment created successfully.",
|
||||
});
|
||||
// Invalidate both appointments and patients queries
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to create appointment: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const createClaimMutation = useMutation({
|
||||
mutationFn: async (claimData: any) => {
|
||||
const res = await apiRequest("POST", "/api/claims/", claimData);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Claim submitted successfully",
|
||||
variant: "default",
|
||||
});
|
||||
closeClaim();
|
||||
// optionally refetch claims or appointments if needed
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: "Error submitting claim",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||
};
|
||||
@@ -111,34 +202,94 @@ export default function ClaimsPage() {
|
||||
setSelectedAppointment(null);
|
||||
};
|
||||
|
||||
const { name, memberId, dob } = getQueryParams();
|
||||
const prefillClaimForm = (patient: Patient) => {
|
||||
setClaimFormData((prev: any) => ({
|
||||
...prev,
|
||||
patientId: patient.id,
|
||||
carrier: patient.insuranceProvider || "",
|
||||
doctorName: user?.username || "",
|
||||
serviceDate: new Date().toISOString().slice(0, 10),
|
||||
clinicalNotes: "",
|
||||
serviceLines: [],
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (memberId && dob) {
|
||||
const matchingPatient = patients.find(
|
||||
(p) =>
|
||||
p.insuranceId?.toLowerCase().trim() === memberId.toLowerCase().trim()
|
||||
);
|
||||
|
||||
if (matchingPatient) {
|
||||
setSelectedPatient(matchingPatient.id);
|
||||
prefillClaimForm(matchingPatient);
|
||||
setIsClaimFormOpen(true);
|
||||
} else {
|
||||
const [firstName, ...rest] = name.trim().split(" ");
|
||||
const lastName = rest.join(" ") || "";
|
||||
|
||||
const newPatient: InsertPatient = {
|
||||
firstName,
|
||||
lastName,
|
||||
dateOfBirth: new Date(dob),
|
||||
gender: "unknown",
|
||||
phone: "000-000-0000",
|
||||
userId: user?.id ?? 1,
|
||||
insuranceId: memberId,
|
||||
};
|
||||
|
||||
addPatientMutation.mutate(newPatient, {
|
||||
onSuccess: (created) => {
|
||||
setSelectedPatient(created.id);
|
||||
prefillClaimForm(created);
|
||||
setIsClaimFormOpen(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [memberId, dob, patients]);
|
||||
|
||||
function handleClaimSubmit(claimData: any) {
|
||||
createClaimMutation.mutate(claimData);
|
||||
}
|
||||
|
||||
// Get unique patients with appointments
|
||||
const patientsWithAppointments = appointments.reduce((acc, appointment) => {
|
||||
if (!acc.some(item => item.patientId === appointment.patientId)) {
|
||||
const patient = patients.find(p => p.id === appointment.patientId);
|
||||
const patientsWithAppointments = appointments.reduce(
|
||||
(acc, appointment) => {
|
||||
if (!acc.some((item) => item.patientId === appointment.patientId)) {
|
||||
const patient = patients.find((p) => p.id === appointment.patientId);
|
||||
if (patient) {
|
||||
acc.push({
|
||||
patientId: patient.id,
|
||||
patientName: `${patient.firstName} ${patient.lastName}`,
|
||||
appointmentId: Number(appointment.id),
|
||||
insuranceProvider: patient.insuranceProvider || 'N/A',
|
||||
insuranceId: patient.insuranceId || 'N/A',
|
||||
lastAppointment: String(appointment.date)
|
||||
insuranceProvider: patient.insuranceProvider || "N/A",
|
||||
insuranceId: patient.insuranceId || "N/A",
|
||||
lastAppointment: String(appointment.date),
|
||||
});
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<{
|
||||
},
|
||||
[] as Array<{
|
||||
patientId: number;
|
||||
patientName: string;
|
||||
appointmentId: number;
|
||||
insuranceProvider: string;
|
||||
insuranceId: string;
|
||||
lastAppointment: string;
|
||||
}>);
|
||||
}>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
||||
<Sidebar
|
||||
isMobileOpen={isMobileMenuOpen}
|
||||
setIsMobileOpen={setIsMobileMenuOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||
@@ -146,8 +297,12 @@ export default function ClaimsPage() {
|
||||
<main className="flex-1 overflow-y-auto p-4">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-gray-800">Insurance Claims</h1>
|
||||
<p className="text-gray-600">Manage and submit insurance claims for patients</p>
|
||||
<h1 className="text-2xl font-semibold text-gray-800">
|
||||
Insurance Claims
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Manage and submit insurance claims for patients
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* New Claims Section */}
|
||||
@@ -158,16 +313,22 @@ export default function ClaimsPage() {
|
||||
onClick={() => {
|
||||
if (patientsWithAppointments.length > 0) {
|
||||
const firstPatient = patientsWithAppointments[0];
|
||||
handleNewClaim(Number(firstPatient?.patientId), Number(firstPatient?.appointmentId));
|
||||
handleNewClaim(
|
||||
Number(firstPatient?.patientId),
|
||||
Number(firstPatient?.appointmentId)
|
||||
);
|
||||
} else {
|
||||
toast({
|
||||
title: "No patients available",
|
||||
description: "There are no patients with appointments to create a claim",
|
||||
description:
|
||||
"There are no patients with appointments to create a claim",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<h2 className="text-xl font-medium text-gray-800 group-hover:text-primary">New Claims</h2>
|
||||
<h2 className="text-xl font-medium text-gray-800 group-hover:text-primary">
|
||||
New Claims
|
||||
</h2>
|
||||
<div className="ml-2 text-primary">
|
||||
<FileCheck className="h-5 w-5" />
|
||||
</div>
|
||||
@@ -180,31 +341,43 @@ export default function ClaimsPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoadingPatients || isLoadingAppointments ? (
|
||||
<div className="text-center py-4">Loading patients data...</div>
|
||||
<div className="text-center py-4">
|
||||
Loading patients data...
|
||||
</div>
|
||||
) : patientsWithAppointments.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
{patientsWithAppointments.map((item) => (
|
||||
<div
|
||||
key={item.patientId}
|
||||
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => handleNewClaim(item.patientId, item.appointmentId)}
|
||||
onClick={() =>
|
||||
handleNewClaim(item.patientId, item.appointmentId)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-medium">{item.patientName}</h3>
|
||||
<div className="text-sm text-gray-500">
|
||||
<span>Insurance: {item.insuranceProvider === 'delta'
|
||||
? 'Delta Dental'
|
||||
: item.insuranceProvider === 'metlife'
|
||||
? 'MetLife'
|
||||
: item.insuranceProvider === 'cigna'
|
||||
? 'Cigna'
|
||||
: item.insuranceProvider === 'aetna'
|
||||
? 'Aetna'
|
||||
: item.insuranceProvider}</span>
|
||||
<span>
|
||||
Insurance:{" "}
|
||||
{item.insuranceProvider === "delta"
|
||||
? "Delta Dental"
|
||||
: item.insuranceProvider === "metlife"
|
||||
? "MetLife"
|
||||
: item.insuranceProvider === "cigna"
|
||||
? "Cigna"
|
||||
: item.insuranceProvider === "aetna"
|
||||
? "Aetna"
|
||||
: item.insuranceProvider}
|
||||
</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>ID: {item.insuranceId}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>Last Visit: {new Date(item.lastAppointment).toLocaleDateString()}</span>
|
||||
<span>
|
||||
Last Visit:{" "}
|
||||
{new Date(
|
||||
item.lastAppointment
|
||||
).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-primary">
|
||||
@@ -216,98 +389,28 @@ export default function ClaimsPage() {
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<FileCheck className="h-12 w-12 mx-auto text-gray-400 mb-3" />
|
||||
<h3 className="text-lg font-medium">No eligible patients for claims</h3>
|
||||
<h3 className="text-lg font-medium">
|
||||
No eligible patients for claims
|
||||
</h3>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Patients with appointments will appear here for insurance claim processing
|
||||
Patients with appointments will appear here for insurance
|
||||
claim processing
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Old Claims Section */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-medium text-gray-800">Old Claims</h2>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>Submitted Claims History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Sample Old Claims */}
|
||||
<div className="divide-y">
|
||||
{patientsWithAppointments.slice(0, 3).map((item, index) => (
|
||||
<div
|
||||
key={`old-claim-${index}`}
|
||||
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => toast({
|
||||
title: "Claim Details",
|
||||
description: `Viewing details for claim #${2000 + index}`
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-medium">{item.patientName}</h3>
|
||||
<div className="text-sm text-gray-500">
|
||||
<span>Claim #: {2000 + index}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>Submitted: {format(new Date(new Date().setDate(new Date().getDate() - (index * 15))), 'MMM dd, yyyy')}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>Amount: ${(Math.floor(Math.random() * 500) + 100).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
index === 0 ? 'bg-yellow-100 text-yellow-800' :
|
||||
index === 1 ? 'bg-green-100 text-green-800' :
|
||||
'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{index === 0 ? (
|
||||
<span className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Pending
|
||||
</span>
|
||||
) : index === 1 ? (
|
||||
<span className="flex items-center">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Approved
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center">
|
||||
<AlertCircle className="h-3 w-3 mr-1" />
|
||||
Review
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{patientsWithAppointments.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<Clock className="h-12 w-12 mx-auto text-gray-400 mb-3" />
|
||||
<h3 className="text-lg font-medium">No claim history</h3>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Submitted insurance claims will appear here
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Claim Form Modal */}
|
||||
{isClaimFormOpen && selectedPatient !== null && selectedAppointment !== null && (
|
||||
{isClaimFormOpen && selectedPatient !== null && (
|
||||
<ClaimForm
|
||||
patientId={selectedPatient}
|
||||
appointmentId={selectedAppointment}
|
||||
patientName="" // Will be loaded by the component
|
||||
onClose={closeClaim}
|
||||
extractedData={claimFormData}
|
||||
onSubmit={handleClaimSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { format, parse, parseISO } from "date-fns";
|
||||
import { format, parse, isValid, parseISO } from "date-fns";
|
||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { StatCard } from "@/components/ui/stat-card";
|
||||
@@ -14,8 +14,8 @@ import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { AppointmentsByDay } from "@/components/analytics/appointments-by-day";
|
||||
import { NewPatients } from "@/components/analytics/new-patients";
|
||||
import { AppointmentUncheckedCreateInputObjectSchema } from '@repo/db/usedSchemas';
|
||||
import { PatientUncheckedCreateInputObjectSchema } from '@repo/db/usedSchemas';
|
||||
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||
|
||||
import {
|
||||
Users,
|
||||
@@ -630,15 +630,26 @@ export default function Dashboard() {
|
||||
</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Date of Birth:</span>{" "}
|
||||
<span className="text-gray-500">Date of Birth:</span>{" "}
|
||||
{format(
|
||||
parse(
|
||||
currentPatient.dateOfBirth,
|
||||
"yyyy-MM-dd",
|
||||
new Date()
|
||||
),
|
||||
"PPP"
|
||||
{currentPatient.dateOfBirth ? (
|
||||
(() => {
|
||||
const dobDate = parseISO(currentPatient.dateOfBirth);
|
||||
return isValid(dobDate) ? (
|
||||
<span>
|
||||
<span className="text-gray-500">
|
||||
Date of Birth:
|
||||
</span>{" "}
|
||||
{format(dobDate, "PPP")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-500">
|
||||
Date of Birth: N/A
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<span className="text-gray-500">
|
||||
Date of Birth: N/A
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import useExtractPdfData from "@/hooks/use-extractPdfData";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
@@ -86,9 +87,13 @@ export default function PatientsPage() {
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isExtracting, setIsExtracting] = useState(false);
|
||||
const [formData, setFormData] = useState({ PatientName: "", PatientMemberId: "", PatientDob:"" });
|
||||
const [formData, setFormData] = useState({
|
||||
PatientName: "",
|
||||
PatientMemberId: "",
|
||||
PatientDob: "",
|
||||
});
|
||||
const { mutate: extractPdf } = useExtractPdfData();
|
||||
|
||||
const [location, navigate] = useLocation();
|
||||
|
||||
// Fetch patients
|
||||
const {
|
||||
@@ -244,7 +249,10 @@ export default function PatientsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isLoadingPatients || addPatientMutation.isPending || updatePatientMutation.isPending;
|
||||
const isLoading =
|
||||
isLoadingPatients ||
|
||||
addPatientMutation.isPending ||
|
||||
updatePatientMutation.isPending;
|
||||
|
||||
// Search handling
|
||||
const handleSearch = (criteria: SearchCriteria) => {
|
||||
@@ -291,7 +299,6 @@ export default function PatientsPage() {
|
||||
});
|
||||
}, [patients, searchCriteria]);
|
||||
|
||||
|
||||
// File upload handling
|
||||
const handleFileUpload = (file: File) => {
|
||||
setIsUploading(true);
|
||||
@@ -327,10 +334,17 @@ export default function PatientsPage() {
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setFormData({ PatientName: data.name || "", PatientMemberId: data.memberId || "", PatientDob: data.dob || ""});
|
||||
},
|
||||
const params = new URLSearchParams({
|
||||
name: data.name,
|
||||
memberId: data.memberId,
|
||||
dob: data.dob,
|
||||
});
|
||||
|
||||
navigate(
|
||||
`/claims?name=${encodeURIComponent(data.name)}&memberId=${data.memberId}&dob=${data.dob}`
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -462,7 +476,10 @@ export default function PatientsPage() {
|
||||
</Card>
|
||||
|
||||
{/* View Patient Modal */}
|
||||
<Dialog open={isViewPatientOpen} onOpenChange={setIsViewPatientOpen}>
|
||||
<Dialog
|
||||
open={isViewPatientOpen}
|
||||
onOpenChange={setIsViewPatientOpen}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Patient Details</DialogTitle>
|
||||
@@ -483,7 +500,8 @@ export default function PatientsPage() {
|
||||
{currentPatient.firstName} {currentPatient.lastName}
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
Patient ID: {currentPatient.id.toString().padStart(4, "0")}
|
||||
Patient ID:{" "}
|
||||
{currentPatient.id.toString().padStart(4, "0")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -495,7 +513,9 @@ export default function PatientsPage() {
|
||||
</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Date of Birth:</span>{" "}
|
||||
<span className="text-gray-500">
|
||||
Date of Birth:
|
||||
</span>{" "}
|
||||
{new Date(
|
||||
currentPatient.dateOfBirth
|
||||
).toLocaleDateString()}
|
||||
@@ -539,7 +559,8 @@ export default function PatientsPage() {
|
||||
{currentPatient.address ? (
|
||||
<>
|
||||
{currentPatient.address}
|
||||
{currentPatient.city && `, ${currentPatient.city}`}
|
||||
{currentPatient.city &&
|
||||
`, ${currentPatient.city}`}
|
||||
{currentPatient.zipCode &&
|
||||
` ${currentPatient.zipCode}`}
|
||||
</>
|
||||
@@ -562,7 +583,8 @@ export default function PatientsPage() {
|
||||
? "MetLife"
|
||||
: currentPatient.insuranceProvider === "cigna"
|
||||
? "Cigna"
|
||||
: currentPatient.insuranceProvider === "aetna"
|
||||
: currentPatient.insuranceProvider ===
|
||||
"aetna"
|
||||
? "Aetna"
|
||||
: currentPatient.insuranceProvider
|
||||
: "N/A"}
|
||||
@@ -576,7 +598,9 @@ export default function PatientsPage() {
|
||||
{currentPatient.groupNumber || "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Policy Holder:</span>{" "}
|
||||
<span className="text-gray-500">
|
||||
Policy Holder:
|
||||
</span>{" "}
|
||||
{currentPatient.policyHolder || "Self"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -592,8 +616,11 @@ export default function PatientsPage() {
|
||||
{currentPatient.allergies || "None reported"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Medical Conditions:</span>{" "}
|
||||
{currentPatient.medicalConditions || "None reported"}
|
||||
<span className="text-gray-500">
|
||||
Medical Conditions:
|
||||
</span>{" "}
|
||||
{currentPatient.medicalConditions ||
|
||||
"None reported"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -620,7 +647,6 @@ export default function PatientsPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
{/* Add/Edit Patient Modal */}
|
||||
<AddPatientModal
|
||||
ref={addPatientModalRef}
|
||||
|
||||
@@ -25,6 +25,7 @@ model User {
|
||||
password String
|
||||
patients Patient[]
|
||||
appointments Appointment[]
|
||||
claims Claim[]
|
||||
}
|
||||
|
||||
model Patient {
|
||||
@@ -49,6 +50,7 @@ model Patient {
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
appointments Appointment[]
|
||||
claims Claim[]
|
||||
}
|
||||
|
||||
model Appointment {
|
||||
@@ -68,6 +70,7 @@ model Appointment {
|
||||
patient Patient @relation(fields: [patientId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
staff Staff? @relation(fields: [staffId], references: [id])
|
||||
claims Claim[]
|
||||
}
|
||||
|
||||
model Staff {
|
||||
@@ -79,3 +82,32 @@ model Staff {
|
||||
createdAt DateTime @default(now())
|
||||
appointments Appointment[]
|
||||
}
|
||||
|
||||
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?
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user