checkpoint claimpage

This commit is contained in:
2025-07-28 14:40:01 +05:30
parent af1b0f8d70
commit d9ce19df80
3 changed files with 167 additions and 236 deletions

View File

@@ -108,7 +108,6 @@ interface ClaimFormData {
interface ClaimFormProps { interface ClaimFormProps {
patientId: number; patientId: number;
extractedData?: Partial<Patient>;
onSubmit: (data: ClaimFormData) => Promise<Claim>; onSubmit: (data: ClaimFormData) => Promise<Claim>;
onHandleAppointmentSubmit: ( onHandleAppointmentSubmit: (
appointmentData: InsertAppointment | UpdateAppointment appointmentData: InsertAppointment | UpdateAppointment
@@ -123,7 +122,6 @@ type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export function ClaimForm({ export function ClaimForm({
patientId, patientId,
extractedData,
onHandleAppointmentSubmit, onHandleAppointmentSubmit,
onHandleUpdatePatient, onHandleUpdatePatient,
onHandleForMHSelenium, onHandleForMHSelenium,
@@ -133,10 +131,7 @@ export function ClaimForm({
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
// Patient state - initialize from extractedData (if given ) or null (new patient) const [patient, setPatient] = useState<Patient | null>(null);
const [patient, setPatient] = useState<Patient | null>(
extractedData ? ({ ...extractedData } as Patient) : null
);
// Query patient based on given patient id // Query patient based on given patient id
const { const {
@@ -185,14 +180,6 @@ export function ClaimForm({
const [serviceDate, setServiceDate] = useState<string>( const [serviceDate, setServiceDate] = useState<string>(
formatLocalDate(new Date()) formatLocalDate(new Date())
); );
useEffect(() => {
if (extractedData?.serviceDate) {
const parsed = parseLocalDate(extractedData.serviceDate);
const isoFormatted = formatLocalDate(parsed);
setServiceDateValue(parsed);
setServiceDate(isoFormatted);
}
}, [extractedData]);
// Update service date when calendar date changes // Update service date when calendar date changes
const onServiceDateChange = (date: Date | undefined) => { const onServiceDateChange = (date: Date | undefined) => {
@@ -219,7 +206,7 @@ export function ClaimForm({
}); });
}, [serviceDate]); }, [serviceDate]);
// Determine patient date of birth format // Determine patient date of birth format - required as date extracted from pdfs has different format.
const formatDOB = (dob: string | undefined) => { const formatDOB = (dob: string | undefined) => {
if (!dob) return ""; if (!dob) return "";
if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) return dob; // already MM/DD/YYYY if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) return dob; // already MM/DD/YYYY

View File

@@ -31,6 +31,7 @@ import { CalendarIcon } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
const PatientSchema = ( const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -94,7 +95,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
return { return {
...sanitizedPatient, ...sanitizedPatient,
dateOfBirth: patient.dateOfBirth dateOfBirth: patient.dateOfBirth
? new Date(patient.dateOfBirth).toISOString().split("T")[0] ? formatLocalDate(new Date(patient.dateOfBirth))
: "", : "",
}; };
} }
@@ -146,7 +147,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
const resetValues: Partial<Patient> = { const resetValues: Partial<Patient> = {
...sanitizedPatient, ...sanitizedPatient,
dateOfBirth: patient.dateOfBirth dateOfBirth: patient.dateOfBirth
? new Date(patient.dateOfBirth).toISOString().split("T")[0] ? formatLocalDate(new Date(patient.dateOfBirth))
: "", : "",
}; };
form.reset(resetValues); form.reset(resetValues);
@@ -218,7 +219,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
)} )}
> >
{field.value ? ( {field.value ? (
format(field.value, "PPP") format(parseLocalDate(field.value), "PPP")
) : ( ) : (
<span>Pick a date</span> <span>Pick a date</span>
)} )}
@@ -229,10 +230,14 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
<PopoverContent className="w-auto p-4"> <PopoverContent className="w-auto p-4">
<Calendar <Calendar
mode="single" mode="single"
selected={field.value} selected={
field.value
? parseLocalDate(field.value)
: undefined
}
onSelect={(date) => { onSelect={(date) => {
if (date) { if (date) {
const localDate = format(date, "yyyy-MM-dd"); const localDate = formatLocalDate(date); // keep yyyy-MM-dd
field.onChange(localDate); field.onChange(localDate);
} }
}} }}

View File

@@ -91,13 +91,11 @@ export default function ClaimsPage() {
const { status, message, show } = useAppSelector( const { status, message, show } = useAppSelector(
(state) => state.seleniumClaimSubmitTask (state) => state.seleniumClaimSubmitTask
); );
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
const [claimFormData, setClaimFormData] = useState<any>({ const toggleMobileMenu = () => {
patientId: null, setIsMobileMenuOpen(!isMobileMenuOpen);
serviceDate: "", };
});
// Fetch patients // Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery< const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
@@ -204,51 +202,95 @@ export default function ClaimsPage() {
}, },
}); });
// Update appointment mutation // create claim mutation
const updateAppointmentMutation = useMutation({ const createClaimMutation = useMutation({
mutationFn: async ({ mutationFn: async (claimData: any) => {
id, const res = await apiRequest("POST", "/api/claims/", claimData);
appointment, return res.json();
}: {
id: number;
appointment: UpdateAppointment;
}) => {
const res = await apiRequest(
"PUT",
`/api/appointments/${id}`,
appointment
);
return await res.json();
}, },
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: "Success", title: "Claim created successfully",
description: "Appointment updated successfully.", variant: "default",
}); });
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
}, },
onError: (error: Error) => { onError: (error: any) => {
toast({ toast({
title: "Error", title: "Error submitting claim",
description: `Failed to update appointment: ${error.message}`, description: error.message,
variant: "destructive", variant: "destructive",
}); });
}, },
}); });
// workflow starts from there - this params are set by pdf extraction/patient page.
// the fields are either send by pdfExtraction or by selecting patients.
const [location] = useLocation();
const { name, memberId, dob } = useMemo(() => {
const search = window.location.search;
const params = new URLSearchParams(search);
return {
name: params.get("name") || "",
memberId: params.get("memberId") || "",
dob: params.get("dob") || "",
};
}, [location]);
const handleNewClaim = (patientId: number) => {
setSelectedPatientId(patientId);
setIsClaimFormOpen(true);
};
useEffect(() => {
if (memberId && dob) {
// if matching patient found then simply send its id to claim form,
// if not then create new patient then send its id to claim form.
const fetchMatchingPatient = async () => {
try {
const matchingPatient = await getPatientByInsuranceId(memberId);
if (matchingPatient) {
handleNewClaim(matchingPatient.id);
} else {
const [firstName, ...rest] = name.trim().split(" ");
const lastName = rest.join(" ") || "";
const parsedDob = parse(dob, "M/d/yyyy", new Date()); // robust for "4/17/1964", "12/1/1975", etc.
const newPatient: InsertPatient = {
firstName,
lastName,
dateOfBirth: formatLocalDate(parsedDob),
gender: "",
phone: "",
userId: user?.id ?? 1,
status: "active",
insuranceId: memberId,
};
addPatientMutation.mutate(newPatient, {
onSuccess: (created) => {
handleNewClaim(created.id);
},
});
}
} catch (err) {
console.error("Error checking/creating patient:", err);
}
};
fetchMatchingPatient();
}
}, [memberId, dob]);
// 1. upsert appointment.
const handleAppointmentSubmit = async ( const handleAppointmentSubmit = async (
appointmentData: InsertAppointment | UpdateAppointment appointmentData: InsertAppointment | UpdateAppointment
): Promise<number> => { ): Promise<number> => {
return new Promise<number>((resolve, reject) => { return new Promise<number>((resolve, reject) => {
console.log("Constructed appointmentData:", appointmentData);
console.log(appointmentData.date);
createAppointmentMutation.mutate( createAppointmentMutation.mutate(
{ {
date: appointmentData.date, date: appointmentData.date,
startTime: appointmentData.startTime || "09:00", startTime: "09:00",
endTime: appointmentData.endTime || "09:30", endTime: "09:30",
staffId: appointmentData.staffId, staffId: appointmentData.staffId,
patientId: appointmentData.patientId, patientId: appointmentData.patientId,
userId: user?.id, userId: user?.id,
@@ -272,192 +314,15 @@ export default function ClaimsPage() {
}); });
}; };
const createClaimMutation = useMutation({ // 2. Update Patient ( for insuranceId and Insurance Provider)
mutationFn: async (claimData: any) => {
const res = await apiRequest("POST", "/api/claims/", claimData);
return res.json();
},
onSuccess: () => {
toast({
title: "Claim created successfully",
variant: "default",
});
},
onError: (error: any) => {
toast({
title: "Error submitting claim",
description: error.message,
variant: "destructive",
});
},
});
function handleClaimSubmit(claimData: any): Promise<Claim> {
return createClaimMutation.mutateAsync(claimData).then((data) => {
return data;
});
}
const [location] = useLocation(); // gets path, e.g. "/claims"
const { name, memberId, dob } = useMemo(() => {
const search = window.location.search; // this gets the real query string
const params = new URLSearchParams(search);
return {
name: params.get("name") || "",
memberId: params.get("memberId") || "",
dob: params.get("dob") || "",
};
}, [location]);
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const handleNewClaim = (patientId: number) => {
setSelectedPatientId(patientId);
setIsClaimFormOpen(true);
const patient = patients.find((p) => p.id === patientId);
if (!patient) return;
prefillClaimForm(patient);
};
const closeClaim = () => {
setSelectedPatientId(null);
setIsClaimFormOpen(false);
setClaimFormData({
patientId: null,
serviceDate: "",
});
// Remove query parameters without reload
const url = new URL(window.location.href);
url.searchParams.delete("memberId");
url.searchParams.delete("dob");
url.searchParams.delete("name");
// Use history.replaceState to update the URL without reloading
window.history.replaceState({}, document.title, url.toString());
};
const prefillClaimForm = (patient: Patient) => {
const patientAppointments = appointments
.filter((a) => a.patientId === patient.id)
.sort(
(a, b) =>
parseLocalDate(b.date).getTime() - parseLocalDate(a.date).getTime()
);
const lastAppointment = patientAppointments[0]; // most recent
const dateToUse = lastAppointment
? parseLocalDate(lastAppointment.date)
: parseLocalDate(new Date());
setClaimFormData((prev: any) => ({
...prev,
patientId: patient.id,
serviceDate: formatLocalDate(dateToUse),
}));
};
useEffect(() => {
if (memberId && dob) {
const matchingPatient = patients.find(
(p) =>
p.insuranceId?.toLowerCase().trim() === memberId.toLowerCase().trim()
);
if (matchingPatient) {
setSelectedPatientId(matchingPatient.id);
prefillClaimForm(matchingPatient);
setIsClaimFormOpen(true);
} else {
const [firstName, ...rest] = name.trim().split(" ");
const lastName = rest.join(" ") || "";
const parsedDob = parse(dob, "M/d/yyyy", new Date()); // robust for "4/17/1964", "12/1/1975", etc.
const newPatient: InsertPatient = {
firstName,
lastName,
dateOfBirth: formatLocalDate(parsedDob),
gender: "",
phone: "",
userId: user?.id ?? 1,
status: "active",
insuranceId: memberId,
};
addPatientMutation.mutate(newPatient, {
onSuccess: (created) => {
setSelectedPatientId(created.id);
prefillClaimForm(created);
setIsClaimFormOpen(true);
},
});
}
}
}, [memberId, dob]);
const getDisplayProvider = (provider: string) => {
const insuranceMap: Record<string, string> = {
delta: "Delta Dental",
metlife: "MetLife",
cigna: "Cigna",
aetna: "Aetna",
};
return insuranceMap[provider?.toLowerCase()] || provider;
};
// Get unique patients with appointments
const patientsWithAppointments = patients.reduce(
(acc, patient) => {
const patientAppointments = appointments
.filter((appt) => appt.patientId === patient.id)
.sort(
(a, b) =>
parseLocalDate(b.date).getTime() - parseLocalDate(a.date).getTime()
); // Sort descending by date
if (patientAppointments.length > 0) {
const latestAppointment = patientAppointments[0];
acc.push({
patientId: patient.id,
patientName: `${patient.firstName} ${patient.lastName}`,
appointmentId: latestAppointment!.id,
insuranceProvider: patient.insuranceProvider || "N/A",
insuranceId: patient.insuranceId || "N/A",
lastAppointment: formatLocalDate(
parseLocalDate(latestAppointment!.date)
),
});
}
return acc;
},
[] as Array<{
patientId: number;
patientName: string;
appointmentId: number;
insuranceProvider: string;
insuranceId: string;
lastAppointment: string;
}>
);
// Update Patient ( for insuranceId and Insurance Provider)
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => { const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
if (patient && user) { if (patient) {
const { id, ...sanitizedPatient } = patient; const { id, ...sanitizedPatient } = patient;
updatePatientMutation.mutate({ updatePatientMutation.mutate({
id: Number(patient.id), id: Number(patient.id),
patient: sanitizedPatient, patient: sanitizedPatient,
}); });
} else { } else {
console.error("No current patient or user found for update");
toast({ toast({
title: "Error", title: "Error",
description: "Cannot update patient: No patient or user found", description: "Cannot update patient: No patient or user found",
@@ -466,7 +331,14 @@ export default function ClaimsPage() {
} }
}; };
// handle selenium sybmiting Mass Health claim // 3. create claim.
const handleClaimSubmit = (claimData: any): Promise<Claim> => {
return createClaimMutation.mutateAsync(claimData).then((data) => {
return data;
});
};
// 4. handle selenium sybmiting Mass Health claim
const handleMHClaimSubmitSelenium = async (data: any) => { const handleMHClaimSubmitSelenium = async (data: any) => {
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(data)); formData.append("data", JSON.stringify(data));
@@ -509,7 +381,10 @@ export default function ClaimsPage() {
variant: "default", variant: "default",
}); });
const result2 = await handleMHSeleniumPdfDownload(result1); const result2 = await handleMHSeleniumPdfDownload(
result1,
selectedPatientId
);
return result2; return result2;
} catch (error: any) { } catch (error: any) {
dispatch( dispatch(
@@ -526,8 +401,11 @@ export default function ClaimsPage() {
} }
}; };
// selenium pdf download handler // 5. selenium pdf download handler
const handleMHSeleniumPdfDownload = async (data: any) => { const handleMHSeleniumPdfDownload = async (
data: any,
selectedPatientId: number | null
) => {
try { try {
if (!selectedPatientId) { if (!selectedPatientId) {
throw new Error("Missing patientId"); throw new Error("Missing patientId");
@@ -575,6 +453,68 @@ export default function ClaimsPage() {
} }
}; };
// 6. close claim
const closeClaim = () => {
setSelectedPatientId(null);
setIsClaimFormOpen(false);
// Remove query parameters without reload
const url = new URL(window.location.href);
url.searchParams.delete("memberId");
url.searchParams.delete("dob");
url.searchParams.delete("name");
// Use history.replaceState to update the URL without reloading
window.history.replaceState({}, document.title, url.toString());
};
// helper func for frontend
const getDisplayProvider = (provider: string) => {
const insuranceMap: Record<string, string> = {
delta: "Delta Dental",
metlife: "MetLife",
cigna: "Cigna",
aetna: "Aetna",
};
return insuranceMap[provider?.toLowerCase()] || provider;
};
// Get unique patients with appointments - might not needed now, can shift this to the recent patients table
const patientsWithAppointments = patients.reduce(
(acc, patient) => {
const patientAppointments = appointments
.filter((appt) => appt.patientId === patient.id)
.sort(
(a, b) =>
parseLocalDate(b.date).getTime() - parseLocalDate(a.date).getTime()
); // Sort descending by date
if (patientAppointments.length > 0) {
const latestAppointment = patientAppointments[0];
acc.push({
patientId: patient.id,
patientName: `${patient.firstName} ${patient.lastName}`,
appointmentId: latestAppointment!.id,
insuranceProvider: patient.insuranceProvider || "N/A",
insuranceId: patient.insuranceId || "N/A",
lastAppointment: formatLocalDate(
parseLocalDate(latestAppointment!.date)
),
});
}
return acc;
},
[] as Array<{
patientId: number;
patientName: string;
appointmentId: number;
insuranceProvider: string;
insuranceId: string;
lastAppointment: string;
}>
);
return ( return (
<div className="flex h-screen overflow-hidden bg-gray-100"> <div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar <Sidebar
@@ -720,7 +660,6 @@ export default function ClaimsPage() {
<ClaimForm <ClaimForm
patientId={selectedPatientId} patientId={selectedPatientId}
onClose={closeClaim} onClose={closeClaim}
extractedData={claimFormData}
onSubmit={handleClaimSubmit} onSubmit={handleClaimSubmit}
onHandleAppointmentSubmit={handleAppointmentSubmit} onHandleAppointmentSubmit={handleAppointmentSubmit}
onHandleUpdatePatient={handleUpdatePatient} onHandleUpdatePatient={handleUpdatePatient}