From ea4a988033929b6f0ef8661f14fa8005d1da6f32 Mon Sep 17 00:00:00 2001 From: Potenz Date: Tue, 22 Jul 2025 22:37:23 +0530 Subject: [PATCH] recent claim table, checkpoint --- apps/Backend/src/routes/claims.ts | 44 +- apps/Backend/src/storage/index.ts | 66 +-- .../src/components/claims/claim-form.tsx | 47 +- .../claims/claims-for-patient-table.tsx | 125 +++++ .../claims/claims-patient-search-table.tsx | 69 +++ .../components/claims/claims-recent-table.tsx | 529 ++++++++++++++++++ .../src/components/claims/recent-claims.tsx | 181 ------ .../src/components/patients/patient-table.tsx | 12 +- apps/Frontend/src/pages/claims-page.tsx | 8 +- apps/Frontend/src/utils/dateUtils.ts | 26 + packages/db/prisma/schema.prisma | 18 +- packages/db/usedSchemas/index.ts | 3 +- 12 files changed, 822 insertions(+), 306 deletions(-) create mode 100644 apps/Frontend/src/components/claims/claims-for-patient-table.tsx create mode 100644 apps/Frontend/src/components/claims/claims-patient-search-table.tsx create mode 100644 apps/Frontend/src/components/claims/claims-recent-table.tsx delete mode 100644 apps/Frontend/src/components/claims/recent-claims.tsx diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 2f116fd..5479c3f 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -90,9 +90,10 @@ router.post( claimData.insuranceSiteKey ); if (!credentials) { - return res - .status(404) - .json({ error: "No insurance credentials found for this provider. Kindly Update this at Settings Page." }); + return res.status(404).json({ + error: + "No insurance credentials found for this provider. Kindly Update this at Settings Page.", + }); } const enrichedData = { @@ -183,35 +184,20 @@ router.post( } ); -// GET /api/claims?page=1&limit=5 -router.get("/", async (req: Request, res: Response) => { - const userId = req.user!.id; - const offset = parseInt(req.query.offset as string) || 0; - const limit = parseInt(req.query.limit as string) || 5; - - try { - const [claims, total] = await Promise.all([ - storage.getClaimsPaginated(userId, offset, limit), - storage.countClaimsByUserId(userId), - ]); - - res.json({ - data: claims, - page: Math.floor(offset / limit) + 1, - limit, - total, - }); - } catch (error) { - res.status(500).json({ message: "Failed to retrieve paginated claims" }); - } -}); - // GET /api/claims/recent router.get("/recent", async (req: Request, res: Response) => { try { - const claims = await storage.getClaimsMetadataByUser(req.user!.id); - res.json(claims); // Just ID and createdAt + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [claims, totalCount] = await Promise.all([ + storage.getRecentClaimsByUser(req.user!.id, limit, offset), + storage.getTotalClaimCountByUser(req.user!.id), + ]); + + res.json({ claims, totalCount }); } catch (error) { + console.error("Failed to retrieve recent claims:", error); res.status(500).json({ message: "Failed to retrieve recent claims" }); } }); @@ -219,7 +205,7 @@ router.get("/recent", async (req: Request, res: Response) => { // Get all claims for the logged-in user router.get("/all", async (req: Request, res: Response) => { try { - const claims = await storage.getClaimsByUserId(req.user!.id); + const claims = await storage.getTotalClaimCountByUser(req.user!.id); res.json(claims); } catch (error) { res.status(500).json({ message: "Failed to retrieve claims" }); diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index b31b91e..6bac228 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -218,18 +218,14 @@ export interface IStorage { // Claim methods getClaim(id: number): Promise; - getClaimsByUserId(userId: number): Promise; getClaimsByPatientId(patientId: number): Promise; getClaimsByAppointmentId(appointmentId: number): Promise; - getClaimsPaginated( + getRecentClaimsByUser( userId: number, - offset: number, - limit: number + limit: number, + offset: number ): Promise; - countClaimsByUserId(userId: number): Promise; - getClaimsMetadataByUser( - userId: number - ): Promise<{ id: number; createdAt: Date; status: string }[]>; + getTotalClaimCountByUser(userId: number): Promise; createClaim(claim: InsertClaim): Promise; updateClaim(id: number, updates: UpdateClaim): Promise; deleteClaim(id: number): Promise; @@ -524,10 +520,6 @@ export const storage: IStorage = { return claim ?? undefined; }, - async getClaimsByUserId(userId: number): Promise { - return await db.claim.findMany({ where: { userId } }); - }, - async getClaimsByPatientId(patientId: number): Promise { return await db.claim.findMany({ where: { patientId } }); }, @@ -536,6 +528,24 @@ export const storage: IStorage = { return await db.claim.findMany({ where: { appointmentId } }); }, + async getRecentClaimsByUser( + userId: number, + limit: number, + offset: number + ): Promise { + return db.claim.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + skip: offset, + take: limit, + include: { serviceLines: true }, + }); + }, + + async getTotalClaimCountByUser(userId: number): Promise { + return db.claim.count({ where: { userId } }); + }, + async createClaim(claim: InsertClaim): Promise { return await db.claim.create({ data: claim as Claim }); }, @@ -559,38 +569,6 @@ export const storage: IStorage = { } }, - async getClaimsPaginated( - userId: number, - offset: number, - limit: number - ): Promise { - return db.claim.findMany({ - where: { userId }, - orderBy: { createdAt: "desc" }, - skip: offset, - take: limit, - include: { serviceLines: true }, - }); - }, - - async countClaimsByUserId(userId: number): Promise { - return db.claim.count({ where: { userId } }); - }, - - async getClaimsMetadataByUser( - userId: number - ): Promise<{ id: number; createdAt: Date; status: string }[]> { - return db.claim.findMany({ - where: { userId }, - orderBy: { createdAt: "desc" }, - select: { - id: true, - createdAt: true, - status: true, - }, - }); - }, - // Insurance Creds async getInsuranceCredentialsByUser(userId: number) { return await db.insuranceCredential.findMany({ where: { userId } }); diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 7563192..d8797ac 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -35,6 +35,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import procedureCodes from "../../assets/data/procedureCodes.json"; +import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -79,7 +80,6 @@ type UpdateAppointment = z.infer; type Claim = z.infer; - interface ServiceLine { procedureCode: string; procedureDate: string; // YYYY-MM-DD @@ -185,33 +185,15 @@ export function ClaimForm({ }, [staffMembersRaw, staff]); // Service date state - function parseLocalDate(dateInput: Date | string): Date { - if (dateInput instanceof Date) return dateInput; - - const parts = dateInput.split("-"); - if (parts.length !== 3) { - throw new Error(`Invalid date format: ${dateInput}`); - } - - const year = Number(parts[0]); - const month = Number(parts[1]); - const day = Number(parts[2]); - - if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) { - throw new Error(`Invalid date parts in date string: ${dateInput}`); - } - - return new Date(year, month - 1, day); // month is 0-indexed - } - const [serviceDateValue, setServiceDateValue] = useState(new Date()); const [serviceDate, setServiceDate] = useState( - new Date().toLocaleDateString("en-CA") // "YYYY-MM-DD" + formatLocalDate(new Date()) ); + useEffect(() => { if (extractedData?.serviceDate) { const parsed = parseLocalDate(extractedData.serviceDate); - const isoFormatted = parsed.toLocaleDateString("en-CA"); + const isoFormatted = formatLocalDate(parsed); setServiceDateValue(parsed); setServiceDate(isoFormatted); } @@ -220,7 +202,7 @@ export function ClaimForm({ // Update service date when calendar date changes const onServiceDateChange = (date: Date | undefined) => { if (date) { - const formattedDate = date.toLocaleDateString("en-CA"); // "YYYY-MM-DD" + const formattedDate = formatLocalDate(date); setServiceDateValue(date); setServiceDate(formattedDate); setForm((prev) => ({ ...prev, serviceDate: formattedDate })); @@ -253,7 +235,7 @@ export function ClaimForm({ serviceDate: serviceDate, insuranceProvider: "", insuranceSiteKey: "", - status: "pending", + status: "PENDING", serviceLines: Array.from({ length: 10 }, () => ({ procedureCode: "", procedureDate: serviceDate, @@ -321,8 +303,7 @@ export function ClaimForm({ const updateProcedureDate = (index: number, date: Date | undefined) => { if (!date) return; - - const formattedDate = date.toLocaleDateString("en-CA"); + const formattedDate = formatLocalDate(date); const updatedLines = [...form.serviceLines]; if (updatedLines[index]) { @@ -417,13 +398,13 @@ export function ClaimForm({ const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form; const createdClaim = await onSubmit({ - ...formToCreateClaim, - serviceLines: filteredServiceLines, - staffId: Number(staff?.id), - patientId: patientId, - insuranceProvider: "MassHealth", - appointmentId: appointmentId!, - }); + ...formToCreateClaim, + serviceLines: filteredServiceLines, + staffId: Number(staff?.id), + patientId: patientId, + insuranceProvider: "MassHealth", + appointmentId: appointmentId!, + }); // 4. sending form data to selenium service onHandleForSelenium({ diff --git a/apps/Frontend/src/components/claims/claims-for-patient-table.tsx b/apps/Frontend/src/components/claims/claims-for-patient-table.tsx new file mode 100644 index 0000000..3f062f5 --- /dev/null +++ b/apps/Frontend/src/components/claims/claims-for-patient-table.tsx @@ -0,0 +1,125 @@ +// 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(null); + const [deleteClaim, setDeleteClaim] = useState(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 ( +
+

