claim page done
This commit is contained in:
@@ -24,7 +24,11 @@ const PatientSchema = (
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
export default function ClaimsOfPatientModal() {
|
||||
interface ClaimsOfPatientModalProps {
|
||||
onNewClaim?: (patientId: number) => void;
|
||||
}
|
||||
|
||||
export default function ClaimsOfPatientModal({ onNewClaim }: ClaimsOfPatientModalProps) {
|
||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [claimsPage, setClaimsPage] = useState(1);
|
||||
@@ -70,13 +74,18 @@ export default function ClaimsOfPatientModal() {
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>
|
||||
View and manage all patient information
|
||||
Select any patient and View all their recent claims.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
Also create new claim for any patients.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowView
|
||||
allowCheckbox
|
||||
allowNewClaim
|
||||
onNewClaim={onNewClaim}
|
||||
onSelectPatient={handleSelectPatient}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -94,7 +94,6 @@ export default function ClaimsRecentTable({
|
||||
patientId,
|
||||
}: ClaimsRecentTableProps) {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [isViewClaimOpen, setIsViewClaimOpen] = useState(false);
|
||||
const [isEditClaimOpen, setIsEditClaimOpen] = useState(false);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Delete, Edit, Eye } from "lucide-react";
|
||||
import { Delete, Edit, Eye, FileCheck } from "lucide-react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
Pagination,
|
||||
@@ -69,6 +69,8 @@ interface PatientTableProps {
|
||||
allowView?: boolean;
|
||||
allowDelete?: boolean;
|
||||
allowCheckbox?: boolean;
|
||||
allowNewClaim?: boolean;
|
||||
onNewClaim?: (patientId: number) => void;
|
||||
onSelectPatient?: (patient: Patient | null) => void;
|
||||
onPageChange?: (page: number) => void;
|
||||
onSearchChange?: (searchTerm: string) => void;
|
||||
@@ -79,6 +81,8 @@ export function PatientTable({
|
||||
allowView,
|
||||
allowDelete,
|
||||
allowCheckbox,
|
||||
allowNewClaim,
|
||||
onNewClaim,
|
||||
onSelectPatient,
|
||||
onPageChange,
|
||||
onSearchChange,
|
||||
@@ -481,6 +485,17 @@ export function PatientTable({
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{allowNewClaim && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onNewClaim?.(patient.id)}
|
||||
className="text-green-600 hover:text-green-800 hover:bg-green-50"
|
||||
aria-label="New Claim"
|
||||
>
|
||||
<FileCheck className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
{allowView && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -97,39 +97,13 @@ export default function ClaimsPage() {
|
||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||
};
|
||||
|
||||
// Fetch patients
|
||||
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||
Patient[]
|
||||
>({
|
||||
queryKey: ["/api/patients/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/patients/");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Fetch appointments
|
||||
const {
|
||||
data: appointments = [] as Appointment[],
|
||||
isLoading: isLoadingAppointments,
|
||||
} = useQuery<Appointment[]>({
|
||||
queryKey: ["/api/appointments/all"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/appointments/all");
|
||||
return res.json();
|
||||
},
|
||||
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/"] });
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient added successfully!",
|
||||
@@ -158,7 +132,6 @@ export default function ClaimsPage() {
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient updated successfully!",
|
||||
@@ -184,14 +157,11 @@ export default function ClaimsPage() {
|
||||
);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (appointment) => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Appointment created successfully.",
|
||||
title: "Appointment Scheduled",
|
||||
description: appointment.message || "Appointment created successfully.",
|
||||
});
|
||||
// Invalidate both appointments and patients queries
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
@@ -209,6 +179,11 @@ export default function ClaimsPage() {
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["claims-recent"],
|
||||
exact: false,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Claim created successfully",
|
||||
variant: "default",
|
||||
@@ -223,8 +198,17 @@ export default function ClaimsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
// workflow starts from there - this params are set by pdf extraction/patient page.
|
||||
// the fields are either send by pdfExtraction or by selecting patients.
|
||||
// helper
|
||||
const getPatientByInsuranceId = async (insuranceId: string) => {
|
||||
const res = await apiRequest(
|
||||
"GET",
|
||||
`/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(insuranceId)}`
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to fetch patient by insuranceId");
|
||||
return res.json(); // returns patient object or null
|
||||
};
|
||||
|
||||
// workflow starts from there - this params are set by pdf extraction/patient page. then used in claim page here.
|
||||
const [location] = useLocation();
|
||||
const { name, memberId, dob } = useMemo(() => {
|
||||
const search = window.location.search;
|
||||
@@ -279,7 +263,7 @@ export default function ClaimsPage() {
|
||||
};
|
||||
fetchMatchingPatient();
|
||||
}
|
||||
}, [memberId, dob]);
|
||||
}, [memberId]);
|
||||
|
||||
// 1. upsert appointment.
|
||||
const handleAppointmentSubmit = async (
|
||||
@@ -468,53 +452,6 @@ export default function ClaimsPage() {
|
||||
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 (
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||
<Sidebar
|
||||
@@ -546,92 +483,8 @@ export default function ClaimsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New Claims Section */}
|
||||
<div className="mb-8 mt-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div
|
||||
className="flex items-center cursor-pointer group"
|
||||
onClick={() => {
|
||||
if (patientsWithAppointments.length > 0) {
|
||||
const firstPatient = patientsWithAppointments[0];
|
||||
handleNewClaim(Number(firstPatient?.patientId));
|
||||
} else {
|
||||
toast({
|
||||
title: "No patients available",
|
||||
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>
|
||||
<div className="ml-2 text-primary">
|
||||
<FileCheck className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>Recent Patients for Claims</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoadingPatients || isLoadingAppointments ? (
|
||||
<div className="text-center py-4">
|
||||
Loading patients data...
|
||||
</div>
|
||||
) : patientsWithAppointments.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
{patientsWithAppointments.map(
|
||||
(item: (typeof patientsWithAppointments)[number]) => (
|
||||
<div
|
||||
key={item.patientId}
|
||||
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => handleNewClaim(item.patientId)}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-medium">{item.patientName}</h3>
|
||||
<div className="text-sm text-gray-500">
|
||||
<span>
|
||||
Insurance:{" "}
|
||||
{getDisplayProvider(item.insuranceProvider)}
|
||||
</span>
|
||||
|
||||
<span className="mx-2">•</span>
|
||||
<span>ID: {item.insuranceId}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>
|
||||
Last Visit:{" "}
|
||||
{parseLocalDate(
|
||||
item.lastAppointment
|
||||
).toLocaleDateString("en-CA")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-primary">
|
||||
<FileCheck className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Patients with appointments will appear here for insurance
|
||||
claim processing
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Recent Claims by Patients also handles new claims */}
|
||||
<ClaimsOfPatientModal onNewClaim={handleNewClaim} />
|
||||
|
||||
{/* Recent Claims Section */}
|
||||
<Card>
|
||||
@@ -649,9 +502,6 @@ export default function ClaimsPage() {
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Claims by Patients */}
|
||||
<ClaimsOfPatientModal />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user