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;
billedAmount: number;
}[];
staff: Staff | null;
};
// Pdf types:
@@ -538,7 +539,7 @@ export const storage: IStorage = {
orderBy: { createdAt: "desc" },
skip: offset,
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,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {
ClaimUncheckedCreateInputObjectSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import React from "react";
import { z } from "zod";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
//creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
type ClaimWithServiceLines = Claim & {
serviceLines: {
@@ -24,6 +29,7 @@ type ClaimWithServiceLines = Claim & {
toothSurface: string | null;
billedAmount: number;
}[];
staff: Staff | null;
};
type ClaimViewModalProps = {
@@ -43,8 +49,7 @@ export default function ClaimViewModal({
}: ClaimViewModalProps) {
return (
<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>
<DialogTitle>Claim Details</DialogTitle>
<DialogDescription>
@@ -70,10 +75,6 @@ export default function ClaimViewModal({
<div>
<h4 className="font-medium text-gray-900">Basic Information</h4>
<div className="mt-2 space-y-2">
<p>
<span className="text-gray-500">Member ID:</span>{" "}
{claim.memberId}
</p>
<p>
<span className="text-gray-500">Date of Birth:</span>{" "}
{new Date(claim.dateOfBirth).toLocaleDateString()}
@@ -105,12 +106,16 @@ export default function ClaimViewModal({
</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">
<p>
<span className="text-gray-500">Provider:</span>{" "}
<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"}
@@ -119,49 +124,98 @@ export default function ClaimViewModal({
</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>
<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, index) => (
<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 && (
<>
{claim.serviceLines.map((line, index) => (
<div
key={line.id}
className="border p-3 rounded-md bg-gray-50"
>
<p>
<span className="text-gray-500">
Oral Cavity Area:
</span>{" "}
{line.oralCavityArea}
<span className="text-gray-500">Procedure Code:</span>{" "}
{line.procedureCode}
</p>
)}
{line.toothNumber && (
<p>
<span className="text-gray-500">Tooth Number:</span>{" "}
{line.toothNumber}
<span className="text-gray-500">Procedure Date:</span>{" "}
{new Date(line.procedureDate).toLocaleDateString()}
</p>
)}
{line.toothSurface && (
{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">Tooth Surface:</span>{" "}
{line.toothSurface}
<span className="text-gray-500">Billed Amount:</span>{" "}
${line.billedAmount.toFixed(2)}
</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>
)}

View File

@@ -32,6 +32,7 @@ import {
PatientUncheckedCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema,
ClaimStatusSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
import { useAuth } from "@/hooks/use-auth";
@@ -42,10 +43,12 @@ import { cn } from "@/lib/utils";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import ClaimViewModal from "./claim-view-modal";
import ClaimEditModal from "./claim-edit-modal";
//creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
type ClaimWithServiceLines = Claim & {
serviceLines: {
@@ -58,6 +61,7 @@ type ClaimWithServiceLines = Claim & {
toothSurface: string | null;
billedAmount: number;
}[];
staff: Staff | null;
};
const PatientSchema = (
@@ -140,6 +144,42 @@ export default function ClaimsRecentTable({
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({
mutationFn: async (id: number) => {
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 */}
{totalPages > 1 && (
<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.
* @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.
// The Date constructor is quite flexible with various string formats.
const date = new Date(dateInput);