Files
DentalManagement09/apps/Frontend/src/components/claims/claim-edit-modal.jsx
T
GiteadandClaude Sonnet 4.6 1edf73fdc8 feat: add new frontend components, MH batch worker, and gitignore rules
- Add all new Frontend source files (pages, components, hooks, utils)
- Add selenium_MHBatchPaymentCheckWorker.py and MHSinglePaymentCheckWorker.py
- Add install-steps-5-13.sh setup script
- Update .gitignore to exclude runtime/sensitive data (backups, uploads,
  chat-history, keys, downloads, generated .d.ts files) while keeping folders
- Add .gitkeep to preserve empty runtime folders in git

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 00:23:43 -04:00

272 lines
12 KiB
React

import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
import React, { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { safeParseMissingTeeth, splitTeeth, ToothChip, toStatusLabel, } from "./tooth-ui";
export default function ClaimEditModal({ isOpen, onOpenChange, onClose, claim, onSave, }) {
const [status, setStatus] = useState(claim?.status ?? "PENDING");
const [claimNumber, setClaimNumber] = useState(claim?.claimNumber ?? "");
const [selectedNpiProviderId, setSelectedNpiProviderId] = useState(claim?.npiProviderId ?? null);
const { data: npiProviders = [] } = useQuery({
queryKey: ["/api/npiProviders/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/npiProviders/");
return res.json();
},
});
// Default to Mary Scannell (or first provider) only when no provider is set
useEffect(() => {
if (!npiProviders.length)
return;
if (selectedNpiProviderId !== null)
return;
const mary = npiProviders.find((p) => p.providerName.toLowerCase().includes("mary scannell"));
const fallback = mary ?? npiProviders[0];
if (fallback)
setSelectedNpiProviderId(fallback.id);
}, [npiProviders]);
if (!claim)
return null;
const handleSave = () => {
const updatedClaim = {
...claim,
status,
claimNumber: claimNumber.trim() || null,
npiProviderId: selectedNpiProviderId,
npiProvider: npiProviders.find((p) => p.id === selectedNpiProviderId) ?? null,
};
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>
<p className="text-gray-500">
Claim No: {claimNumber || "—"}
</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>{" "}
{formatDateToHumanReadable(claim.dateOfBirth)}
</p>
<p>
<span className="text-gray-500">Service Date:</span>{" "}
{formatDateToHumanReadable(claim.serviceDate)}
</p>
<div>
<span className="text-gray-500">Status:</span>
<Select value={status} onValueChange={(value) => setStatus(value)}>
<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>
<span className="text-gray-500">Rendering Provider:</span>
<Select value={selectedNpiProviderId?.toString() ?? ""} onValueChange={(val) => setSelectedNpiProviderId(Number(val))}>
<SelectTrigger className="mt-1 w-full">
<SelectValue placeholder="Select Provider"/>
</SelectTrigger>
<SelectContent>
{npiProviders.map((p) => (<SelectItem key={p.id} value={p.id.toString()}>
{p.npiNumber} — {p.providerName}
</SelectItem>))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900">Insurance Details</h4>
<div className="mt-2 space-y-2">
<div>
<span className="text-gray-500">Claim Number:</span>
<Input className="mt-1" value={claimNumber} onChange={(e) => setClaimNumber(e.target.value)} placeholder="Enter claim number"/>
</div>
<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>{" "}
{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) => {
const billed = line.totalBilled
? parseFloat(line.totalBilled)
: 0;
return total + billed;
}, 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.missingTeethStatus)}
</p>
{/* Only show details when the user chose "Specify Missing" */}
{claim.missingTeethStatus === "Yes_missing" &&
(() => {
const map = safeParseMissingTeeth(claim.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.missingTeethStatus === "endentulous" && (<p className="text-sm text-gray-700">Patient is edentulous.</p>)}
</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>);
}