recent claim added to claim page

This commit is contained in:
2025-06-04 22:51:44 +05:30
parent 27039d18f4
commit 99b826568d
5 changed files with 293 additions and 10 deletions

View File

@@ -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<any> => {
try {

View File

@@ -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<User | undefined>;
@@ -171,6 +184,15 @@ export interface IStorage {
getClaimsByUserId(userId: number): Promise<Claim[]>;
getClaimsByPatientId(patientId: number): Promise<Claim[]>;
getClaimsByAppointmentId(appointmentId: number): Promise<Claim[]>;
getClaimsPaginated(
userId: number,
offset: number,
limit: number
): Promise<Claim[]>;
countClaimsByUserId(userId: number): Promise<number>;
getClaimsMetadataByUser(
userId: number
): Promise<{ id: number; createdAt: Date; status: string }[]>;
createClaim(claim: InsertClaim): Promise<Claim>;
updateClaim(id: number, updates: UpdateClaim): Promise<Claim>;
deleteClaim(id: number): Promise<void>;
@@ -185,7 +207,10 @@ export interface IStorage {
updates: Partial<InsuranceCredential>
): Promise<InsuranceCredential>;
deleteInsuranceCredential(id: number): Promise<void>;
getInsuranceCredentialByUserAndSiteKey(userId: number, siteKey: string): Promise<InsuranceCredential | null>;
getInsuranceCredentialByUserAndSiteKey(
userId: number,
siteKey: string
): Promise<InsuranceCredential | null>;
}
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<InsuranceCredential | null> {
return await db.insuranceCredential.findFirst({
where: { userId, siteKey },
});
},
async getClaimsPaginated(
userId: number,
offset: number,
limit: number
): Promise<ClaimWithServiceLines[]> {
return db.claim.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
skip: offset,
take: limit,
include: { serviceLines: true },
});
},
async countClaimsByUserId(userId: number): Promise<number> {
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,
},
});
},
};

View File

@@ -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<ClaimResponse>;
},
});
const claims = data?.data ?? [];
const total = data?.total ?? 0;
const canGoBack = offset > 0;
const canGoNext = offset + limit < total;
if (isLoading) {
return <p className="text-sm text-gray-500">Loading claims...</p>;
}
if (error) {
return (
<p className="text-sm text-red-500">Failed to load recent claims.</p>
);
}
return (
<div className="mt-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">Recent Claims</h2>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Submitted Claims</CardTitle>
</CardHeader>
<CardContent>
{claims.length === 0 ? (
<div className="text-center py-8">
<Clock className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No claims found</h3>
<p className="text-gray-500 mt-1">
Any recent insurance claims will show up here.
</p>
</div>
) : (
<div className="divide-y">
{claims.map((claim: Claim) => {
const totalBilled = claim.serviceLines.reduce(
(sum: number, line: ServiceLine) => sum + line.billedAmount,
0
);
return (
<div
key={`claim-${claim.id}`}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() =>
toast({
title: "Claim Details",
description: `Viewing details for claim #${claim.id}`,
})
}
>
<div>
<h3 className="font-medium">{claim.patientName}</h3>
<div className="text-sm text-gray-500">
<span>Claim #: {claim.id}</span>
<span className="mx-2"></span>
<span>
Submitted:{" "}
{format(new Date(claim.createdAt), "MMM dd, yyyy")}
</span>
<span className="mx-2"></span>
<span>Provider: {claim.insuranceProvider}</span>
<span className="mx-2"></span>
<span>Amount: ${totalBilled.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center gap-2">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
claim.status === "pending"
? "bg-yellow-100 text-yellow-800"
: claim.status === "completed"
? "bg-green-100 text-green-800"
: "bg-blue-100 text-blue-800"
}`}
>
{claim.status === "pending" ? (
<span className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
Pending
</span>
) : claim.status === "approved" ? (
<span className="flex items-center">
<CheckCircle className="h-3 w-3 mr-1" />
Approved
</span>
) : (
<span className="flex items-center">
<AlertCircle className="h-3 w-3 mr-1" />
{claim.status}
</span>
)}
</span>
</div>
</div>
);
})}
</div>
)}
{total > limit && (
<div className="flex items-center justify-between mt-6">
<div className="text-sm text-gray-500">
Showing {offset + 1}{Math.min(offset + limit, total)} of{" "}
{total} claims
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={!canGoBack || isFetching}
onClick={() => setOffset((prev) => Math.max(prev - limit, 0))}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={!canGoNext || isFetching}
onClick={() => setOffset((prev) => prev + limit)}
>
Next
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -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<typeof AppointmentUncheckedCreateInputObjectSchema>;
@@ -611,6 +612,9 @@ export default function ClaimsPage() {
</CardContent>
</Card>
</div>
{/* Recent Claims Section */}
<RecentClaims />
</main>
</div>