initial commit
This commit is contained in:
369
apps/Frontend/src/components/claims/claim-view-modal.tsx
Executable file
369
apps/Frontend/src/components/claims/claim-view-modal.tsx
Executable file
@@ -0,0 +1,369 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React from "react";
|
||||
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
||||
import { ClaimFileMeta, ClaimWithServiceLines } from "@repo/db/types";
|
||||
import { FileText, Paperclip } from "lucide-react";
|
||||
import {
|
||||
safeParseMissingTeeth,
|
||||
splitTeeth,
|
||||
ToothChip,
|
||||
toStatusLabel,
|
||||
} from "./tooth-ui";
|
||||
|
||||
type ClaimViewModalProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onClose: () => void;
|
||||
claim: ClaimWithServiceLines | null;
|
||||
onEditClaim: (claim: ClaimWithServiceLines) => void;
|
||||
};
|
||||
|
||||
export default function ClaimViewModal({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onClose,
|
||||
claim,
|
||||
onEditClaim,
|
||||
}: ClaimViewModalProps) {
|
||||
// Normalizer: supports both ClaimFile[] and nested-create shape { create: ClaimFile[] }
|
||||
const getClaimFilesArray = (
|
||||
c: ClaimWithServiceLines | null
|
||||
): ClaimFileMeta[] => {
|
||||
if (!c) return [];
|
||||
|
||||
// If it's already a plain array (runtime from Prisma include), return it
|
||||
const maybeFiles = (c as any).claimFiles;
|
||||
if (!maybeFiles) return [];
|
||||
|
||||
if (Array.isArray(maybeFiles)) {
|
||||
// ensure each item has filename field (best-effort)
|
||||
return maybeFiles.map((f: any) => ({
|
||||
id: f?.id,
|
||||
filename: String(f?.filename ?? ""),
|
||||
mimeType: f?.mimeType ?? f?.mime ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
// Nested-create shape: { create: [...] }
|
||||
if (maybeFiles && Array.isArray(maybeFiles.create)) {
|
||||
return maybeFiles.create.map((f: any) => ({
|
||||
id: f?.id,
|
||||
filename: String(f?.filename ?? ""),
|
||||
mimeType: f?.mimeType ?? f?.mime ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
// No recognized shape -> empty
|
||||
return [];
|
||||
};
|
||||
|
||||
const claimFiles = getClaimFilesArray(claim);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Claim Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detailed view of the selected claim.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{claim && (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<p className="text-gray-500">
|
||||
Claim No: {claim.claimNumber || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>{" "}
|
||||
{formatDateToHumanReadable(claim.dateOfBirth)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Service Date:</span>{" "}
|
||||
{formatDateToHumanReadable(claim.serviceDate)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Status:</span>{" "}
|
||||
<span
|
||||
className={`font-medium ${
|
||||
claim.status === "APPROVED"
|
||||
? "text-green-600"
|
||||
: claim.status === "CANCELLED"
|
||||
? "text-red-600"
|
||||
: claim.status === "REVIEW"
|
||||
? "text-yellow-600"
|
||||
: "text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{claim?.status
|
||||
? claim.status.charAt(0).toUpperCase() +
|
||||
claim.status.slice(1).toLowerCase()
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</p>
|
||||
</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">Claim Number:</span>{" "}
|
||||
{claim.claimNumber || "—"}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
{/* 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>{" "}
|
||||
{formatDateToHumanReadable(line.procedureDate)}
|
||||
</p>
|
||||
{line.quad && (
|
||||
<p>
|
||||
<span className="text-gray-500">Quad:</span>{" "}
|
||||
{line.quad}
|
||||
</p>
|
||||
)}
|
||||
{line.arch && (
|
||||
<p>
|
||||
<span className="text-gray-500">Arch:</span>{" "}
|
||||
{line.arch}
|
||||
</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>{" "}
|
||||
${Number(line.totalBilled).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 + Number(line.totalBilled || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(2)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-gray-500">No service lines available.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Missing Teeth */}
|
||||
<div className="space-y-2 pt-4">
|
||||
<h4 className="font-medium text-gray-900">Missing Teeth</h4>
|
||||
|
||||
<p>
|
||||
<span className="text-gray-500">Status:</span>{" "}
|
||||
{toStatusLabel((claim as any).missingTeethStatus)}
|
||||
</p>
|
||||
|
||||
{/* Only show details when the user chose "Specify Missing" */}
|
||||
{(claim as any).missingTeethStatus === "Yes_missing" &&
|
||||
(() => {
|
||||
const map = safeParseMissingTeeth(
|
||||
(claim as any).missingTeeth
|
||||
);
|
||||
const { permanent, primary } = splitTeeth(map);
|
||||
const hasAny = permanent.length > 0 || primary.length > 0;
|
||||
|
||||
if (!hasAny) {
|
||||
return (
|
||||
<p className="text-gray-500">
|
||||
No specific teeth marked as missing.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2 space-y-3">
|
||||
{permanent.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-600 mb-2">
|
||||
Permanent
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{permanent.map((t) => (
|
||||
<ToothChip key={t.name} name={t.name} v={t.v} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{primary.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-600 mb-2">
|
||||
Primary
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{primary.map((t) => (
|
||||
<ToothChip key={t.name} name={t.name} v={t.v} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{(claim as any).missingTeethStatus === "endentulous" && (
|
||||
<p className="text-sm text-gray-700">Patient is edentulous.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Claim Files (metadata) */}
|
||||
<div className="pt-4">
|
||||
<h4 className="font-medium text-gray-900 flex items-center space-x-2">
|
||||
<Paperclip className="w-4 h-4 inline-block" />
|
||||
<span>Attached Files</span>
|
||||
</h4>
|
||||
|
||||
{claimFiles.length > 0 ? (
|
||||
<ul className="mt-3 space-y-2">
|
||||
{claimFiles.map((f) => (
|
||||
<li
|
||||
key={f.id ?? f.filename}
|
||||
className="flex items-center justify-between border rounded-md p-3 bg-white"
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<FileText className="w-5 h-5 text-gray-500 mt-1" />
|
||||
<div>
|
||||
<div className="font-medium">{f.filename}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{f.mimeType || "unknown"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mt-2 text-gray-500">No files attached.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
onEditClaim(claim);
|
||||
}}
|
||||
>
|
||||
Edit Claim
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user