diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 894ddda..18316f5 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -76,8 +76,43 @@ router.post("/selenium", upload.array("pdfs"), async (req: Request, res: Respons } }); -// Get all claims for the logged-in user +// GET /api/claims?page=1&limit=5 router.get("/", async (req: Request, res: Response) => { + const userId = req.user!.id; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 5; + const offset = (page - 1) * limit; + + try { + const [claims, total] = await Promise.all([ + storage.getClaimsPaginated(userId, offset, limit), + storage.countClaimsByUserId(userId), + ]); + + res.json({ + data: claims, + page, + 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 + } catch (error) { + res.status(500).json({ message: "Failed to retrieve recent claims" }); + } +}); + + +// 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); res.json(claims); @@ -86,6 +121,7 @@ router.get("/", async (req: Request, res: Response) => { } }); + // Get a single claim by ID router.get("/:id", async (req: Request, res: Response): Promise => { try { diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 5b6af3f..0bcd234 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -132,6 +132,19 @@ type InsertInsuranceCredential = z.infer< typeof insertInsuranceCredentialSchema >; +type ClaimWithServiceLines = Claim & { + serviceLines: { + id: number; + claimId: number; + procedureCode: string; + procedureDate: Date; + oralCavityArea: string | null; + toothNumber: string | null; + toothSurface: string | null; + billedAmount: number; + }[]; +}; + export interface IStorage { // User methods getUser(id: number): Promise; @@ -171,6 +184,15 @@ export interface IStorage { getClaimsByUserId(userId: number): Promise; getClaimsByPatientId(patientId: number): Promise; getClaimsByAppointmentId(appointmentId: number): Promise; + getClaimsPaginated( + userId: number, + offset: number, + limit: number + ): Promise; + countClaimsByUserId(userId: number): Promise; + getClaimsMetadataByUser( + userId: number + ): Promise<{ id: number; createdAt: Date; status: string }[]>; createClaim(claim: InsertClaim): Promise; updateClaim(id: number, updates: UpdateClaim): Promise; deleteClaim(id: number): Promise; @@ -185,7 +207,10 @@ export interface IStorage { updates: Partial ): Promise; deleteInsuranceCredential(id: number): Promise; - getInsuranceCredentialByUserAndSiteKey(userId: number, siteKey: string): Promise; + getInsuranceCredentialByUserAndSiteKey( + userId: number, + siteKey: string + ): Promise; } export const storage: IStorage = { @@ -389,7 +414,9 @@ export const storage: IStorage = { }, async createInsuranceCredential(data: InsertInsuranceCredential) { - return await db.insuranceCredential.create({ data: data as InsuranceCredential }); + return await db.insuranceCredential.create({ + data: data as InsuranceCredential, + }); }, async updateInsuranceCredential( @@ -406,9 +433,44 @@ export const storage: IStorage = { await db.insuranceCredential.delete({ where: { id } }); }, - async getInsuranceCredentialByUserAndSiteKey(userId: number, siteKey: string) { - return await db.insuranceCredential.findFirst({ - where: { userId, siteKey }, - }); -}, + async getInsuranceCredentialByUserAndSiteKey( + userId: number, + siteKey: string + ): Promise { + return await db.insuranceCredential.findFirst({ + where: { userId, siteKey }, + }); + }, + + 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, + }, + }); + }, }; diff --git a/apps/Frontend/src/components/claims/recent-claims.tsx b/apps/Frontend/src/components/claims/recent-claims.tsx new file mode 100644 index 0000000..c2a5d25 --- /dev/null +++ b/apps/Frontend/src/components/claims/recent-claims.tsx @@ -0,0 +1,181 @@ +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/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 66061d4..25c434f 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -10,11 +10,12 @@ import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; -import { FileCheck } from "lucide-react"; +import { AlertCircle, CheckCircle, Clock, FileCheck } from "lucide-react"; 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"; //creating types out of schema auto generated. type Appointment = z.infer; @@ -611,6 +612,9 @@ export default function ClaimsPage() { + + {/* Recent Claims Section */} + diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index bb52d93..edd465e 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -99,7 +99,7 @@ model Claim { insuranceProvider String // e.g., "Delta MA" createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - status String @default("pending") // "pending", "completed", "cancelled", "no-show" + status String @default("pending") // "pending", "approved", "cancelled", "review" patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)