import { useEffect, useState, useMemo } 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 { AlertCircle, CheckCircle, Clock, Delete, Edit, Eye, } from "lucide-react"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { DeleteConfirmationDialog } from "../ui/deleteDialog"; import LoadingScreen from "@/components/ui/LoadingScreen"; import { Checkbox } from "@/components/ui/checkbox"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { formatDateToHumanReadable } from "@/utils/dateUtils"; import ClaimViewModal from "./claim-view-modal"; import ClaimEditModal from "./claim-edit-modal"; import { Claim, ClaimStatus, ClaimWithServiceLines } from "@repo/db/types"; interface ClaimApiResponse { claims: ClaimWithServiceLines[]; totalCount: number; } interface ClaimsRecentTableProps { allowEdit?: boolean; allowView?: boolean; allowDelete?: boolean; allowCheckbox?: boolean; onSelectClaim?: (claim: Claim | null) => void; onPageChange?: (page: number) => void; patientId?: number; } // 🔑 exported base key export const QK_CLAIMS_BASE = ["claims-recent"] as const; // helper for specific pages/patient scope export const qkClaimsRecent = (opts: { patientId?: number | null; page: number; }) => opts.patientId ? ([...QK_CLAIMS_BASE, "patient", opts.patientId, opts.page] as const) : ([...QK_CLAIMS_BASE, "global", opts.page] as const); export default function ClaimsRecentTable({ allowEdit, allowView, allowDelete, allowCheckbox, onSelectClaim, onPageChange, patientId, }: ClaimsRecentTableProps) { const { toast } = useToast(); const [isViewClaimOpen, setIsViewClaimOpen] = useState(false); const [isEditClaimOpen, setIsEditClaimOpen] = useState(false); const [isDeleteClaimOpen, setIsDeleteClaimOpen] = useState(false); const [currentPage, setCurrentPage] = useState(1); const claimsPerPage = 5; const offset = (currentPage - 1) * claimsPerPage; const [currentClaim, setCurrentClaim] = useState< ClaimWithServiceLines | undefined >(undefined); const [selectedClaimId, setSelectedClaimId] = useState(null); const handleSelectClaim = (claim: Claim) => { const isSelected = selectedClaimId === claim.id; const newSelectedId = isSelected ? null : claim.id; setSelectedClaimId(Number(newSelectedId)); if (onSelectClaim) { onSelectClaim(isSelected ? null : claim); } }; useEffect(() => { setCurrentPage(1); }, [patientId]); const queryKey = qkClaimsRecent({ patientId: patientId ?? undefined, page: currentPage, }); const { data: claimsData, isLoading, isError, } = useQuery({ queryKey, queryFn: async () => { const endpoint = patientId ? `/api/claims/patient/${patientId}?limit=${claimsPerPage}&offset=${offset}` : `/api/claims/recent?limit=${claimsPerPage}&offset=${offset}`; const res = await apiRequest("GET", endpoint); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.message || "Search failed"); } return res.json(); }, placeholderData: { claims: [], totalCount: 0 }, }); const updateClaimMutation = useMutation({ mutationFn: async (claim: ClaimWithServiceLines) => { const response = await apiRequest("PUT", `/api/claims/${claim.id}`, { status: claim.status, }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || "Failed to update claim"); } return response.json(); }, onSuccess: () => { setIsEditClaimOpen(false); toast({ title: "Success", description: "Claim updated successfully!", variant: "default", }); queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE }); }, onError: (error) => { toast({ title: "Error", description: `Update failed: ${error.message}`, variant: "destructive", }); }, }); const deleteClaimMutation = useMutation({ mutationFn: async (id: number) => { await apiRequest("DELETE", `/api/claims/${id}`); return; }, onSuccess: () => { setIsDeleteClaimOpen(false); queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE }); toast({ title: "Success", description: "Claim deleted successfully!", variant: "default", }); }, onError: (error) => { console.log(error); toast({ title: "Error", description: `Failed to delete claim: ${error.message}`, variant: "destructive", }); }, }); const handleEditClaim = (claim: ClaimWithServiceLines) => { setCurrentClaim(claim); setIsEditClaimOpen(true); }; const handleViewClaim = (claim: ClaimWithServiceLines) => { setCurrentClaim(claim); setIsViewClaimOpen(true); }; const handleDeleteClaim = (claim: ClaimWithServiceLines) => { setCurrentClaim(claim); setIsDeleteClaimOpen(true); }; const handleConfirmDeleteClaim = async () => { if (currentClaim) { if (typeof currentClaim.id === "number") { deleteClaimMutation.mutate(currentClaim.id); } else { toast({ title: "Error", description: "Selected claim is missing an ID for deletion.", variant: "destructive", }); } } else { toast({ title: "Error", description: "No patient selected for deletion.", variant: "destructive", }); } }; useEffect(() => { if (onPageChange) onPageChange(currentPage); }, [currentPage, onPageChange]); const totalPages = useMemo( () => Math.ceil((claimsData?.totalCount || 0) / claimsPerPage), [claimsData?.totalCount, claimsPerPage] ); const startItem = offset + 1; const endItem = Math.min(offset + claimsPerPage, claimsData?.totalCount || 0); const getInitialsFromName = (fullName: string) => { const parts = fullName.trim().split(/\s+/); const filteredParts = parts.filter((part) => part.length > 0); if (filteredParts.length === 0) { return ""; } const firstInitial = filteredParts[0]!.charAt(0).toUpperCase(); if (filteredParts.length === 1) { return firstInitial; } else { const lastInitial = filteredParts[filteredParts.length - 1]!.charAt(0).toUpperCase(); return firstInitial + lastInitial; } }; const getAvatarColor = (id: number) => { const colorClasses = [ "bg-blue-500", "bg-teal-500", "bg-amber-500", "bg-rose-500", "bg-indigo-500", "bg-green-500", "bg-purple-500", ]; return colorClasses[id % colorClasses.length]; }; const getStatusInfo = (status?: ClaimStatus) => { switch (status) { case "PENDING": return { label: "Pending", color: "bg-yellow-100 text-yellow-800", icon: , }; case "APPROVED": return { label: "Approved", color: "bg-green-100 text-green-800", icon: , }; case "CANCELLED": return { label: "Cancelled", color: "bg-red-100 text-red-800", icon: , }; default: return { label: status ? status.charAt(0).toUpperCase() + status.slice(1) : "Unknown", color: "bg-gray-100 text-gray-800", icon: , }; } }; const getTotalBilled = (claim: ClaimWithServiceLines) => { return claim.serviceLines.reduce( (sum, line) => sum + Number(line.totalBilled || 0), 0 ); }; function getPageNumbers(current: number, total: number): (number | "...")[] { const delta = 2; const range: (number | "...")[] = []; const left = Math.max(2, current - delta); const right = Math.min(total - 1, current + delta); range.push(1); if (left > 2) range.push("..."); for (let i = left; i <= right; i++) { range.push(i); } if (right < total - 1) range.push("..."); if (total > 1) range.push(total); return range; } return (
{allowCheckbox && Select} Claim ID Patient Name Submission Date Insurance Provider Member ID Total Billed Status Actions {isLoading ? ( ) : isError ? ( Error loading claims. ) : (claimsData?.claims ?? []).length === 0 ? ( No claims found. ) : ( claimsData?.claims.map((claim) => ( {allowCheckbox && ( handleSelectClaim(claim)} /> )}
CLM-{claim.id!.toString().padStart(4, "0")}
{getInitialsFromName(claim.patientName)}
{claim.patientName}
DOB: {formatDateToHumanReadable(claim.dateOfBirth)}
{formatDateToHumanReadable(claim.createdAt!)}
{claim.insuranceProvider ?? "Not specified"}
{claim.memberId ?? "Not specified"}
${getTotalBilled(claim).toFixed(2)}
{(() => { const { label, color, icon } = getStatusInfo( claim.status ); return ( {icon} {label} ); })()}
{allowDelete && ( )} {allowEdit && ( )} {allowView && ( )}
)) )}
setIsDeleteClaimOpen(false)} entityName={currentClaim?.patientName} /> {isViewClaimOpen && currentClaim && ( setIsViewClaimOpen(false)} onOpenChange={(open) => setIsViewClaimOpen(open)} onEditClaim={(claim) => handleEditClaim(claim)} claim={currentClaim} /> )} {isEditClaimOpen && currentClaim && ( setIsEditClaimOpen(false)} onOpenChange={(open) => setIsEditClaimOpen(open)} claim={currentClaim} onSave={(updatedClaim) => { updateClaimMutation.mutate(updatedClaim); }} /> )} {/* Pagination */} {totalPages > 1 && (
Showing {startItem}–{endItem} of {claimsData?.totalCount || 0}{" "} results
{ e.preventDefault(); if (currentPage > 1) setCurrentPage(currentPage - 1); }} className={ currentPage === 1 ? "pointer-events-none opacity-50" : "" } /> {getPageNumbers(currentPage, totalPages).map((page, idx) => ( {page === "..." ? ( ... ) : ( { e.preventDefault(); setCurrentPage(page as number); }} isActive={currentPage === page} > {page} )} ))} { e.preventDefault(); if (currentPage < totalPages) setCurrentPage(currentPage + 1); }} className={ currentPage === totalPages ? "pointer-events-none opacity-50" : "" } />
)}
); }