claim page table logic done, ui to be fixed
This commit is contained in:
@@ -202,6 +202,39 @@ router.get("/recent", async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/claims/patient/:patientId
|
||||||
|
router.get(
|
||||||
|
"/patient/:patientId",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const patientIdParam = req.params.patientId;
|
||||||
|
if (!patientIdParam) {
|
||||||
|
return res.status(400).json({ message: "Missing patientId" });
|
||||||
|
}
|
||||||
|
const patientId = parseInt(patientIdParam);
|
||||||
|
if (isNaN(patientId)) {
|
||||||
|
return res.status(400).json({ message: "Invalid patientId" });
|
||||||
|
}
|
||||||
|
const limit = parseInt(req.query.limit as string) || 10;
|
||||||
|
const offset = parseInt(req.query.offset as string) || 0;
|
||||||
|
|
||||||
|
if (isNaN(patientId)) {
|
||||||
|
return res.status(400).json({ message: "Invalid patient ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [claims, totalCount] = await Promise.all([
|
||||||
|
storage.getRecentClaimsByPatientId(patientId, limit, offset),
|
||||||
|
storage.getTotalClaimCountByPatient(patientId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({ claims, totalCount });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to retrieve claims for patient:", error);
|
||||||
|
res.status(500).json({ message: "Failed to retrieve patient claims" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Get all claims for the logged-in user
|
// Get all claims for the logged-in user
|
||||||
router.get("/all", async (req: Request, res: Response) => {
|
router.get("/all", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -219,7 +219,13 @@ export interface IStorage {
|
|||||||
|
|
||||||
// Claim methods
|
// Claim methods
|
||||||
getClaim(id: number): Promise<Claim | undefined>;
|
getClaim(id: number): Promise<Claim | undefined>;
|
||||||
getClaimsByPatientId(patientId: number): Promise<Claim[]>;
|
getRecentClaimsByPatientId(
|
||||||
|
patientId: number,
|
||||||
|
limit: number,
|
||||||
|
offset: number
|
||||||
|
): Promise<ClaimWithServiceLines[]>;
|
||||||
|
|
||||||
|
getTotalClaimCountByPatient(patientId: number): Promise<number>;
|
||||||
getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]>;
|
getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]>;
|
||||||
getRecentClaimsByUser(
|
getRecentClaimsByUser(
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -521,8 +527,27 @@ export const storage: IStorage = {
|
|||||||
return claim ?? undefined;
|
return claim ?? undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getClaimsByPatientId(patientId: number): Promise<Claim[]> {
|
async getRecentClaimsByPatientId(
|
||||||
return await db.claim.findMany({ where: { patientId } });
|
patientId: number,
|
||||||
|
limit: number,
|
||||||
|
offset: number
|
||||||
|
): Promise<ClaimWithServiceLines[]> {
|
||||||
|
return db.claim.findMany({
|
||||||
|
where: { patientId },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
skip: offset,
|
||||||
|
take: limit,
|
||||||
|
include: {
|
||||||
|
serviceLines: true,
|
||||||
|
staff: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTotalClaimCountByPatient(patientId: number): Promise<number> {
|
||||||
|
return db.claim.count({
|
||||||
|
where: { patientId },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]> {
|
async getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]> {
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
// components/claims/ClaimsForPatientTable.tsx
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Edit, Eye, Trash2 } from "lucide-react";
|
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { Pagination, PaginationContent, PaginationItem, PaginationLink } from "@/components/ui/pagination";
|
|
||||||
import { DeleteConfirmationDialog } from "@/components/modals/DeleteConfirmationDialog";
|
|
||||||
import ClaimViewModal from "@/components/modals/ClaimViewModal";
|
|
||||||
|
|
||||||
interface Claim {
|
|
||||||
id: number;
|
|
||||||
patientId: number;
|
|
||||||
patientName: string;
|
|
||||||
serviceDate: string;
|
|
||||||
insuranceProvider: string;
|
|
||||||
status: string;
|
|
||||||
remarks: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
patientId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ClaimsForPatientTable({ patientId }: Props) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [viewClaim, setViewClaim] = useState<Claim | null>(null);
|
|
||||||
const [deleteClaim, setDeleteClaim] = useState<Claim | null>(null);
|
|
||||||
|
|
||||||
const limit = 5;
|
|
||||||
const offset = (currentPage - 1) * limit;
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
|
||||||
queryKey: ["claims-by-patient", patientId, currentPage],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await apiRequest("GET", `/api/claims/by-patient/${patientId}?limit=${limit}&offset=${offset}`);
|
|
||||||
if (!res.ok) throw new Error("Failed to load claims for patient");
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
enabled: !!patientId,
|
|
||||||
placeholderData: { data: [], total: 0 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
|
||||||
mutationFn: async (id: number) => {
|
|
||||||
const res = await apiRequest("DELETE", `/api/claims/${id}`);
|
|
||||||
if (!res.ok) throw new Error("Failed to delete claim");
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast({ title: "Deleted", description: "Claim removed." });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["claims-by-patient"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
if (deleteClaim) deleteMutation.mutate(deleteClaim.id);
|
|
||||||
setDeleteClaim(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalPages = useMemo(() => Math.ceil((data?.total || 0) / limit), [data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded shadow p-4 mt-4">
|
|
||||||
<h2 className="text-xl font-bold mb-4">Claims for Selected Patient</h2>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Claim ID</TableHead>
|
|
||||||
<TableHead>Service Date</TableHead>
|
|
||||||
<TableHead>Insurance</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data?.data?.map((claim: Claim) => (
|
|
||||||
<TableRow key={claim.id}>
|
|
||||||
<TableCell>CLM-{claim.id.toString().padStart(4, "0")}</TableCell>
|
|
||||||
<TableCell>{format(new Date(claim.serviceDate), "MMM dd, yyyy")}</TableCell>
|
|
||||||
<TableCell>{claim.insuranceProvider}</TableCell>
|
|
||||||
<TableCell>{claim.status}</TableCell>
|
|
||||||
<TableCell className="text-right space-x-2">
|
|
||||||
<Button size="icon" variant="ghost" onClick={() => setViewClaim(claim)}>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button size="icon" variant="ghost" onClick={() => {/* handle edit */}}>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button size="icon" variant="ghost" onClick={() => setDeleteClaim(claim)}>
|
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<Pagination>
|
|
||||||
<PaginationContent>
|
|
||||||
{Array.from({ length: totalPages }).map((_, i) => (
|
|
||||||
<PaginationItem key={i}>
|
|
||||||
<PaginationLink isActive={i + 1 === currentPage} onClick={() => setCurrentPage(i + 1)}>
|
|
||||||
{i + 1}
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
))}
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
|
|
||||||
<ClaimViewModal claim={viewClaim} onClose={() => setViewClaim(null)} />
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
isOpen={!!deleteClaim}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
onCancel={() => setDeleteClaim(null)}
|
|
||||||
entityName="claim"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import ClaimsRecentTable from "./claims-recent-table";
|
||||||
|
import { PatientTable } from "../patients/patient-table";
|
||||||
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
|
||||||
|
const PatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
|
appointments: true,
|
||||||
|
});
|
||||||
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
|
export default function ClaimsOfPatientModal() {
|
||||||
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [claimsPage, setClaimsPage] = useState(1);
|
||||||
|
|
||||||
|
const handleSelectPatient = (patient: Patient | null) => {
|
||||||
|
if (patient) {
|
||||||
|
setSelectedPatient(patient);
|
||||||
|
setClaimsPage(1);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 py-8">
|
||||||
|
{/* Claims Section */}
|
||||||
|
{selectedPatient && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle>
|
||||||
|
Claims for {selectedPatient.firstName} {selectedPatient.lastName}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Displaying recent claims for the selected patient.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<div className="p-4">
|
||||||
|
<ClaimsRecentTable
|
||||||
|
patientId={selectedPatient.id}
|
||||||
|
allowView
|
||||||
|
allowEdit
|
||||||
|
allowDelete
|
||||||
|
onPageChange={setClaimsPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Patients Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle>Patients</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Select a patient to view their recent claims.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<div className="p-4">
|
||||||
|
<PatientTable
|
||||||
|
allowView
|
||||||
|
allowCheckbox
|
||||||
|
onSelectPatient={handleSelectPatient}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// components/patients/PatientSearchTable.tsx
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
interface Patient {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
gender: string;
|
|
||||||
dob: string;
|
|
||||||
memberId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSelectPatient: (patient: Patient) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PatientSearchTable({ onSelectPatient }: Props) {
|
|
||||||
const [term, setTerm] = useState("");
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
|
||||||
queryKey: ["patients", term],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await apiRequest("GET", `/api/patients/search?term=${term}`);
|
|
||||||
if (!res.ok) throw new Error("Failed to load patients");
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
enabled: !!term,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (term.length > 0) setVisible(true);
|
|
||||||
}, [term]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Input placeholder="Search patients..." value={term} onChange={(e) => setTerm(e.target.value)} />
|
|
||||||
|
|
||||||
{visible && data?.length > 0 && (
|
|
||||||
<div className="border rounded overflow-hidden">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Gender</TableHead>
|
|
||||||
<TableHead>DOB</TableHead>
|
|
||||||
<TableHead>Member ID</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((patient: Patient) => (
|
|
||||||
<TableRow key={patient.id} onClick={() => onSelectPatient(patient)} className="cursor-pointer hover:bg-muted">
|
|
||||||
<TableCell>{patient.name}</TableCell>
|
|
||||||
<TableCell>{patient.gender}</TableCell>
|
|
||||||
<TableCell>{patient.dob}</TableCell>
|
|
||||||
<TableCell>{patient.memberId}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -83,6 +83,7 @@ interface ClaimsRecentTableProps {
|
|||||||
allowCheckbox?: boolean;
|
allowCheckbox?: boolean;
|
||||||
onSelectClaim?: (claim: Claim | null) => void;
|
onSelectClaim?: (claim: Claim | null) => void;
|
||||||
onPageChange?: (page: number) => void;
|
onPageChange?: (page: number) => void;
|
||||||
|
patientId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ClaimsRecentTable({
|
export default function ClaimsRecentTable({
|
||||||
@@ -92,6 +93,7 @@ export default function ClaimsRecentTable({
|
|||||||
allowCheckbox,
|
allowCheckbox,
|
||||||
onSelectClaim,
|
onSelectClaim,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
|
patientId,
|
||||||
}: ClaimsRecentTableProps) {
|
}: ClaimsRecentTableProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -119,22 +121,24 @@ export default function ClaimsRecentTable({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getClaimsQueryKey = () =>
|
||||||
|
patientId
|
||||||
|
? ["claims-recent", "patient", patientId, currentPage]
|
||||||
|
: ["claims-recent", "global", currentPage];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: claimsData,
|
data: claimsData,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery<ClaimApiResponse, Error>({
|
} = useQuery<ClaimApiResponse, Error>({
|
||||||
queryKey: [
|
queryKey: getClaimsQueryKey(),
|
||||||
"claims-recent",
|
|
||||||
{
|
|
||||||
page: currentPage,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest(
|
const endpoint = patientId
|
||||||
"GET",
|
? `/api/claims/patient/${patientId}?limit=${claimsPerPage}&offset=${offset}`
|
||||||
`/api/claims/recent?limit=${claimsPerPage}&offset=${offset}`
|
: `/api/claims/recent?limit=${claimsPerPage}&offset=${offset}`;
|
||||||
);
|
|
||||||
|
const res = await apiRequest("GET", endpoint);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json();
|
const errorData = await res.json();
|
||||||
throw new Error(errorData.message || "Search failed");
|
throw new Error(errorData.message || "Search failed");
|
||||||
@@ -163,12 +167,7 @@ export default function ClaimsRecentTable({
|
|||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [
|
queryKey: getClaimsQueryKey(),
|
||||||
"claims-recent",
|
|
||||||
{
|
|
||||||
page: currentPage,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -188,12 +187,7 @@ export default function ClaimsRecentTable({
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsDeleteClaimOpen(false);
|
setIsDeleteClaimOpen(false);
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [
|
queryKey: getClaimsQueryKey(),
|
||||||
"claims-recent",
|
|
||||||
{
|
|
||||||
page: currentPage,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
||||||
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
||||||
import ClaimsRecentTable from "@/components/claims/claims-recent-table";
|
import ClaimsRecentTable from "@/components/claims/claims-recent-table";
|
||||||
|
import ClaimsOfPatientModal from "@/components/claims/claims-of-patient-table";
|
||||||
|
|
||||||
//creating types out of schema auto generated.
|
//creating types out of schema auto generated.
|
||||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||||
@@ -708,6 +709,9 @@ export default function ClaimsPage() {
|
|||||||
allowView={true}
|
allowView={true}
|
||||||
allowDelete={true}
|
allowDelete={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Recent Claims by Patients */}
|
||||||
|
<ClaimsOfPatientModal />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user