recent claim table, checkpoint

This commit is contained in:
2025-07-22 22:37:23 +05:30
parent 297f29ac43
commit ea4a988033
12 changed files with 822 additions and 306 deletions

View File

@@ -90,9 +90,10 @@ router.post(
claimData.insuranceSiteKey claimData.insuranceSiteKey
); );
if (!credentials) { if (!credentials) {
return res return res.status(404).json({
.status(404) error:
.json({ error: "No insurance credentials found for this provider. Kindly Update this at Settings Page." }); "No insurance credentials found for this provider. Kindly Update this at Settings Page.",
});
} }
const enrichedData = { 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 // GET /api/claims/recent
router.get("/recent", async (req: Request, res: Response) => { router.get("/recent", async (req: Request, res: Response) => {
try { try {
const claims = await storage.getClaimsMetadataByUser(req.user!.id); const limit = parseInt(req.query.limit as string) || 10;
res.json(claims); // Just ID and createdAt 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) { } catch (error) {
console.error("Failed to retrieve recent claims:", error);
res.status(500).json({ message: "Failed to retrieve recent claims" }); 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 // Get all claims for the logged-in user
router.get("/all", async (req: Request, res: Response) => { router.get("/all", async (req: Request, res: Response) => {
try { try {
const claims = await storage.getClaimsByUserId(req.user!.id); const claims = await storage.getTotalClaimCountByUser(req.user!.id);
res.json(claims); res.json(claims);
} catch (error) { } catch (error) {
res.status(500).json({ message: "Failed to retrieve claims" }); res.status(500).json({ message: "Failed to retrieve claims" });

View File

@@ -218,18 +218,14 @@ export interface IStorage {
// Claim methods // Claim methods
getClaim(id: number): Promise<Claim | undefined>; getClaim(id: number): Promise<Claim | undefined>;
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( getRecentClaimsByUser(
userId: number, userId: number,
offset: number, limit: number,
limit: number offset: number
): Promise<Claim[]>; ): Promise<Claim[]>;
countClaimsByUserId(userId: number): Promise<number>; getTotalClaimCountByUser(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>;
@@ -524,10 +520,6 @@ export const storage: IStorage = {
return claim ?? undefined; return claim ?? undefined;
}, },
async getClaimsByUserId(userId: number): Promise<Claim[]> {
return await db.claim.findMany({ where: { userId } });
},
async getClaimsByPatientId(patientId: number): Promise<Claim[]> { async getClaimsByPatientId(patientId: number): Promise<Claim[]> {
return await db.claim.findMany({ where: { patientId } }); return await db.claim.findMany({ where: { patientId } });
}, },
@@ -536,6 +528,24 @@ export const storage: IStorage = {
return await db.claim.findMany({ where: { appointmentId } }); return await db.claim.findMany({ where: { appointmentId } });
}, },
async getRecentClaimsByUser(
userId: number,
limit: number,
offset: number
): Promise<ClaimWithServiceLines[]> {
return db.claim.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
skip: offset,
take: limit,
include: { serviceLines: true },
});
},
async getTotalClaimCountByUser(userId: number): Promise<number> {
return db.claim.count({ where: { userId } });
},
async createClaim(claim: InsertClaim): Promise<Claim> { async createClaim(claim: InsertClaim): Promise<Claim> {
return await db.claim.create({ data: claim as Claim }); 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<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,
},
});
},
// Insurance Creds // Insurance Creds
async getInsuranceCredentialsByUser(userId: number) { async getInsuranceCredentialsByUser(userId: number) {
return await db.insuranceCredential.findMany({ where: { userId } }); return await db.insuranceCredential.findMany({ where: { userId } });

View File

@@ -35,6 +35,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import procedureCodes from "../../assets/data/procedureCodes.json"; import procedureCodes from "../../assets/data/procedureCodes.json";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
const PatientSchema = ( const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -79,7 +80,6 @@ type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>; type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
interface ServiceLine { interface ServiceLine {
procedureCode: string; procedureCode: string;
procedureDate: string; // YYYY-MM-DD procedureDate: string; // YYYY-MM-DD
@@ -185,33 +185,15 @@ export function ClaimForm({
}, [staffMembersRaw, staff]); }, [staffMembersRaw, staff]);
// Service date state // 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<Date>(new Date()); const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
const [serviceDate, setServiceDate] = useState<string>( const [serviceDate, setServiceDate] = useState<string>(
new Date().toLocaleDateString("en-CA") // "YYYY-MM-DD" formatLocalDate(new Date())
); );
useEffect(() => { useEffect(() => {
if (extractedData?.serviceDate) { if (extractedData?.serviceDate) {
const parsed = parseLocalDate(extractedData.serviceDate); const parsed = parseLocalDate(extractedData.serviceDate);
const isoFormatted = parsed.toLocaleDateString("en-CA"); const isoFormatted = formatLocalDate(parsed);
setServiceDateValue(parsed); setServiceDateValue(parsed);
setServiceDate(isoFormatted); setServiceDate(isoFormatted);
} }
@@ -220,7 +202,7 @@ export function ClaimForm({
// Update service date when calendar date changes // Update service date when calendar date changes
const onServiceDateChange = (date: Date | undefined) => { const onServiceDateChange = (date: Date | undefined) => {
if (date) { if (date) {
const formattedDate = date.toLocaleDateString("en-CA"); // "YYYY-MM-DD" const formattedDate = formatLocalDate(date);
setServiceDateValue(date); setServiceDateValue(date);
setServiceDate(formattedDate); setServiceDate(formattedDate);
setForm((prev) => ({ ...prev, serviceDate: formattedDate })); setForm((prev) => ({ ...prev, serviceDate: formattedDate }));
@@ -253,7 +235,7 @@ export function ClaimForm({
serviceDate: serviceDate, serviceDate: serviceDate,
insuranceProvider: "", insuranceProvider: "",
insuranceSiteKey: "", insuranceSiteKey: "",
status: "pending", status: "PENDING",
serviceLines: Array.from({ length: 10 }, () => ({ serviceLines: Array.from({ length: 10 }, () => ({
procedureCode: "", procedureCode: "",
procedureDate: serviceDate, procedureDate: serviceDate,
@@ -321,8 +303,7 @@ export function ClaimForm({
const updateProcedureDate = (index: number, date: Date | undefined) => { const updateProcedureDate = (index: number, date: Date | undefined) => {
if (!date) return; if (!date) return;
const formattedDate = formatLocalDate(date);
const formattedDate = date.toLocaleDateString("en-CA");
const updatedLines = [...form.serviceLines]; const updatedLines = [...form.serviceLines];
if (updatedLines[index]) { if (updatedLines[index]) {
@@ -417,13 +398,13 @@ export function ClaimForm({
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form; const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form;
const createdClaim = await onSubmit({ const createdClaim = await onSubmit({
...formToCreateClaim, ...formToCreateClaim,
serviceLines: filteredServiceLines, serviceLines: filteredServiceLines,
staffId: Number(staff?.id), staffId: Number(staff?.id),
patientId: patientId, patientId: patientId,
insuranceProvider: "MassHealth", insuranceProvider: "MassHealth",
appointmentId: appointmentId!, appointmentId: appointmentId!,
}); });
// 4. sending form data to selenium service // 4. sending form data to selenium service
onHandleForSelenium({ onHandleForSelenium({

View File

@@ -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<Claim | null>(null);
const [deleteClaim, setDeleteClaim] = useState<Claim | null>(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 (
<div className="bg-white rounded shadow p-4 mt-4">
<h2 className="text-xl font-bold mb-4">Claims for Selected Patient</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Claim ID</TableHead>
<TableHead>Service Date</TableHead>
<TableHead>Insurance</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.data?.map((claim: Claim) => (
<TableRow key={claim.id}>
<TableCell>CLM-{claim.id.toString().padStart(4, "0")}</TableCell>
<TableCell>{format(new Date(claim.serviceDate), "MMM dd, yyyy")}</TableCell>
<TableCell>{claim.insuranceProvider}</TableCell>
<TableCell>{claim.status}</TableCell>
<TableCell className="text-right space-x-2">
<Button size="icon" variant="ghost" onClick={() => setViewClaim(claim)}>
<Eye className="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" onClick={() => {/* handle edit */}}>
<Edit className="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" onClick={() => setDeleteClaim(claim)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Pagination>
<PaginationContent>
{Array.from({ length: totalPages }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink isActive={i + 1 === currentPage} onClick={() => setCurrentPage(i + 1)}>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
</PaginationContent>
</Pagination>
<ClaimViewModal claim={viewClaim} onClose={() => setViewClaim(null)} />
<DeleteConfirmationDialog
isOpen={!!deleteClaim}
onConfirm={handleDelete}
onCancel={() => setDeleteClaim(null)}
entityName="claim"
/>
</div>
);
}

View File

@@ -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 (
<div className="space-y-2">
<Input placeholder="Search patients..." value={term} onChange={(e) => setTerm(e.target.value)} />
{visible && data?.length > 0 && (
<div className="border rounded overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Gender</TableHead>
<TableHead>DOB</TableHead>
<TableHead>Member ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((patient: Patient) => (
<TableRow key={patient.id} onClick={() => onSelectPatient(patient)} className="cursor-pointer hover:bg-muted">
<TableCell>{patient.name}</TableCell>
<TableCell>{patient.gender}</TableCell>
<TableCell>{patient.dob}</TableCell>
<TableCell>{patient.memberId}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
);
}

View File

@@ -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<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
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<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
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<Claim | undefined>(
undefined
);
const [selectedClaimId, setSelectedClaimId] = useState<number | null>(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<ClaimApiResponse, Error>({
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: <Clock className="h-3 w-3 mr-1" />,
};
case "APPROVED":
return {
label: "Approved",
color: "bg-green-100 text-green-800",
icon: <CheckCircle className="h-3 w-3 mr-1" />,
};
case "CANCELLED":
return {
label: "Cancelled",
color: "bg-red-100 text-red-800",
icon: <AlertCircle className="h-3 w-3 mr-1" />,
};
default:
return {
label: status
? status.charAt(0).toUpperCase() + status.slice(1)
: "Unknown",
color: "bg-gray-100 text-gray-800",
icon: <AlertCircle className="h-3 w-3 mr-1" />,
};
}
};
const getTotalBilled = (claim: ClaimWithServiceLines) => {
return claim.serviceLines.reduce(
(sum, line) => sum + (line.billedAmount || 0),
0
);
};
return (
<Card className="mt-8">
<CardHeader className="pb-8">
<CardTitle>Recently Submitted Claims</CardTitle>
</CardHeader>
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
{allowCheckbox && <TableHead>Select</TableHead>}
<TableHead>Claim ID</TableHead>
<TableHead>Patient Name</TableHead>
<TableHead>Submission Date</TableHead>
<TableHead>Insurance Provider</TableHead>
<TableHead>Member ID</TableHead>
<TableHead>Total Billed</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-muted-foreground"
>
<LoadingScreen />
</TableCell>
</TableRow>
) : isError ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-red-500"
>
Error loading claims.
</TableCell>
</TableRow>
) : (claimsData?.claims ?? []).length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-muted-foreground"
>
No claims found.
</TableCell>
</TableRow>
) : (
claimsData?.claims.map((claim) => (
<TableRow key={claim.id} className="hover:bg-gray-50">
{allowCheckbox && (
<TableCell>
<Checkbox
checked={selectedClaimId === claim.id}
onCheckedChange={() => handleSelectClaim(claim)}
/>
</TableCell>
)}
<TableCell>
<div className="text-sm font-medium text-gray-900">
CML-{claim.id!.toString().padStart(4, "0")}
</div>
</TableCell>
<TableCell>
<div className="flex items-center">
<Avatar
className={`h-10 w-10 ${getAvatarColor(claim.patientId)}`}
>
<AvatarFallback className="text-white">
{getInitialsFromName(claim.patientName)}
</AvatarFallback>
</Avatar>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{claim.patientName}
</div>
<div className="text-sm text-gray-500">
DOB: {formatDateToHumanReadable(claim.dateOfBirth)}
</div>
</div>
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{formatDateToHumanReadable(claim.createdAt!)}
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{claim.insuranceProvider ?? "Not specified"}
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{claim.memberId ?? "Not specified"}
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
${getTotalBilled(claim).toFixed(2)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{(() => {
const { label, color, icon } = getStatusInfo(
claim.status
);
return (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${color}`}
>
<span className="flex items-center">
{icon}
{label}
</span>
</span>
);
})()}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
{allowDelete && (
<Button
onClick={() => {
handleDeleteClaim(claim);
}}
className="text-red-600 hover:text-red-900"
aria-label="Delete Staff"
variant="ghost"
size="icon"
>
<Delete />
</Button>
)}
{allowEdit && (
<Button
variant="ghost"
size="icon"
onClick={() => {
handleEditClaim(claim);
}}
className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
>
<Edit className="h-4 w-4" />
</Button>
)}
{allowView && (
<Button
variant="ghost"
size="icon"
onClick={() => {
handleViewClaim(claim);
}}
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
>
<Eye className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<DeleteConfirmationDialog
isOpen={isDeleteClaimOpen}
onConfirm={handleConfirmDeleteClaim}
onCancel={() => setIsDeleteClaimOpen(false)}
entityName={currentClaim?.patientName}
/>
{/* Pagination */}
{totalPages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-muted-foreground mb-2 sm:mb-0 whitespace-nowrap">
Showing {startItem}{endItem} of {claimsData?.totalCount || 0}{" "}
results
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={
currentPage === 1
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
{Array.from({ length: totalPages }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage(i + 1);
}}
isActive={currentPage === i + 1}
>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages)
setCurrentPage(currentPage + 1);
}}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
)}
</div>
</Card>
);
}

View File

@@ -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<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

@@ -38,6 +38,7 @@ import { PatientSearch, SearchCriteria } from "./patient-search";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Checkbox } from "../ui/checkbox"; import { Checkbox } from "../ui/checkbox";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
const PatientSchema = ( const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -329,15 +330,6 @@ export function PatientTable({
return colorClasses[id % colorClasses.length]; 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 ( return (
<div className="bg-white shadow rounded-lg overflow-hidden"> <div className="bg-white shadow rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -428,7 +420,7 @@ export function PatientTable({
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="text-sm text-gray-900"> <div className="text-sm text-gray-900">
{formatDate(patient.dateOfBirth)} {formatDateToHumanReadable(patient.dateOfBirth)}
</div> </div>
<div className="text-sm text-gray-500 capitalize"> <div className="text-sm text-gray-500 capitalize">
{patient.gender} {patient.gender}

View File

@@ -16,7 +16,6 @@ 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";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch, useAppSelector } from "@/redux/hooks";
import { import {
setTaskStatus, setTaskStatus,
@@ -24,6 +23,7 @@ import {
} from "@/redux/slices/seleniumClaimSubmitTaskSlice"; } from "@/redux/slices/seleniumClaimSubmitTaskSlice";
import { SeleniumTaskBanner } from "@/components/claims/selenium-task-banner"; import { SeleniumTaskBanner } from "@/components/claims/selenium-task-banner";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
import ClaimsRecentTable from "@/components/claims/claims-recent-table";
//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>;
@@ -703,7 +703,11 @@ export default function ClaimsPage() {
</div> </div>
{/* Recent Claims Section */} {/* Recent Claims Section */}
<RecentClaims /> <ClaimsRecentTable
allowEdit={true}
allowView={true}
allowDelete={true}
/>
</main> </main>
</div> </div>

View File

@@ -72,3 +72,29 @@ export function normalizeToISOString(date: Date | string): string {
const parsed = parseLocalDate(date); const parsed = parseLocalDate(date);
return parsed.toISOString(); // ensures it always starts from local midnight 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);
};

View File

@@ -93,20 +93,20 @@ model Staff {
} }
model Claim { model Claim {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
patientId Int patientId Int
appointmentId Int appointmentId Int
userId Int userId Int
staffId Int staffId Int
patientName String patientName String
memberId String memberId String
dateOfBirth DateTime @db.Date dateOfBirth DateTime @db.Date
remarks String remarks String
serviceDate DateTime serviceDate DateTime
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", "approved", "cancelled", "review" status ClaimStatus @default(PENDING)
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)
@@ -116,6 +116,13 @@ model Claim {
serviceLines ServiceLine[] serviceLines ServiceLine[]
} }
enum ClaimStatus {
PENDING
APPROVED
CANCELLED
REVIEW
}
model ServiceLine { model ServiceLine {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
claimId Int claimId Int
@@ -163,7 +170,6 @@ model PdfFile {
group PdfGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) group PdfGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
@@index([groupId]) @@index([groupId])
} }
enum PdfCategory { enum PdfCategory {

View File

@@ -8,3 +8,4 @@ export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput
export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema'
export * from '../shared/schemas/objects/PdfGroupUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PdfGroupUncheckedCreateInput.schema'
export * from '../shared/schemas/enums/PdfCategory.schema' export * from '../shared/schemas/enums/PdfCategory.schema'
export * from '../shared/schemas/enums/ClaimStatus.schema'