recent claim added to claim page
This commit is contained in:
@@ -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) => {
|
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 {
|
try {
|
||||||
const claims = await storage.getClaimsByUserId(req.user!.id);
|
const claims = await storage.getClaimsByUserId(req.user!.id);
|
||||||
res.json(claims);
|
res.json(claims);
|
||||||
@@ -86,6 +121,7 @@ router.get("/", async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Get a single claim by ID
|
// Get a single claim by ID
|
||||||
router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -132,6 +132,19 @@ type InsertInsuranceCredential = z.infer<
|
|||||||
typeof insertInsuranceCredentialSchema
|
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 {
|
export interface IStorage {
|
||||||
// User methods
|
// User methods
|
||||||
getUser(id: number): Promise<User | undefined>;
|
getUser(id: number): Promise<User | undefined>;
|
||||||
@@ -171,6 +184,15 @@ export interface IStorage {
|
|||||||
getClaimsByUserId(userId: number): Promise<Claim[]>;
|
getClaimsByUserId(userId: number): Promise<Claim[]>;
|
||||||
getClaimsByPatientId(patientId: number): Promise<Claim[]>;
|
getClaimsByPatientId(patientId: number): Promise<Claim[]>;
|
||||||
getClaimsByAppointmentId(appointmentId: 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>;
|
createClaim(claim: InsertClaim): Promise<Claim>;
|
||||||
updateClaim(id: number, updates: UpdateClaim): Promise<Claim>;
|
updateClaim(id: number, updates: UpdateClaim): Promise<Claim>;
|
||||||
deleteClaim(id: number): Promise<void>;
|
deleteClaim(id: number): Promise<void>;
|
||||||
@@ -185,7 +207,10 @@ export interface IStorage {
|
|||||||
updates: Partial<InsuranceCredential>
|
updates: Partial<InsuranceCredential>
|
||||||
): Promise<InsuranceCredential>;
|
): Promise<InsuranceCredential>;
|
||||||
deleteInsuranceCredential(id: number): Promise<void>;
|
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 = {
|
export const storage: IStorage = {
|
||||||
@@ -389,7 +414,9 @@ export const storage: IStorage = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async createInsuranceCredential(data: InsertInsuranceCredential) {
|
async createInsuranceCredential(data: InsertInsuranceCredential) {
|
||||||
return await db.insuranceCredential.create({ data: data as InsuranceCredential });
|
return await db.insuranceCredential.create({
|
||||||
|
data: data as InsuranceCredential,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateInsuranceCredential(
|
async updateInsuranceCredential(
|
||||||
@@ -406,9 +433,44 @@ export const storage: IStorage = {
|
|||||||
await db.insuranceCredential.delete({ where: { id } });
|
await db.insuranceCredential.delete({ where: { id } });
|
||||||
},
|
},
|
||||||
|
|
||||||
async getInsuranceCredentialByUserAndSiteKey(userId: number, siteKey: string) {
|
async getInsuranceCredentialByUserAndSiteKey(
|
||||||
|
userId: number,
|
||||||
|
siteKey: string
|
||||||
|
): Promise<InsuranceCredential | null> {
|
||||||
return await db.insuranceCredential.findFirst({
|
return await db.insuranceCredential.findFirst({
|
||||||
where: { userId, siteKey },
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
181
apps/Frontend/src/components/claims/recent-claims.tsx
Normal file
181
apps/Frontend/src/components/claims/recent-claims.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,11 +10,12 @@ import {
|
|||||||
PatientUncheckedCreateInputObjectSchema,
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
AppointmentUncheckedCreateInputObjectSchema,
|
AppointmentUncheckedCreateInputObjectSchema,
|
||||||
} from "@repo/db/usedSchemas";
|
} from "@repo/db/usedSchemas";
|
||||||
import { FileCheck } from "lucide-react";
|
import { AlertCircle, CheckCircle, Clock, FileCheck } from "lucide-react";
|
||||||
import { parse, format } from "date-fns";
|
import { parse, format } from "date-fns";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
import RecentClaims from "@/components/claims/recent-claims";
|
||||||
|
|
||||||
//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>;
|
||||||
@@ -611,6 +612,9 @@ export default function ClaimsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Claims Section */}
|
||||||
|
<RecentClaims />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ model Claim {
|
|||||||
insuranceProvider String // e.g., "Delta MA"
|
insuranceProvider String // e.g., "Delta MA"
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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)
|
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||||
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
|
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
Reference in New Issue
Block a user