claim recent table done

This commit is contained in:
2025-07-25 18:07:18 +05:30
parent 4ac357fba7
commit 0f54bc6121
5 changed files with 407 additions and 43 deletions

View File

@@ -146,6 +146,7 @@ type ClaimWithServiceLines = Claim & {
toothSurface: string | null; toothSurface: string | null;
billedAmount: number; billedAmount: number;
}[]; }[];
staff: Staff | null;
}; };
// Pdf types: // Pdf types:
@@ -538,7 +539,7 @@ export const storage: IStorage = {
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
skip: offset, skip: offset,
take: limit, take: limit,
include: { serviceLines: true }, include: { serviceLines: true, staff: true },
}); });
}, },

View File

@@ -0,0 +1,254 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
import React, { useState } from "react";
import { z } from "zod";
import {
ClaimUncheckedCreateInputObjectSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { ClaimStatus } from "./claims-recent-table";
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
type ClaimWithServiceLines = Claim & {
serviceLines: {
id: number;
claimId: number;
procedureCode: string;
procedureDate: Date;
oralCavityArea: string | null;
toothNumber: string | null;
toothSurface: string | null;
billedAmount: number;
}[];
staff: Staff | null;
};
type ClaimEditModalProps = {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onClose: () => void;
claim: ClaimWithServiceLines | null;
onSave: (updatedClaim: ClaimWithServiceLines) => void;
};
export default function ClaimEditModal({
isOpen,
onOpenChange,
onClose,
claim,
onSave,
}: ClaimEditModalProps) {
const [status, setStatus] = useState<ClaimStatus>(
claim?.status ?? ("PENDING" as ClaimStatus)
);
if (!claim) return null;
const handleSave = () => {
const updatedClaim: ClaimWithServiceLines = {
...claim,
status,
};
onSave(updatedClaim);
onOpenChange(false);
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Claim Status</DialogTitle>
<DialogDescription>Update the status of the claim.</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Patient Details */}
<div className="flex items-center space-x-4">
<div className="h-16 w-16 rounded-full bg-blue-600 text-white flex items-center justify-center text-xl font-medium">
{claim.patientName.charAt(0)}
</div>
<div>
<h3 className="text-xl font-semibold">{claim.patientName}</h3>
<p className="text-gray-500">
Claim ID: {claim.id?.toString().padStart(4, "0")}
</p>
</div>
</div>
{/* Basic Info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
<div>
<h4 className="font-medium text-gray-900">Basic Information</h4>
<div className="mt-2 space-y-2">
<p>
<span className="text-gray-500">Date of Birth:</span>{" "}
{new Date(claim.dateOfBirth).toLocaleDateString()}
</p>
<p>
<span className="text-gray-500">Service Date:</span>{" "}
{new Date(claim.serviceDate).toLocaleDateString()}
</p>
<div>
<span className="text-gray-500">Status:</span>
<Select
value={status}
onValueChange={(value) => setStatus(value as ClaimStatus)}
>
<SelectTrigger className="mt-1 w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="REVIEW">Review</SelectItem>
<SelectItem value="APPROVED">Approved</SelectItem>
<SelectItem value="CANCELLED">Cancelled</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900">Insurance Details</h4>
<div className="mt-2 space-y-2">
<p>
<span className="text-gray-500">Insurance Provider:</span>{" "}
{claim.insuranceProvider || "N/A"}
</p>
<p>
<span className="text-gray-500">Member ID:</span>{" "}
{claim.memberId}
</p>
<p>
<span className="text-gray-500">Remarks:</span>{" "}
{claim.remarks || "N/A"}
</p>
</div>
</div>
</div>
{/* Timestamps */}
<div className="space-y-2">
<h4 className="font-medium text-gray-900">Timestamps</h4>
<p>
<span className="text-gray-500">Created At:</span>{" "}
{formatDateToHumanReadable(claim.createdAt)}
</p>
<p>
<span className="text-gray-500">Updated At:</span>{" "}
{formatDateToHumanReadable(claim.updatedAt)}
</p>
</div>
{/* Staff Info */}
{claim.staff && (
<div className="space-y-2 pt-4">
<h4 className="font-medium text-gray-900">Assigned Staff</h4>
<p>
<span className="text-gray-500">Name:</span> {claim.staff.name}
</p>
<p>
<span className="text-gray-500">Role:</span> {claim.staff.role}
</p>
{claim.staff.email && (
<p>
<span className="text-gray-500">Email:</span>{" "}
{claim.staff.email}
</p>
)}
{claim.staff.phone && (
<p>
<span className="text-gray-500">Phone:</span>{" "}
{claim.staff.phone}
</p>
)}
</div>
)}
{/* Service Lines */}
<div>
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
<div className="mt-2 space-y-3">
{claim.serviceLines.length > 0 ? (
<>
{claim.serviceLines.map((line) => (
<div
key={line.id}
className="border p-3 rounded-md bg-gray-50"
>
<p>
<span className="text-gray-500">Procedure Code:</span>{" "}
{line.procedureCode}
</p>
<p>
<span className="text-gray-500">Procedure Date:</span>{" "}
{new Date(line.procedureDate).toLocaleDateString()}
</p>
{line.oralCavityArea && (
<p>
<span className="text-gray-500">
Oral Cavity Area:
</span>{" "}
{line.oralCavityArea}
</p>
)}
{line.toothNumber && (
<p>
<span className="text-gray-500">Tooth Number:</span>{" "}
{line.toothNumber}
</p>
)}
{line.toothSurface && (
<p>
<span className="text-gray-500">Tooth Surface:</span>{" "}
{line.toothSurface}
</p>
)}
<p>
<span className="text-gray-500">Billed Amount:</span> $
{line.billedAmount.toFixed(2)}
</p>
</div>
))}
<div className="text-right font-semibold text-gray-900 pt-2 border-t mt-4">
Total Billed Amount: $
{claim.serviceLines
.reduce((total, line) => total + line.billedAmount, 0)
.toFixed(2)}
</div>
</>
) : (
<p className="text-gray-500">No service lines available.</p>
)}
</div>
</div>
{/* Actions */}
<div className="flex justify-end space-x-2 pt-4">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave}>Save Changes</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -6,12 +6,17 @@ import {
DialogDescription, DialogDescription,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import {
ClaimUncheckedCreateInputObjectSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import React from "react"; import React from "react";
import { z } from "zod"; import { z } from "zod";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
//creating types out of schema auto generated. //creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>; type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
type ClaimWithServiceLines = Claim & { type ClaimWithServiceLines = Claim & {
serviceLines: { serviceLines: {
@@ -24,6 +29,7 @@ type ClaimWithServiceLines = Claim & {
toothSurface: string | null; toothSurface: string | null;
billedAmount: number; billedAmount: number;
}[]; }[];
staff: Staff | null;
}; };
type ClaimViewModalProps = { type ClaimViewModalProps = {
@@ -44,7 +50,6 @@ export default function ClaimViewModal({
return ( return (
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Claim Details</DialogTitle> <DialogTitle>Claim Details</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -70,10 +75,6 @@ export default function ClaimViewModal({
<div> <div>
<h4 className="font-medium text-gray-900">Basic Information</h4> <h4 className="font-medium text-gray-900">Basic Information</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<p>
<span className="text-gray-500">Member ID:</span>{" "}
{claim.memberId}
</p>
<p> <p>
<span className="text-gray-500">Date of Birth:</span>{" "} <span className="text-gray-500">Date of Birth:</span>{" "}
{new Date(claim.dateOfBirth).toLocaleDateString()} {new Date(claim.dateOfBirth).toLocaleDateString()}
@@ -105,12 +106,16 @@ export default function ClaimViewModal({
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900">Insurance</h4> <h4 className="font-medium text-gray-900">Insurance Details</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<p> <p>
<span className="text-gray-500">Provider:</span>{" "} <span className="text-gray-500">Insurance Provider:</span>{" "}
{claim.insuranceProvider || "N/A"} {claim.insuranceProvider || "N/A"}
</p> </p>
<p>
<span className="text-gray-500">Member ID:</span>{" "}
{claim.memberId}
</p>
<p> <p>
<span className="text-gray-500">Remarks:</span>{" "} <span className="text-gray-500">Remarks:</span>{" "}
{claim.remarks || "N/A"} {claim.remarks || "N/A"}
@@ -119,11 +124,51 @@ export default function ClaimViewModal({
</div> </div>
</div> </div>
{/* Metadata */}
<div className="space-y-2">
<h4 className="font-medium text-gray-900">Timestamps</h4>
<p>
<span className="text-gray-500">Created At:</span>{" "}
{formatDateToHumanReadable(claim.createdAt)}
</p>
<p>
<span className="text-gray-500">Updated At:</span>{" "}
{formatDateToHumanReadable(claim.updatedAt)}
</p>
</div>
{claim.staff && (
<div className="space-y-2 pt-4">
<h4 className="font-medium text-gray-900">Assigned Staff</h4>
<p>
<span className="text-gray-500">Name:</span>{" "}
{claim.staff.name}
</p>
<p>
<span className="text-gray-500">Role:</span>{" "}
{claim.staff.role}
</p>
{claim.staff.email && (
<p>
<span className="text-gray-500">Email:</span>{" "}
{claim.staff.email}
</p>
)}
{claim.staff.phone && (
<p>
<span className="text-gray-500">Phone:</span>{" "}
{claim.staff.phone}
</p>
)}
</div>
)}
<div> <div>
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4> <h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
<div className="mt-2 space-y-3"> <div className="mt-2 space-y-3">
{claim.serviceLines.length > 0 ? ( {claim.serviceLines.length > 0 ? (
claim.serviceLines.map((line, index) => ( <>
{claim.serviceLines.map((line, index) => (
<div <div
key={line.id} key={line.id}
className="border p-3 rounded-md bg-gray-50" className="border p-3 rounded-md bg-gray-50"
@@ -152,16 +197,25 @@ export default function ClaimViewModal({
)} )}
{line.toothSurface && ( {line.toothSurface && (
<p> <p>
<span className="text-gray-500">Tooth Surface:</span>{" "} <span className="text-gray-500">
Tooth Surface:
</span>{" "}
{line.toothSurface} {line.toothSurface}
</p> </p>
)} )}
<p> <p>
<span className="text-gray-500">Billed Amount:</span> $ <span className="text-gray-500">Billed Amount:</span>{" "}
{line.billedAmount.toFixed(2)} ${line.billedAmount.toFixed(2)}
</p> </p>
</div> </div>
)) ))}
<div className="text-right font-semibold text-gray-900 pt-2 border-t mt-4">
Total Billed Amount: $
{claim.serviceLines
.reduce((total, line) => total + line.billedAmount, 0)
.toFixed(2)}
</div>
</>
) : ( ) : (
<p className="text-gray-500">No service lines available.</p> <p className="text-gray-500">No service lines available.</p>
)} )}

View File

@@ -32,6 +32,7 @@ import {
PatientUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema, ClaimUncheckedCreateInputObjectSchema,
ClaimStatusSchema, ClaimStatusSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas"; } from "@repo/db/usedSchemas";
import { z } from "zod"; import { z } from "zod";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -42,10 +43,12 @@ import { cn } from "@/lib/utils";
import { formatDateToHumanReadable } from "@/utils/dateUtils"; import { formatDateToHumanReadable } from "@/utils/dateUtils";
import { Card, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import ClaimViewModal from "./claim-view-modal"; import ClaimViewModal from "./claim-view-modal";
import ClaimEditModal from "./claim-edit-modal";
//creating types out of schema auto generated. //creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>; type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>; export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
type ClaimWithServiceLines = Claim & { type ClaimWithServiceLines = Claim & {
serviceLines: { serviceLines: {
@@ -58,6 +61,7 @@ type ClaimWithServiceLines = Claim & {
toothSurface: string | null; toothSurface: string | null;
billedAmount: number; billedAmount: number;
}[]; }[];
staff: Staff | null;
}; };
const PatientSchema = ( const PatientSchema = (
@@ -140,6 +144,42 @@ export default function ClaimsRecentTable({
placeholderData: { claims: [], totalCount: 0 }, placeholderData: { claims: [], totalCount: 0 },
}); });
const updateClaimMutation = useMutation({
mutationFn: async (claim: ClaimWithServiceLines) => {
const response = await apiRequest("PUT", `/api/claims/${claim.id}`, {
status: claim.status,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to update claim");
}
return response.json();
},
onSuccess: () => {
setIsEditClaimOpen(false);
toast({
title: "Success",
description: "Claim updated successfully!",
variant: "default",
});
queryClient.invalidateQueries({
queryKey: [
"claims-recent",
{
page: currentPage,
},
],
});
},
onError: (error) => {
toast({
title: "Error",
description: `Update failed: ${error.message}`,
variant: "destructive",
});
},
});
const deleteClaimMutation = useMutation({ const deleteClaimMutation = useMutation({
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
await apiRequest("DELETE", `/api/claims/${id}`); await apiRequest("DELETE", `/api/claims/${id}`);
@@ -475,6 +515,18 @@ export default function ClaimsRecentTable({
/> />
)} )}
{isEditClaimOpen && currentClaim && (
<ClaimEditModal
isOpen={isEditClaimOpen}
onClose={() => setIsEditClaimOpen(false)}
onOpenChange={(open) => setIsEditClaimOpen(open)}
claim={currentClaim}
onSave={(updatedClaim) => {
updateClaimMutation.mutate(updatedClaim);
}}
/>
)}
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200"> <div className="bg-white px-4 py-3 border-t border-gray-200">

View File

@@ -80,7 +80,10 @@ export function normalizeToISOString(date: Date | string): string {
* @param dateInput The date as a string (e.g., ISO, YYYY-MM-DD) or a Date object. * @param dateInput The date as a string (e.g., ISO, YYYY-MM-DD) or a Date object.
* @returns A formatted date string. * @returns A formatted date string.
*/ */
export const formatDateToHumanReadable = (dateInput: string | Date): string => { export const formatDateToHumanReadable = (
dateInput?: string | Date
): string => {
if (!dateInput) return "N/A";
// Create a Date object from the input. // Create a Date object from the input.
// The Date constructor is quite flexible with various string formats. // The Date constructor is quite flexible with various string formats.
const date = new Date(dateInput); const date = new Date(dateInput);