ui and minor changes for payment page
This commit is contained in:
@@ -364,7 +364,7 @@ export default function ClaimsRecentTable({
|
||||
)}
|
||||
<TableCell>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
CML-{claim.id!.toString().padStart(4, "0")}
|
||||
CLM-{claim.id!.toString().padStart(4, "0")}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -29,12 +29,16 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
type PaymentEditModalProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onClose: () => void;
|
||||
onEditServiceLine: (payload: NewTransactionPayload) => void;
|
||||
isUpdatingServiceLine?: boolean;
|
||||
onUpdateStatus: (paymentId: number, status: PaymentStatus) => void;
|
||||
isUpdatingStatus?: boolean;
|
||||
payment: PaymentWithExtras | null;
|
||||
};
|
||||
|
||||
@@ -44,9 +48,12 @@ export default function PaymentEditModal({
|
||||
onClose,
|
||||
payment,
|
||||
onEditServiceLine,
|
||||
isUpdatingServiceLine,
|
||||
onUpdateStatus,
|
||||
isUpdatingStatus,
|
||||
}: PaymentEditModalProps) {
|
||||
if (!payment) return null;
|
||||
|
||||
|
||||
const [expandedLineId, setExpandedLineId] = useState<number | null>(null);
|
||||
const [paymentStatus, setPaymentStatus] = React.useState<PaymentStatus>(
|
||||
payment.status
|
||||
@@ -99,7 +106,56 @@ export default function PaymentEditModal({
|
||||
|
||||
const handleSavePayment = async () => {
|
||||
if (!formState.serviceLineId) {
|
||||
toast({ title: "Error", description: "No service line selected." });
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "No service line selected.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const paidAmount = Number(formState.paidAmount) || 0;
|
||||
const adjustedAmount = Number(formState.adjustedAmount) || 0;
|
||||
|
||||
if (paidAmount < 0 || adjustedAmount < 0) {
|
||||
toast({
|
||||
title: "Invalid Amount",
|
||||
description: "Amounts cannot be negative.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (paidAmount === 0 && adjustedAmount === 0) {
|
||||
toast({
|
||||
title: "Invalid Amount",
|
||||
description:
|
||||
"Either paid or adjusted amount must be greater than zero.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const line = payment.claim.serviceLines.find(
|
||||
(sl) => sl.id === formState.serviceLineId
|
||||
);
|
||||
if (!line) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Selected service line not found.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dueAmount = Number(line.totalDue);
|
||||
if (paidAmount > dueAmount) {
|
||||
toast({
|
||||
title: "Invalid Payment",
|
||||
description: `Paid amount ($${paidAmount.toFixed(
|
||||
2
|
||||
)}) cannot exceed due amount ($${dueAmount.toFixed(2)}).`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,70 +170,148 @@ export default function PaymentEditModal({
|
||||
adjustedAmount: Number(formState.adjustedAmount) || 0,
|
||||
method: formState.method,
|
||||
receivedDate: parseLocalDate(formState.receivedDate),
|
||||
payerName: formState.payerName || undefined,
|
||||
notes: formState.notes || undefined,
|
||||
payerName: formState.payerName?.trim() || undefined,
|
||||
notes: formState.notes?.trim() || undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await onEditServiceLine(payload);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Payment Transaction added successfully.",
|
||||
});
|
||||
setExpandedLineId(null);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast({ title: "Error", description: "Failed to save payment." });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayFullDue = async (
|
||||
line: (typeof payment.claim.serviceLines)[0]
|
||||
) => {
|
||||
if (!line || !payment) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Service line or payment data missing.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dueAmount = Number(line.totalDue);
|
||||
if (isNaN(dueAmount) || dueAmount <= 0) {
|
||||
toast({
|
||||
title: "No Due",
|
||||
description: "This service line has no outstanding balance.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: NewTransactionPayload = {
|
||||
paymentId: payment.id,
|
||||
status: paymentStatus,
|
||||
serviceLineTransactions: [
|
||||
{
|
||||
serviceLineId: line.id,
|
||||
paidAmount: dueAmount,
|
||||
adjustedAmount: 0,
|
||||
method: paymentMethodOptions[1] as PaymentMethod, // Maybe make dynamic later
|
||||
receivedDate: new Date(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await onEditServiceLine(payload);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Full due amount ($${dueAmount.toFixed(
|
||||
2
|
||||
)}) paid for ${line.procedureCode}`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update payment.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Payment</DialogTitle>
|
||||
<DialogDescription>
|
||||
View and manage payments applied to service lines.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="relative">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Payment</DialogTitle>
|
||||
<DialogDescription>
|
||||
View and manage payments applied to service lines.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Close button in top-right */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="absolute right-0 top-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Claim + Patient Info */}
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xl font-semibold">
|
||||
<div className="space-y-2 border-b border-gray-200 pb-4">
|
||||
<h3 className="text-2xl font-bold text-gray-900">
|
||||
{payment.claim.patientName}
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
Claim ID: {payment.claimId.toString().padStart(4, "0")}
|
||||
</p>
|
||||
<p className="text-gray-500">
|
||||
Service Date:{" "}
|
||||
{formatDateToHumanReadable(payment.claim.serviceDate)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Notes:</span>{" "}
|
||||
{payment.notes || "N/A"}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||
Claim #{payment.claimId.toString().padStart(4, "0")}
|
||||
</span>
|
||||
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||
Service Date:{" "}
|
||||
{formatDateToHumanReadable(payment.claim.serviceDate)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
||||
{/* Payment Summary + Metadata */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Payment Info */}
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">Payment Info</h4>
|
||||
<div className="mt-2 space-y-1">
|
||||
<h4 className="font-semibold text-gray-900">Payment Info</h4>
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<p>
|
||||
<span className="text-gray-500">Total Billed:</span> $
|
||||
{Number(payment.totalBilled || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Total Billed:</span>{" "}
|
||||
<span className="font-medium">
|
||||
${Number(payment.totalBilled || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Total Paid:</span> $
|
||||
{Number(payment.totalPaid || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Total Paid:</span>{" "}
|
||||
<span className="font-medium text-green-600">
|
||||
${Number(payment.totalPaid || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Total Due:</span> $
|
||||
{Number(payment.totalDue || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Total Due:</span>{" "}
|
||||
<span className="font-medium text-red-600">
|
||||
${Number(payment.totalDue || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<div className="pt-2">
|
||||
<label className="text-sm text-gray-600">Status</label>
|
||||
|
||||
{/* Status Selector */}
|
||||
<div className="pt-3">
|
||||
<label className="block text-sm text-gray-600 mb-1">
|
||||
Payment Status
|
||||
</label>
|
||||
<Select
|
||||
value={paymentStatus}
|
||||
onValueChange={(value: PaymentStatus) =>
|
||||
@@ -196,12 +330,23 @@ export default function PaymentEditModal({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isUpdatingStatus}
|
||||
onClick={() =>
|
||||
payment && onUpdateStatus(payment.id, paymentStatus)
|
||||
}
|
||||
>
|
||||
{isUpdatingStatus ? "Updating..." : "Update Status"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">Metadata</h4>
|
||||
<div className="mt-2 space-y-1">
|
||||
<h4 className="font-semibold text-gray-900">Metadata</h4>
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<p>
|
||||
<span className="text-gray-500">Created At:</span>{" "}
|
||||
{payment.createdAt
|
||||
@@ -209,7 +354,7 @@ export default function PaymentEditModal({
|
||||
: "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Last Upadated At:</span>{" "}
|
||||
<span className="text-gray-500">Last Updated At:</span>{" "}
|
||||
{payment.updatedAt
|
||||
? formatDateToHumanReadable(payment.updatedAt)
|
||||
: "N/A"}
|
||||
@@ -221,176 +366,186 @@ export default function PaymentEditModal({
|
||||
{/* Service Lines Payments */}
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="mt-3 space-y-4">
|
||||
{payment.claim.serviceLines.length > 0 ? (
|
||||
<>
|
||||
{payment.claim.serviceLines.map((line) => {
|
||||
return (
|
||||
<div
|
||||
key={line.id}
|
||||
className="border p-3 rounded-md bg-gray-50"
|
||||
>
|
||||
payment.claim.serviceLines.map((line) => {
|
||||
const isExpanded = expandedLineId === line.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={line.id}
|
||||
className="border border-gray-200 p-4 rounded-xl bg-white shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||
>
|
||||
{/* Top Info */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Procedure Code:</span>{" "}
|
||||
{line.procedureCode}
|
||||
<span className="font-medium">
|
||||
{line.procedureCode}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Billed:</span> $
|
||||
{Number(line.totalBilled || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Billed:</span>{" "}
|
||||
<span className="font-semibold">
|
||||
${Number(line.totalBilled || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Paid:</span> $
|
||||
{Number(line.totalPaid || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Paid:</span>{" "}
|
||||
<span className="font-semibold text-green-600">
|
||||
${Number(line.totalPaid || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Adjusted:</span> $
|
||||
{Number(line.totalAdjusted || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Adjusted:</span>{" "}
|
||||
<span className="font-semibold text-yellow-600">
|
||||
${Number(line.totalAdjusted || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Due:</span> $
|
||||
{Number(line.totalDue || 0).toFixed(2)}
|
||||
<span className="text-gray-500">Due:</span>{" "}
|
||||
<span className="font-semibold text-red-600">
|
||||
${Number(line.totalDue || 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="pt-4 flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditServiceLine(line.id)}
|
||||
>
|
||||
{isExpanded ? "Cancel" : "Pay Partially"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handlePayFullDue(line)}
|
||||
>
|
||||
Pay Full Due
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Expanded Partial Payment Form */}
|
||||
{isExpanded && (
|
||||
<div className="mt-4 p-4 border-t border-gray-200 bg-gray-50 rounded-lg space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Paid Amount
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Paid Amount"
|
||||
defaultValue={formState.paidAmount}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
"paidAmount",
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Adjusted Amount
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Adjusted Amount"
|
||||
defaultValue={formState.adjustedAmount}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
"adjustedAmount",
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Payment Method
|
||||
</label>
|
||||
<Select
|
||||
value={formState.method}
|
||||
onValueChange={(value: PaymentMethod) =>
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
method: value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a payment method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{paymentMethodOptions.map((methodOption) => (
|
||||
<SelectItem
|
||||
key={methodOption}
|
||||
value={methodOption}
|
||||
>
|
||||
{methodOption}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Received Date
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formState.receivedDate}
|
||||
onChange={(e) =>
|
||||
updateField("receivedDate", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Payer Name
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Payer Name"
|
||||
value={formState.payerName}
|
||||
onChange={(e) =>
|
||||
updateField("payerName", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Notes</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Notes"
|
||||
onChange={(e) =>
|
||||
updateField("notes", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditServiceLine(line.id)}
|
||||
disabled={isUpdatingServiceLine}
|
||||
onClick={() => handleSavePayment()}
|
||||
>
|
||||
{expandedLineId === line.id
|
||||
? "Cancel"
|
||||
: "Edit Payments"}
|
||||
{isUpdatingStatus ? "Updating..." : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{expandedLineId === line.id && (
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor={`paid-${line.id}`}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Paid Amount
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Paid Amount"
|
||||
defaultValue={formState.paidAmount}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
"paidAmount",
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor={`adjusted-${line.id}`}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Adjusted Amount
|
||||
</label>
|
||||
<Input
|
||||
id={`adjusted-${line.id}`}
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Adjusted Amount"
|
||||
defaultValue={formState.adjustedAmount}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
"adjustedAmount",
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Payment Method
|
||||
</label>
|
||||
<Select
|
||||
value={formState.method}
|
||||
onValueChange={(value: PaymentMethod) =>
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
method: value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a payment method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{paymentMethodOptions.map((methodOption) => (
|
||||
<SelectItem
|
||||
key={methodOption}
|
||||
value={methodOption}
|
||||
>
|
||||
{methodOption}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Received Date
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formState.receivedDate}
|
||||
onChange={(e) =>
|
||||
updateField("receivedDate", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">
|
||||
Payer Name
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Payer Name"
|
||||
value={formState.payerName}
|
||||
onChange={(e) =>
|
||||
updateField("payerName", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor={`notes-${line.id}`}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Notes
|
||||
</label>
|
||||
<Input
|
||||
id={`notes-${line.id}`}
|
||||
type="text"
|
||||
placeholder="Notes"
|
||||
onChange={(e) =>
|
||||
updateField("notes", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleSavePayment()}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500">No service lines available.</p>
|
||||
)}
|
||||
@@ -400,39 +555,82 @@ export default function PaymentEditModal({
|
||||
{/* Transactions Overview */}
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 pt-6">All Transactions</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="mt-4 space-y-3">
|
||||
{payment.serviceLineTransactions.length > 0 ? (
|
||||
payment.serviceLineTransactions.map((tx) => (
|
||||
<div
|
||||
key={tx.id}
|
||||
className="border p-3 rounded-md bg-white shadow-sm"
|
||||
className="border border-gray-200 p-4 rounded-xl bg-white shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||
>
|
||||
<p>
|
||||
<span className="text-gray-500">Date:</span>{" "}
|
||||
{formatDateToHumanReadable(tx.receivedDate)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Paid Amount:</span> $
|
||||
{Number(tx.paidAmount).toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Adjusted Amount:</span> $
|
||||
{Number(tx.adjustedAmount).toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Method:</span> {tx.method}
|
||||
</p>
|
||||
{tx.payerName && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
|
||||
{/* Transaction ID */}
|
||||
{tx.id && (
|
||||
<p>
|
||||
<span className="text-gray-500">Transaction ID:</span>{" "}
|
||||
<span className="font-medium">{tx.id}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Procedure Code */}
|
||||
{tx.serviceLine?.procedureCode && (
|
||||
<p>
|
||||
<span className="text-gray-500">Procedure Code:</span>{" "}
|
||||
<span className="font-medium">
|
||||
{tx.serviceLine.procedureCode}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Paid Amount */}
|
||||
<p>
|
||||
<span className="text-gray-500">Payer Name:</span>{" "}
|
||||
{tx.payerName}
|
||||
<span className="text-gray-500">Paid Amount:</span>{" "}
|
||||
<span className="font-semibold text-green-600">
|
||||
${Number(tx.paidAmount).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{tx.notes && (
|
||||
|
||||
{/* Adjusted Amount */}
|
||||
{Number(tx.adjustedAmount) > 0 && (
|
||||
<p>
|
||||
<span className="text-gray-500">
|
||||
Adjusted Amount:
|
||||
</span>{" "}
|
||||
<span className="font-semibold text-yellow-600">
|
||||
${Number(tx.adjustedAmount).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<p>
|
||||
<span className="text-gray-500">Notes:</span> {tx.notes}
|
||||
<span className="text-gray-500">Date:</span>{" "}
|
||||
<span>
|
||||
{formatDateToHumanReadable(tx.receivedDate)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Method */}
|
||||
<p>
|
||||
<span className="text-gray-500">Method:</span>{" "}
|
||||
<span className="capitalize">{tx.method}</span>
|
||||
</p>
|
||||
|
||||
{/* Payer Name */}
|
||||
{tx.payerName && tx.payerName.trim() !== "" && (
|
||||
<p className="md:col-span-2">
|
||||
<span className="text-gray-500">Payer Name:</span>{" "}
|
||||
<span>{tx.payerName}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
{tx.notes && tx.notes.trim() !== "" && (
|
||||
<p className="md:col-span-2">
|
||||
<span className="text-gray-500">Notes:</span>{" "}
|
||||
<span>{tx.notes}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
@@ -443,7 +641,7 @@ export default function PaymentEditModal({
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-2 pt-6">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<Button variant="default" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,9 @@ import {
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
TrendingUp,
|
||||
ThumbsDown,
|
||||
DollarSign,
|
||||
} from "lucide-react";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -30,15 +33,15 @@ import {
|
||||
} from "@/components/ui/pagination";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
|
||||
import PaymentViewModal from "./payment-view-modal";
|
||||
import LoadingScreen from "../ui/LoadingScreen";
|
||||
import {
|
||||
ClaimStatus,
|
||||
ClaimWithServiceLines,
|
||||
NewTransactionPayload,
|
||||
PaymentStatus,
|
||||
PaymentWithExtras,
|
||||
} from "@repo/db/types";
|
||||
import EditPaymentModal from "./payment-edit-modal";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
|
||||
interface PaymentApiResponse {
|
||||
payments: PaymentWithExtras[];
|
||||
@@ -47,7 +50,6 @@ interface PaymentApiResponse {
|
||||
|
||||
interface PaymentsRecentTableProps {
|
||||
allowEdit?: boolean;
|
||||
allowView?: boolean;
|
||||
allowDelete?: boolean;
|
||||
allowCheckbox?: boolean;
|
||||
onSelectPayment?: (payment: PaymentWithExtras | null) => void;
|
||||
@@ -57,7 +59,6 @@ interface PaymentsRecentTableProps {
|
||||
|
||||
export default function PaymentsRecentTable({
|
||||
allowEdit,
|
||||
allowView,
|
||||
allowDelete,
|
||||
allowCheckbox,
|
||||
onSelectPayment,
|
||||
@@ -66,7 +67,6 @@ export default function PaymentsRecentTable({
|
||||
}: PaymentsRecentTableProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isViewPaymentOpen, setIsViewPaymentOpen] = useState(false);
|
||||
const [isEditPaymentOpen, setIsEditPaymentOpen] = useState(false);
|
||||
const [isDeletePaymentOpen, setIsDeletePaymentOpen] = useState(false);
|
||||
|
||||
@@ -131,17 +131,25 @@ export default function PaymentsRecentTable({
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsEditPaymentOpen(false);
|
||||
onSuccess: async (updated, { paymentId }) => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Payment updated successfully!",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getPaymentsQueryKey(),
|
||||
});
|
||||
|
||||
// Fetch updated payment and set into local state
|
||||
const refreshedPayment = await apiRequest(
|
||||
"GET",
|
||||
`/api/payments/${paymentId}`
|
||||
).then((res) => res.json());
|
||||
|
||||
setCurrentPayment(refreshedPayment); // <-- keep modal in sync
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -151,6 +159,57 @@ export default function PaymentsRecentTable({
|
||||
},
|
||||
});
|
||||
|
||||
const updatePaymentStatusMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
paymentId,
|
||||
status,
|
||||
}: {
|
||||
paymentId: number;
|
||||
status: PaymentStatus;
|
||||
}) => {
|
||||
const response = await apiRequest(
|
||||
"PATCH",
|
||||
`/api/payments/${paymentId}/status`,
|
||||
{
|
||||
data: { status },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || "Failed to update payment status");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
onSuccess: async (updated, { paymentId }) => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Payment Status updated successfully!",
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getPaymentsQueryKey(),
|
||||
});
|
||||
|
||||
// Fetch updated payment and set into local state
|
||||
const refreshedPayment = await apiRequest(
|
||||
"GET",
|
||||
`/api/payments/${paymentId}`
|
||||
).then((res) => res.json());
|
||||
|
||||
setCurrentPayment(refreshedPayment); // <-- keep modal in sync
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Status update failed: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const deletePaymentMutation = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const res = await apiRequest("DELETE", `/api/payments/${id}`);
|
||||
@@ -182,11 +241,6 @@ export default function PaymentsRecentTable({
|
||||
setIsEditPaymentOpen(true);
|
||||
};
|
||||
|
||||
const handleViewPayment = (payment: PaymentWithExtras) => {
|
||||
setCurrentPayment(payment);
|
||||
setIsViewPaymentOpen(true);
|
||||
};
|
||||
|
||||
const handleDeletePayment = (payment: PaymentWithExtras) => {
|
||||
setCurrentPayment(payment);
|
||||
setIsDeletePaymentOpen(true);
|
||||
@@ -231,7 +285,7 @@ export default function PaymentsRecentTable({
|
||||
paymentsData?.totalCount || 0
|
||||
);
|
||||
|
||||
const getInitialsFromName = (fullName: string) => {
|
||||
const getInitials = (fullName: string) => {
|
||||
const parts = fullName.trim().split(/\s+/);
|
||||
const filteredParts = parts.filter((part) => part.length > 0);
|
||||
if (filteredParts.length === 0) {
|
||||
@@ -260,7 +314,7 @@ export default function PaymentsRecentTable({
|
||||
return colorClasses[id % colorClasses.length];
|
||||
};
|
||||
|
||||
const getStatusInfo = (status?: ClaimStatus) => {
|
||||
const getStatusInfo = (status?: PaymentStatus) => {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return {
|
||||
@@ -268,22 +322,35 @@ export default function PaymentsRecentTable({
|
||||
color: "bg-yellow-100 text-yellow-800",
|
||||
icon: <Clock className="h-3 w-3 mr-1" />,
|
||||
};
|
||||
case "APPROVED":
|
||||
case "PARTIALLY_PAID":
|
||||
return {
|
||||
label: "Approved",
|
||||
label: "Partially Paid",
|
||||
color: "bg-blue-100 text-blue-800",
|
||||
icon: <DollarSign className="h-3 w-3 mr-1" />,
|
||||
};
|
||||
case "PAID":
|
||||
return {
|
||||
label: "Paid",
|
||||
color: "bg-green-100 text-green-800",
|
||||
icon: <CheckCircle className="h-3 w-3 mr-1" />,
|
||||
};
|
||||
case "CANCELLED":
|
||||
case "OVERPAID":
|
||||
return {
|
||||
label: "Cancelled",
|
||||
label: "Overpaid",
|
||||
color: "bg-purple-100 text-purple-800",
|
||||
icon: <TrendingUp className="h-3 w-3 mr-1" />,
|
||||
};
|
||||
case "DENIED":
|
||||
return {
|
||||
label: "Denied",
|
||||
color: "bg-red-100 text-red-800",
|
||||
icon: <AlertCircle className="h-3 w-3 mr-1" />,
|
||||
icon: <ThumbsDown className="h-3 w-3 mr-1" />,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: status
|
||||
? status.charAt(0).toUpperCase() + status.slice(1)
|
||||
? (status as string).charAt(0).toUpperCase() +
|
||||
(status as string).slice(1).toLowerCase()
|
||||
: "Unknown",
|
||||
color: "bg-gray-100 text-gray-800",
|
||||
icon: <AlertCircle className="h-3 w-3 mr-1" />,
|
||||
@@ -318,10 +385,11 @@ export default function PaymentsRecentTable({
|
||||
<TableRow>
|
||||
{allowCheckbox && <TableHead>Select</TableHead>}
|
||||
<TableHead>Payment ID</TableHead>
|
||||
<TableHead>Claim ID</TableHead>
|
||||
<TableHead>Patient Name</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead>Claim Submitted on</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -374,12 +442,39 @@ export default function PaymentsRecentTable({
|
||||
? `PAY-${payment.id.toString().padStart(4, "0")}`
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>{payment.patientName}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{typeof payment.claimId === "number"
|
||||
? `CLM-${payment.claimId.toString().padStart(4, "0")}`
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className={`h-10 w-10 ${getAvatarColor(Number(payment.id))}`}
|
||||
>
|
||||
<AvatarFallback className="text-white">
|
||||
{getInitials(payment.patientName)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{payment.patientName}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
PID-{payment.patientId?.toString().padStart(4, "0")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* 💰 Billed / Paid / Due breakdown */}
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>
|
||||
<strong>Total Billed:</strong> $
|
||||
<strong>Total Billed:</strong> $
|
||||
{Number(totalBilled).toFixed(2)}
|
||||
</span>
|
||||
<span>
|
||||
@@ -400,7 +495,27 @@ export default function PaymentsRecentTable({
|
||||
<TableCell>
|
||||
{formatDateToHumanReadable(payment.paymentDate)}
|
||||
</TableCell>
|
||||
<TableCell>{payment.paymentMethod}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const { label, color, icon } = getStatusInfo(
|
||||
payment.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 && (
|
||||
@@ -428,18 +543,6 @@ export default function PaymentsRecentTable({
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{allowView && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
handleViewPayment(payment);
|
||||
}}
|
||||
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -457,17 +560,6 @@ export default function PaymentsRecentTable({
|
||||
entityName={`ClaimID : ${currentPayment?.claimId}`}
|
||||
/>
|
||||
|
||||
{/* /will hanlde both modal later */}
|
||||
{/* {isViewPaymentOpen && currentPayment && (
|
||||
<ClaimPaymentModal
|
||||
isOpen={isViewPaymentOpen}
|
||||
onClose={() => setIsViewPaymentOpen(false)}
|
||||
onOpenChange={(open) => setIsViewPaymentOpen(open)}
|
||||
onEditClaim={(payment) => handleEditPayment(payment)}
|
||||
payment={currentPayment}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
{isEditPaymentOpen && currentPayment && (
|
||||
<EditPaymentModal
|
||||
isOpen={isEditPaymentOpen}
|
||||
@@ -477,6 +569,11 @@ export default function PaymentsRecentTable({
|
||||
onEditServiceLine={(updatedPayment) => {
|
||||
updatePaymentMutation.mutate(updatedPayment);
|
||||
}}
|
||||
isUpdatingServiceLine={updatePaymentMutation.isPending}
|
||||
onUpdateStatus={(paymentId, status) => {
|
||||
updatePaymentStatusMutation.mutate({ paymentId, status });
|
||||
}}
|
||||
isUpdatingStatus={updatePaymentStatusMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user