Claims for Selected Patient

+ + + + Claim ID + Service Date + Insurance + Status + Actions + + + + {data?.data?.map((claim: Claim) => ( + + CLM-{claim.id.toString().padStart(4, "0")} + {format(new Date(claim.serviceDate), "MMM dd, yyyy")} + {claim.insuranceProvider} + {claim.status} + + + + + + + ))} + +
+ + + + {Array.from({ length: totalPages }).map((_, i) => ( + + setCurrentPage(i + 1)}> + {i + 1} + + + ))} + + + + setViewClaim(null)} /> + setDeleteClaim(null)} + entityName="claim" + /> +
+ ); +} diff --git a/apps/Frontend/src/components/claims/claims-patient-search-table.tsx b/apps/Frontend/src/components/claims/claims-patient-search-table.tsx new file mode 100644 index 0000000..da5fac2 --- /dev/null +++ b/apps/Frontend/src/components/claims/claims-patient-search-table.tsx @@ -0,0 +1,69 @@ +// 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 ( +
+ setTerm(e.target.value)} /> + + {visible && data?.length > 0 && ( +
+ + + + Name + Gender + DOB + Member ID + + + + {data.map((patient: Patient) => ( + onSelectPatient(patient)} className="cursor-pointer hover:bg-muted"> + {patient.name} + {patient.gender} + {patient.dob} + {patient.memberId} + + ))} + +
+
+ )} +
+ ); +} diff --git a/apps/Frontend/src/components/claims/claims-recent-table.tsx b/apps/Frontend/src/components/claims/claims-recent-table.tsx new file mode 100644 index 0000000..4903887 --- /dev/null +++ b/apps/Frontend/src/components/claims/claims-recent-table.tsx @@ -0,0 +1,529 @@ +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 { + PatientUncheckedCreateInputObjectSchema, + ClaimUncheckedCreateInputObjectSchema, + ClaimStatusSchema, +} from "@repo/db/usedSchemas"; +import { z } from "zod"; +import { useAuth } from "@/hooks/use-auth"; +import LoadingScreen from "@/components/ui/LoadingScreen"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import { formatDateToHumanReadable } from "@/utils/dateUtils"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; + +//creating types out of schema auto generated. +type Claim = z.infer; +export type ClaimStatus = z.infer; + +type ClaimWithServiceLines = Claim & { + serviceLines: { + id: number; + claimId: number; + procedureCode: string; + procedureDate: Date; + oralCavityArea: string | null; + toothNumber: string | null; + toothSurface: string | null; + billedAmount: number; + }[]; +}; + +const PatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + appointments: true, +}); +type Patient = z.infer; + +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; +} + +export default function ClaimsRecentTable({ + allowEdit, + allowView, + allowDelete, + allowCheckbox, + onSelectClaim, + onPageChange, +}: ClaimsRecentTableProps) { + const { toast } = useToast(); + const { user } = useAuth(); + + 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( + 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); + } + }; + + const { + data: claimsData, + isLoading, + isError, + } = useQuery({ + queryKey: [ + "claims-recent", + { + page: currentPage, + }, + ], + queryFn: async () => { + const res = await apiRequest( + "GET", + `/api/claims/recent?limit=${claimsPerPage}&offset=${offset}` + ); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.message || "Search failed"); + } + return res.json(); + }, + placeholderData: { claims: [], totalCount: 0 }, + }); + + const deleteClaimMutation = useMutation({ + mutationFn: async (id: number) => { + await apiRequest("DELETE", `/api/claims/${id}`); + return; + }, + onSuccess: () => { + setIsDeleteClaimOpen(false); + queryClient.invalidateQueries({ + queryKey: [ + "claims-recent", + { + page: currentPage, + }, + ], + }); + 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: Claim) => { + setCurrentClaim(claim); + setIsEditClaimOpen(true); + }; + + const handleViewClaim = (claim: Claim) => { + setCurrentClaim(claim); + setIsViewClaimOpen(true); + }; + + const handleDeleteClaim = (claim: Claim) => { + 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] + ); + 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 + (line.billedAmount || 0), + 0 + ); + }; + + return ( + + + Recently Submitted Claims + +
+
+ + + + {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)} + /> + + )} + +
+ CML-{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} + /> + + {/* 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" + : "" + } + /> + + + {Array.from({ length: totalPages }).map((_, i) => ( + + { + e.preventDefault(); + setCurrentPage(i + 1); + }} + isActive={currentPage === i + 1} + > + {i + 1} + + + ))} + + { + e.preventDefault(); + if (currentPage < totalPages) + setCurrentPage(currentPage + 1); + }} + className={ + currentPage === totalPages + ? "pointer-events-none opacity-50" + : "" + } + /> + + + +
+
+ )} +
+
+ ); +} diff --git a/apps/Frontend/src/components/claims/recent-claims.tsx b/apps/Frontend/src/components/claims/recent-claims.tsx deleted file mode 100644 index c2a5d25..0000000 --- a/apps/Frontend/src/components/claims/recent-claims.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { format } from "date-fns"; -import { AlertCircle, CheckCircle, Clock } from "lucide-react"; -import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { apiRequest } from "@/lib/queryClient"; -import { toast } from "@/hooks/use-toast"; -import { useState } from "react"; - -// Types for your data -interface ServiceLine { - billedAmount: number; -} - -interface Claim { - id: number; - patientName: string; - serviceDate: string; - insuranceProvider: string; - status: string; - createdAt: string; - serviceLines: ServiceLine[]; -} - -interface ClaimResponse { - data: Claim[]; - total: number; - page: number; - limit: number; -} - -export default function RecentClaims() { - const [offset, setOffset] = useState(0); - const limit = 5; - - const { data, isLoading, error, isFetching } = useQuery({ - queryKey: ["/api/claims", offset, limit], - queryFn: async () => { - const res = await apiRequest( - "GET", - `/api/claims?offset=${offset}&limit=${limit}` - ); - if (!res.ok) throw new Error("Failed to fetch claims"); - return res.json() as Promise; - }, - }); - - const claims = data?.data ?? []; - const total = data?.total ?? 0; - - const canGoBack = offset > 0; - const canGoNext = offset + limit < total; - - if (isLoading) { - return

Loading claims...

; - } - - if (error) { - return ( -

Failed to load recent claims.

- ); - } - - return ( -
-
-

Recent Claims

-
- - - - Submitted Claims - - - {claims.length === 0 ? ( -
- -

No claims found

-

- Any recent insurance claims will show up here. -

-
- ) : ( -
- {claims.map((claim: Claim) => { - const totalBilled = claim.serviceLines.reduce( - (sum: number, line: ServiceLine) => sum + line.billedAmount, - 0 - ); - - return ( -
- toast({ - title: "Claim Details", - description: `Viewing details for claim #${claim.id}`, - }) - } - > -
-

{claim.patientName}

-
- Claim #: {claim.id} - - - Submitted:{" "} - {format(new Date(claim.createdAt), "MMM dd, yyyy")} - - - Provider: {claim.insuranceProvider} - - Amount: ${totalBilled.toFixed(2)} -
-
-
- - {claim.status === "pending" ? ( - - - Pending - - ) : claim.status === "approved" ? ( - - - Approved - - ) : ( - - - {claim.status} - - )} - -
-
- ); - })} -
- )} - - {total > limit && ( -
-
- Showing {offset + 1}–{Math.min(offset + limit, total)} of{" "} - {total} claims -
-
- - -
-
- )} -
-
-
- ); -} diff --git a/apps/Frontend/src/components/patients/patient-table.tsx b/apps/Frontend/src/components/patients/patient-table.tsx index af4d28f..06ab1eb 100644 --- a/apps/Frontend/src/components/patients/patient-table.tsx +++ b/apps/Frontend/src/components/patients/patient-table.tsx @@ -38,6 +38,7 @@ import { PatientSearch, SearchCriteria } from "./patient-search"; import { useDebounce } from "use-debounce"; import { cn } from "@/lib/utils"; import { Checkbox } from "../ui/checkbox"; +import { formatDateToHumanReadable } from "@/utils/dateUtils"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -329,15 +330,6 @@ export function PatientTable({ return colorClasses[id % colorClasses.length]; }; - const formatDate = (dateString: string | Date) => { - const date = new Date(dateString); - return new Intl.DateTimeFormat("en-US", { - day: "2-digit", - month: "short", - year: "numeric", - }).format(date); - }; - return (
@@ -428,7 +420,7 @@ export function PatientTable({
- {formatDate(patient.dateOfBirth)} + {formatDateToHumanReadable(patient.dateOfBirth)}
{patient.gender} diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index e66abbf..10be423 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -16,7 +16,6 @@ import { parse, format } from "date-fns"; import { z } from "zod"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useLocation } from "wouter"; -import RecentClaims from "@/components/claims/recent-claims"; import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { setTaskStatus, @@ -24,6 +23,7 @@ import { } from "@/redux/slices/seleniumClaimSubmitTaskSlice"; import { SeleniumTaskBanner } from "@/components/claims/selenium-task-banner"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; +import ClaimsRecentTable from "@/components/claims/claims-recent-table"; //creating types out of schema auto generated. type Appointment = z.infer; @@ -703,7 +703,11 @@ export default function ClaimsPage() {
{/* Recent Claims Section */} - +
diff --git a/apps/Frontend/src/utils/dateUtils.ts b/apps/Frontend/src/utils/dateUtils.ts index 32c1a45..a0cc0f5 100644 --- a/apps/Frontend/src/utils/dateUtils.ts +++ b/apps/Frontend/src/utils/dateUtils.ts @@ -72,3 +72,29 @@ export function normalizeToISOString(date: Date | string): string { const parsed = parseLocalDate(date); return parsed.toISOString(); // ensures it always starts from local midnight } + +/** + * Formats a date string or Date object into a human-readable "DD Mon YYYY" string. + * Examples: "22 Jul 2025" + * + * @param dateInput The date as a string (e.g., ISO, YYYY-MM-DD) or a Date object. + * @returns A formatted date string. + */ +export const formatDateToHumanReadable = (dateInput: string | Date): string => { + // Create a Date object from the input. + // The Date constructor is quite flexible with various string formats. + const date = new Date(dateInput); + + // Check if the date is valid. If new Date() fails to parse, it returns "Invalid Date". + if (isNaN(date.getTime())) { + console.error("Invalid date input provided:", dateInput); + return "Invalid Date"; // Or handle this error in a way that suits your UI + } + + // Use Intl.DateTimeFormat for locale-aware, human-readable formatting. + return new Intl.DateTimeFormat("en-US", { + day: "2-digit", // e.g., "01", "22" + month: "short", // e.g., "Jan", "Jul" + year: "numeric", // e.g., "2023", "2025" + }).format(date); +}; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 61767a4..b14c339 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -93,20 +93,20 @@ model Staff { } model Claim { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) patientId Int appointmentId Int userId Int staffId Int patientName String memberId String - dateOfBirth DateTime @db.Date + dateOfBirth DateTime @db.Date remarks String serviceDate DateTime insuranceProvider String // e.g., "Delta MA" - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - status String @default("pending") // "pending", "approved", "cancelled", "review" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + status ClaimStatus @default(PENDING) patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade) @@ -116,6 +116,13 @@ model Claim { serviceLines ServiceLine[] } +enum ClaimStatus { + PENDING + APPROVED + CANCELLED + REVIEW +} + model ServiceLine { id Int @id @default(autoincrement()) claimId Int @@ -163,7 +170,6 @@ model PdfFile { group PdfGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) @@index([groupId]) - } enum PdfCategory { diff --git a/packages/db/usedSchemas/index.ts b/packages/db/usedSchemas/index.ts index b4efb58..87c9315 100644 --- a/packages/db/usedSchemas/index.ts +++ b/packages/db/usedSchemas/index.ts @@ -7,4 +7,5 @@ export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema' export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PdfGroupUncheckedCreateInput.schema' -export * from '../shared/schemas/enums/PdfCategory.schema' \ No newline at end of file +export * from '../shared/schemas/enums/PdfCategory.schema' +export * from '../shared/schemas/enums/ClaimStatus.schema' \ No newline at end of file