Files
DentalManagementMH06/apps/Frontend/src/components/payments/payments-recent-table.tsx
Gitead c5af6c1fa6 fix: revert payment amount when switching away from Paid in Full
Switching from PAID to Pending/Partially Paid now calls revert-full-claim
first to undo the collected amount, then updates the status — so the
report totals stay accurate.

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

1061 lines
37 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useMemo } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import {
Edit,
Eye,
Delete,
Clock,
CheckCircle,
AlertCircle,
TrendingUp,
ThumbsDown,
DollarSign,
Ban,
Paperclip,
} from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { Checkbox } from "@/components/ui/checkbox";
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import LoadingScreen from "../ui/LoadingScreen";
import {
NewTransactionPayload,
PaymentStatus,
PaymentWithExtras,
} from "@repo/db/types";
import EditPaymentModal from "./payment-edit-modal";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { ConfirmationDialog } from "../ui/confirmationDialog";
import { getPageNumbers } from "@/utils/pageNumberGenerator";
interface PaymentApiResponse {
payments: PaymentWithExtras[];
totalCount: number;
}
interface PaymentsRecentTableProps {
allowEdit?: boolean;
allowDelete?: boolean;
allowCheckbox?: boolean;
onSelectPayment?: (payment: PaymentWithExtras | null) => void;
onPageChange?: (page: number) => void;
patientId?: number;
}
// 🔑 exported base key (so others can invalidate all pages/filters)
export const QK_PAYMENTS_RECENT_BASE = ["payments-recent"] as const;
// 🔑 exported helper for specific pages/scopes
export const qkPaymentsRecent = (opts: {
patientId?: number | null;
page: number;
}) =>
opts.patientId
? ([
...QK_PAYMENTS_RECENT_BASE,
"patient",
opts.patientId,
opts.page,
] as const)
: ([...QK_PAYMENTS_RECENT_BASE, "global", opts.page] as const);
export default function PaymentsRecentTable({
allowEdit,
allowDelete,
allowCheckbox,
onSelectPayment,
onPageChange,
patientId,
}: PaymentsRecentTableProps) {
const { toast } = useToast();
const [isEditPaymentOpen, setIsEditPaymentOpen] = useState(false);
const [isDeletePaymentOpen, setIsDeletePaymentOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const paymentsPerPage = 5;
const offset = (currentPage - 1) * paymentsPerPage;
const [currentPayment, setCurrentPayment] = useState<
PaymentWithExtras | undefined
>(undefined);
const [selectedPaymentId, setSelectedPaymentId] = useState<number | null>(null);
const [checkedPaymentIds, setCheckedPaymentIds] = useState<Set<number>>(new Set());
const [isMhChecking, setIsMhChecking] = useState(false);
const [editingMhPaidId, setEditingMhPaidId] = useState<number | null>(null);
const [editingMhPaidValue, setEditingMhPaidValue] = useState<string>("");
const [isRevertOpen, setIsRevertOpen] = useState(false);
const [revertPaymentId, setRevertPaymentId] = useState<number | null>(null);
const handleSelectPayment = (payment: PaymentWithExtras) => {
const isSelected = selectedPaymentId === payment.id;
const newSelectedId = isSelected ? null : payment.id;
setSelectedPaymentId(Number(newSelectedId));
if (onSelectPayment) {
onSelectPayment(isSelected ? null : payment);
}
};
const handleToggleCheck = (paymentId: number) => {
setCheckedPaymentIds((prev) => {
const next = new Set(prev);
if (next.has(paymentId)) {
next.delete(paymentId);
} else {
next.add(paymentId);
}
return next;
});
};
const queryKey = qkPaymentsRecent({
patientId: patientId ?? undefined,
page: currentPage,
});
const {
data: paymentsData,
isLoading,
isError,
} = useQuery<PaymentApiResponse>({
queryKey,
queryFn: async () => {
const endpoint = patientId
? `/api/payments/patient/${patientId}?limit=${paymentsPerPage}&offset=${offset}`
: `/api/payments/recent?limit=${paymentsPerPage}&offset=${offset}`;
const res = await apiRequest("GET", endpoint);
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.message || "Failed to fetch payments");
}
return res.json();
},
placeholderData: { payments: [], totalCount: 0 },
});
const currentPageIds = (paymentsData?.payments ?? []).map((p) => p.id);
const allOnPageChecked =
currentPageIds.length > 0 &&
currentPageIds.every((id) => checkedPaymentIds.has(id));
const someOnPageChecked =
!allOnPageChecked && currentPageIds.some((id) => checkedPaymentIds.has(id));
const handleToggleAll = () => {
setCheckedPaymentIds((prev) => {
const next = new Set(prev);
if (allOnPageChecked) {
currentPageIds.forEach((id) => next.delete(id));
} else {
currentPageIds.forEach((id) => next.add(id));
}
return next;
});
};
const updatePaymentMutation = useMutation({
mutationFn: async (data: NewTransactionPayload) => {
const response = await apiRequest(
"PUT",
`/api/payments/${data.paymentId}`,
{
data: data,
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to update Payment");
}
return response.json();
},
onSuccess: async (updated, { paymentId }) => {
toast({
title: "Success",
description: "Payment updated successfully!",
});
// 🔄 refresh this table page
await queryClient.invalidateQueries({
queryKey: QK_PAYMENTS_RECENT_BASE,
});
// 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: `Update failed: ${error.message}`,
variant: "destructive",
});
},
});
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!",
});
await queryClient.invalidateQueries({
queryKey: QK_PAYMENTS_RECENT_BASE,
});
// 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 fullPaymentMutation = useMutation({
mutationFn: async ({
paymentId,
type,
}: {
paymentId: number;
type: "pay" | "revert";
}) => {
const endpoint =
type === "pay"
? `/api/payments/${paymentId}/pay-absolute-full-claim`
: `/api/payments/${paymentId}/revert-full-claim`;
const response = await apiRequest("PUT", endpoint);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to update Payment");
}
return response.json();
},
onSuccess: async () => {
toast({
title: "Success",
description: "Payment updated successfully!",
});
await queryClient.invalidateQueries({
queryKey: QK_PAYMENTS_RECENT_BASE,
});
},
onError: (error: any) => {
toast({
title: "Error",
description: `Operation failed: ${error.message}`,
variant: "destructive",
});
},
});
const handlePayAbsoluteFullDue = (paymentId: number) => {
fullPaymentMutation.mutate({ paymentId, type: "pay" });
};
const handleRevert = () => {
if (!revertPaymentId) return;
fullPaymentMutation.mutate({
paymentId: revertPaymentId,
type: "revert",
});
setRevertPaymentId(null);
setIsRevertOpen(false);
};
const deletePaymentMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/payments/${id}`);
return;
},
onSuccess: async () => {
setIsDeletePaymentOpen(false);
await queryClient.invalidateQueries({
queryKey: QK_PAYMENTS_RECENT_BASE,
});
toast({
title: "Deleted",
description: "Payment deleted successfully",
variant: "default",
});
},
onError: (error) => {
toast({
title: "Error",
description: `Failed to delete payment: ${error.message}`,
variant: "destructive",
});
},
});
const handleEditPayment = (payment: PaymentWithExtras) => {
setCurrentPayment(payment);
setIsEditPaymentOpen(true);
};
const handleDeletePayment = (payment: PaymentWithExtras) => {
setCurrentPayment(payment);
setIsDeletePaymentOpen(true);
};
const handleConfirmDeletePayment = async () => {
if (currentPayment) {
if (typeof currentPayment.id === "number") {
deletePaymentMutation.mutate(currentPayment.id);
} else {
toast({
title: "Error",
description: "Selected Payment is missing an ID for deletion.",
variant: "destructive",
});
}
} else {
toast({
title: "Error",
description: "No Payment selected for deletion.",
variant: "destructive",
});
}
};
//VOID and UNVOID Feature
const handleVoid = (paymentId: number) => {
updatePaymentStatusMutation.mutate({ paymentId, status: "VOID" });
};
const handleUnvoid = (paymentId: number) => {
updatePaymentStatusMutation.mutate({ paymentId, status: "PENDING" });
};
const [isVoidOpen, setIsVoidOpen] = useState(false);
const [voidPaymentId, setVoidPaymentId] = useState<number | null>(null);
const [isUnvoidOpen, setIsUnvoidOpen] = useState(false);
const [unvoidPaymentId, setUnvoidPaymentId] = useState<number | null>(null);
const handleConfirmVoid = () => {
if (!voidPaymentId) return;
handleVoid(voidPaymentId);
setVoidPaymentId(null);
setIsVoidOpen(false);
};
const handleConfirmUnvoid = () => {
if (!unvoidPaymentId) return;
handleUnvoid(unvoidPaymentId);
setUnvoidPaymentId(null);
setIsUnvoidOpen(false);
};
// Pagination
useEffect(() => {
if (onPageChange) onPageChange(currentPage);
}, [currentPage, onPageChange]);
useEffect(() => {
setCurrentPage(1);
}, [patientId]);
const totalPages = useMemo(
() => Math.ceil((paymentsData?.totalCount || 0) / paymentsPerPage),
[paymentsData?.totalCount, paymentsPerPage]
);
const startItem = offset + 1;
const endItem = Math.min(
offset + paymentsPerPage,
paymentsData?.totalCount || 0
);
const getName = (p: PaymentWithExtras) =>
p.patient
? `${p.patient.firstName} ${p.patient.lastName}`.trim()
: (p.patientName ?? "Unknown");
const getInitials = (fullName: string) => {
const parts = fullName.trim().split(/\s+/);
const filteredParts = parts.filter((part) => part.length > 0);
if (filteredParts.length === 0) {
return "";
}
const firstInitial = filteredParts[0]!.charAt(0).toUpperCase();
if (filteredParts.length === 1) {
return firstInitial;
} else {
const lastInitial =
filteredParts[filteredParts.length - 1]!.charAt(0).toUpperCase();
return firstInitial + lastInitial;
}
};
const getAvatarColor = (id: number) => {
const colorClasses = [
"bg-blue-500",
"bg-teal-500",
"bg-amber-500",
"bg-rose-500",
"bg-indigo-500",
"bg-green-500",
"bg-purple-500",
];
return colorClasses[id % colorClasses.length];
};
const getStatusInfo = (status?: PaymentStatus) => {
switch (status) {
case "PENDING":
return {
label: "Pending",
color: "bg-red-100 text-red-800",
icon: <Clock className="h-3 w-3 mr-1" />,
};
case "PARTIALLY_PAID":
return {
label: "Partially Paid",
color: "bg-blue-100 text-blue-800",
icon: <DollarSign className="h-3 w-3 mr-1" />,
};
case "PAID":
return {
label: "Paid in Full",
color: "bg-teal-100 text-teal-800",
icon: <CheckCircle className="h-3 w-3 mr-1" />,
};
case "OVERPAID":
return {
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: <ThumbsDown className="h-3 w-3 mr-1" />,
};
case "VOID":
return {
label: "Void",
color: "bg-gray-100 text-gray-800",
icon: <Ban className="h-3 w-3 mr-1" />,
};
default:
return {
label: status
? (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" />,
};
}
};
return (
<div className="bg-white shadow rounded-lg overflow-hidden">
{/* Check MH Payment action bar */}
{allowCheckbox && checkedPaymentIds.size > 0 && (
<div className="flex items-center gap-3 px-4 py-2 bg-blue-50 border-b border-blue-200">
<span className="text-sm text-blue-700 font-medium">
{checkedPaymentIds.size} record{checkedPaymentIds.size > 1 ? "s" : ""} selected
</span>
<Button
size="sm"
variant="default"
disabled={isMhChecking}
onClick={async () => {
setIsMhChecking(true);
let successCount = 0;
let failCount = 0;
for (const paymentId of checkedPaymentIds) {
try {
const res = await apiRequest(
"PATCH",
`/api/payments/${paymentId}/mh-payment-check`
);
if (res.ok) {
successCount++;
} else {
const err = await res.json();
console.error(`MH check failed for payment ${paymentId}:`, err.message);
failCount++;
}
} catch (e) {
console.error(`MH check error for payment ${paymentId}:`, e);
failCount++;
}
}
setIsMhChecking(false);
setCheckedPaymentIds(new Set());
await queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE });
if (failCount === 0) {
toast({ title: "MH Payment Check Complete", description: `${successCount} record(s) updated.` });
} else {
toast({
title: "MH Payment Check Done",
description: `${successCount} succeeded, ${failCount} failed. Check credentials or claim numbers.`,
variant: "destructive",
});
}
}}
>
{isMhChecking ? "Checking..." : "Check MH Payment"}
</Button>
<Button
size="sm"
variant="ghost"
className="text-blue-600"
onClick={() => setCheckedPaymentIds(new Set())}
>
Clear
</Button>
</div>
)}
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
{allowCheckbox && (
<TableHead className="w-10">
<Checkbox
checked={allOnPageChecked}
data-state={someOnPageChecked ? "indeterminate" : undefined}
onCheckedChange={handleToggleAll}
aria-label="Select all on page"
/>
</TableHead>
)}
<TableHead>Claim No.</TableHead>
<TableHead>Patient Name</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Service Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Attachments</TableHead>
<TableHead>MH Paid</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead>Payment ID</TableHead>
<TableHead>Claim ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-muted-foreground"
>
<LoadingScreen />
</TableCell>
</TableRow>
) : isError ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-red-500"
>
Error loading payments.
</TableCell>
</TableRow>
) : (paymentsData?.payments?.length ?? 0) === 0 ? (
<TableRow>
<TableCell
colSpan={8}
className="text-center py-8 text-muted-foreground"
>
No payments found
</TableCell>
</TableRow>
) : (
paymentsData?.payments.map((payment) => {
const totalBilled = Number(payment.totalBilled || 0);
const totalPaid = Number(payment.totalPaid || 0);
const totalDue = Number(payment.totalDue || 0);
const displayName = getName(payment);
const submittedOn =
payment.serviceLines?.[0]?.procedureDate ??
payment.claim?.createdAt ??
payment.createdAt ??
payment.serviceLineTransactions?.[0]?.receivedDate ??
null;
return (
<TableRow key={payment.id}>
{allowCheckbox && (
<TableCell className="w-10">
<Checkbox
checked={checkedPaymentIds.has(payment.id)}
onCheckedChange={() => handleToggleCheck(payment.id)}
aria-label={`Select payment ${payment.id}`}
/>
</TableCell>
)}
<TableCell>
<span className="text-sm font-mono">
{payment.claim?.claimNumber ?? <span className="text-gray-400"></span>}
</span>
</TableCell>
<TableCell>
<div className="flex items-center">
<Avatar
className={`h-10 w-10 ${getAvatarColor(Number(payment.id))}`}
>
<AvatarFallback className="text-white">
{getInitials(displayName)}
</AvatarFallback>
</Avatar>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{displayName}
</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> $
{Number(totalBilled).toFixed(2)}
</span>
<span>
<strong>Total Paid:</strong> ${totalPaid.toFixed(2)}
</span>
<span>
<strong>Total Due:</strong>{" "}
{totalDue > 0 ? (
<span className="text-yellow-600">
${totalDue.toFixed(2)}
</span>
) : (
<span className="text-green-600">Settled</span>
)}
</span>
</div>
</TableCell>
<TableCell>
{formatDateToHumanReadable(submittedOn)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{(() => {
const { label, color, icon } = getStatusInfo(
payment.status
);
const switchableStatuses: PaymentStatus[] = [
"PENDING",
"PARTIALLY_PAID",
"PAID",
];
const isSwitchable = switchableStatuses.includes(
payment.status as PaymentStatus
);
if (isSwitchable) {
return (
<select
value={payment.status ?? ""}
onChange={(e) => {
const val = e.target.value as PaymentStatus;
if (val === "PAID") {
handlePayAbsoluteFullDue(payment.id);
} else if (payment.status === "PAID") {
// revert the full payment amount, then set the new status
fullPaymentMutation.mutate(
{ paymentId: payment.id, type: "revert" },
{
onSuccess: () => {
updatePaymentStatusMutation.mutate({
paymentId: payment.id,
status: val,
});
},
}
);
} else {
updatePaymentStatusMutation.mutate({
paymentId: payment.id,
status: val,
});
}
}}
className={`px-2 py-1 text-xs font-medium rounded-full border-0 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-teal-400 ${color}`}
>
<option value="PENDING">Pending</option>
<option value="PARTIALLY_PAID">Partially Paid</option>
<option value="PAID">Paid in Full</option>
</select>
);
}
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>
{payment.claim?.claimFiles && payment.claim.claimFiles.length > 0 ? (
<ul className="space-y-1">
{payment.claim.claimFiles.map((f: { id?: number; filename: string }) => (
<li
key={f.id ?? f.filename}
className="flex items-center gap-1 text-xs text-gray-700"
>
<Paperclip className="h-3 w-3 text-gray-400 shrink-0" />
<span className="truncate max-w-[140px]" title={f.filename}>
{f.filename}
</span>
</li>
))}
</ul>
) : (
<span className="text-xs text-gray-400"></span>
)}
</TableCell>
<TableCell>
{editingMhPaidId === payment.id ? (
<input
type="number"
min="0"
step="0.01"
autoFocus
className="w-24 border border-blue-400 rounded px-1 py-0.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
value={editingMhPaidValue}
onChange={(e) => setEditingMhPaidValue(e.target.value)}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.currentTarget.blur();
} else if (e.key === "Escape") {
setEditingMhPaidId(null);
}
}}
onBlur={async () => {
const val = parseFloat(editingMhPaidValue);
if (!isNaN(val) && val >= 0) {
try {
const res = await apiRequest(
"PATCH",
`/api/payments/${payment.id}/mh-paid-amount`,
{ mhPaidAmount: val }
);
if (res.ok) {
await queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE });
} else {
toast({ title: "Error", description: "Failed to save MH paid amount.", variant: "destructive" });
}
} catch {
toast({ title: "Error", description: "Failed to save MH paid amount.", variant: "destructive" });
}
}
setEditingMhPaidId(null);
}}
/>
) : (
<span
className="text-sm font-medium text-green-700 cursor-pointer hover:underline hover:text-green-900"
title="Click to edit"
onClick={() => {
setEditingMhPaidId(payment.id);
setEditingMhPaidValue(
payment.mhPaidAmount != null
? Number(payment.mhPaidAmount).toFixed(2)
: "0.00"
);
}}
>
{payment.mhPaidAmount != null
? `$${Number(payment.mhPaidAmount).toFixed(2)}`
: <span className="text-gray-400 font-normal"></span>}
</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
{allowDelete && (
<Button
onClick={() => {
handleDeletePayment(payment);
}}
className="text-red-600 hover:text-red-900"
aria-label="Delete Payment"
variant="ghost"
size="icon"
>
<Delete />
</Button>
)}
{allowEdit && (
<Button
variant="ghost"
size="icon"
onClick={() => {
handleEditPayment(payment);
}}
className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
>
<Edit className="h-4 w-4" />
</Button>
)}
{/* When NOT PAID and NOT VOID → Void */}
{payment.status !== "PAID" &&
payment.status !== "VOID" && (
<Button
variant="outline"
size="sm"
onClick={() => {
setVoidPaymentId(payment.id);
setIsVoidOpen(true);
}}
>
Void
</Button>
)}
{/* When PAID → Revert */}
{payment.status === "PAID" && (
<Button
variant="outline"
size="sm"
onClick={() => {
setRevertPaymentId(payment.id);
setIsRevertOpen(true);
}}
>
Revert Full Due
</Button>
)}
{/* When VOID → Unvoid */}
{payment.status === "VOID" && (
<Button
variant="outline"
size="sm"
onClick={() => {
setUnvoidPaymentId(payment.id);
setIsUnvoidOpen(true);
}}
>
Unvoid
</Button>
)}
</div>
</TableCell>
<TableCell className="text-xs text-gray-500">
{typeof payment.id === "number"
? `PAY-${payment.id.toString().padStart(4, "0")}`
: "N/A"}
</TableCell>
<TableCell className="text-xs text-gray-500">
{typeof payment.claimId === "number"
? `CLM-${payment.claimId.toString().padStart(4, "0")}`
: "N/A"}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
{/* Revert Confirmation Dialog */}
<ConfirmationDialog
isOpen={isRevertOpen}
title="Confirm Revert"
message={`Do you want to revert all Service Line payments for Payment ID: ${revertPaymentId}?`}
confirmLabel="Revert"
confirmColor="bg-yellow-600 hover:bg-yellow-700"
onConfirm={handleRevert}
onCancel={() => setIsRevertOpen(false)}
/>
{/* NEW: Void Confirmation Dialog */}
<ConfirmationDialog
isOpen={isVoidOpen}
title="Confirm Void"
message={`Mark this payment as VOID? It will be excluded from balances and Calculations.`}
confirmLabel="Void"
confirmColor="bg-gray-700 hover:bg-gray-800"
onConfirm={handleConfirmVoid}
onCancel={() => setIsVoidOpen(false)}
/>
{/* NEW: Unvoid Confirmation Dialog */}
<ConfirmationDialog
isOpen={isUnvoidOpen}
title="Confirm Unvoid"
message={`Restore this payment to a normal state (PENDING)?`}
confirmLabel="Unvoid"
confirmColor="bg-blue-600 hover:bg-blue-700"
onConfirm={handleConfirmUnvoid}
onCancel={() => setIsUnvoidOpen(false)}
/>
<DeleteConfirmationDialog
isOpen={isDeletePaymentOpen}
onConfirm={handleConfirmDeletePayment}
onCancel={() => setIsDeletePaymentOpen(false)}
entityName={`PaymentID : ${currentPayment?.id}`}
/>
{isEditPaymentOpen && currentPayment && (
<EditPaymentModal
isOpen={isEditPaymentOpen}
onOpenChange={(open) => setIsEditPaymentOpen(open)}
onClose={() => setIsEditPaymentOpen(false)}
payment={currentPayment}
onEditServiceLine={(updatedPayment) => {
updatePaymentMutation.mutate(updatedPayment);
}}
isUpdatingServiceLine={updatePaymentMutation.isPending}
onUpdateStatus={(paymentId, status) => {
updatePaymentStatusMutation.mutate({ paymentId, status });
}}
isUpdatingStatus={updatePaymentStatusMutation.isPending}
/>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-muted-foreground mb-2 sm:mb-0 whitespace-nowrap">
Showing {startItem}{endItem} of {paymentsData?.totalCount || 0}{" "}
results
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={
currentPage === 1 ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
{getPageNumbers(currentPage, totalPages).map((page, idx) => (
<PaginationItem key={idx}>
{page === "..." ? (
<span className="px-2 text-gray-500">...</span>
) : (
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage(page as number);
}}
isActive={currentPage === page}
>
{page}
</PaginationLink>
)}
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages)
setCurrentPage(currentPage + 1);
}}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
)}
</div>
);
}