import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { formatDateToHumanReadable, formatLocalDate, parseLocalDate, } from "@/utils/dateUtils"; import React, { useEffect, useState } from "react"; import { PaymentStatus, PaymentMethod, paymentMethodOptions, PaymentWithExtras, NewTransactionPayload, paymentStatusArray, paymentMethodArray, } from "@repo/db/types"; import { NpiProvider } from "@repo/db/types"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { toast } from "@/hooks/use-toast"; import { X } from "lucide-react"; import { DateInput } from "@/components/ui/dateInput"; import { apiRequest } from "@/lib/queryClient"; import { useMutation, useQuery } from "@tanstack/react-query"; type PaymentEditModalProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; onClose: () => void; // Keeping callbacks optional — if provided parent handlers will be used onEditServiceLine?: (payload: NewTransactionPayload) => void; isUpdatingServiceLine?: boolean; onUpdateStatus?: (paymentId: number, status: PaymentStatus) => void; isUpdatingStatus?: boolean; // Either pass a full payment object OR a paymentId (or both) payment?: PaymentWithExtras | null; paymentId?: number | null; }; export default function PaymentEditModal({ isOpen, onOpenChange, onClose, onEditServiceLine, isUpdatingServiceLine: propUpdatingServiceLine, onUpdateStatus, isUpdatingStatus: propUpdatingStatus, payment: paymentProp, paymentId: paymentIdProp, }: PaymentEditModalProps) { // Local payment state: prefer prop but fetch if paymentId is provided const [payment, setPayment] = useState( paymentProp ?? null ); const [loadingPayment, setLoadingPayment] = useState(false); // Local update states (used if parent didn't provide flags) const [localUpdatingServiceLine, setLocalUpdatingServiceLine] = useState(false); const [localUpdatingStatus, setLocalUpdatingStatus] = useState(false); // derived flags - prefer parent's flags if provided const isUpdatingServiceLine = propUpdatingServiceLine ?? localUpdatingServiceLine; const isUpdatingStatus = propUpdatingStatus ?? localUpdatingStatus; // UI state (kept from your original) const [expandedLineId, setExpandedLineId] = useState(null); const [paymentStatus, setPaymentStatus] = useState( (paymentProp ?? null)?.status ?? ("PENDING" as PaymentStatus) ); const [selectedNpiProviderId, setSelectedNpiProviderId] = useState( (paymentProp ?? null)?.npiProviderId ?? null ); const [isUpdatingProvider, setIsUpdatingProvider] = useState(false); // Fetch all NPI providers 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) if no provider set yet 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]); // Sync provider when payment changes (e.g. fetched from API) useEffect(() => { if (payment?.npiProviderId !== undefined) { setSelectedNpiProviderId(payment.npiProviderId ?? null); } }, [payment?.id]); const handleUpdateProvider = async () => { if (!payment) return; setIsUpdatingProvider(true); try { const res = await apiRequest("PATCH", `/api/payments/${payment.id}/provider`, { npiProviderId: selectedNpiProviderId, }); if (!res.ok) { const err = await res.json(); throw new Error(err.message || "Failed to update provider"); } toast({ title: "Success", description: "Provider updated successfully." }); await refetchPayment(payment.id); } catch (err: any) { toast({ title: "Error", description: err?.message ?? "Failed to update provider.", variant: "destructive" }); } finally { setIsUpdatingProvider(false); } }; const [formState, setFormState] = useState(() => { return { serviceLineId: 0, transactionId: "", paidAmount: 0, adjustedAmount: 0, method: paymentMethodOptions.CHECK as PaymentMethod, receivedDate: formatLocalDate(new Date()), payerName: "", notes: "", }; }); // Sync when parent passes a payment object or paymentId changes useEffect(() => { // if parent gave a full payment object, use it immediately if (paymentProp) { setPayment(paymentProp); setPaymentStatus(paymentProp.status); } }, [paymentProp]); // Fetch payment when modal opens and we only have paymentId (or payment prop not supplied) useEffect(() => { if (!isOpen) return; // if payment prop is already available, no need to fetch if (paymentProp) return; const id = paymentIdProp ?? payment?.id; if (!id) return; let cancelled = false; (async () => { setLoadingPayment(true); try { const res = await apiRequest("GET", `/api/payments/${id}`); if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.message ?? `Failed to fetch payment ${id}`); } const data = await res.json(); if (!cancelled) { setPayment(data as PaymentWithExtras); setPaymentStatus((data as PaymentWithExtras).status); } } catch (err: any) { console.error("Failed to fetch payment:", err); toast({ title: "Error", description: err?.message ?? "Failed to load payment.", variant: "destructive", }); } finally { if (!cancelled) setLoadingPayment(false); } })(); return () => { cancelled = true; }; }, [isOpen, paymentIdProp]); // convenience: get service lines from claim or payment const serviceLines = payment?.claim?.serviceLines ?? payment?.serviceLines ?? []; // small helper to refresh current payment (used after internal writes) async function refetchPayment(id?: number) { const pid = id ?? payment?.id ?? paymentIdProp; if (!pid) return; setLoadingPayment(true); try { const res = await apiRequest("GET", `/api/payments/${pid}`); if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.message ?? `Failed to fetch payment ${pid}`); } const data = await res.json(); setPayment(data as PaymentWithExtras); setPaymentStatus((data as PaymentWithExtras).status); } catch (err: any) { console.error("Failed to refetch payment:", err); toast({ title: "Error", description: err?.message ?? "Failed to refresh payment.", variant: "destructive", }); } finally { setLoadingPayment(false); } } // Internal save (fallback) — used only when parent didn't provide onEditServiceLine const internalUpdatePaymentMutation = 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!", }); await refetchPayment(paymentId); }, onError: (error) => { toast({ title: "Error", description: `Update failed: ${error.message}`, variant: "destructive", }); }, }); const internalUpdatePaymentStatusMutation = 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!", }); // Fetch updated payment and set into local state await refetchPayment(paymentId); }, onError: (error) => { toast({ title: "Error", description: `Status update failed: ${error.message}`, variant: "destructive", }); }, }); // Keep your existing handlers but route to either parent callback or internal functions const handleEditServiceLine = async (payload: NewTransactionPayload) => { if (onEditServiceLine) { await onEditServiceLine(payload); } else { // fallback to internal API call setLocalUpdatingServiceLine(true); await internalUpdatePaymentMutation.mutateAsync(payload); setLocalUpdatingServiceLine(false); } }; const handleUpdateStatus = async ( paymentId: number, status: PaymentStatus ) => { if (onUpdateStatus) { await onUpdateStatus(paymentId, status); } else { setLocalUpdatingStatus(true); await internalUpdatePaymentStatusMutation.mutateAsync({ paymentId, status, }); setLocalUpdatingStatus(false); } }; const handleEditServiceLineClick = (lineId: number) => { if (expandedLineId === lineId) { // Closing current line setExpandedLineId(null); return; } // Find line data const line = serviceLines.find((sl) => sl.id === lineId); if (!line) return; // updating form to show its data, while expanding. setFormState({ serviceLineId: line.id, transactionId: "", paidAmount: Number(line.totalDue) > 0 ? Number(line.totalDue) : 0, adjustedAmount: 0, method: paymentMethodOptions.CHECK as PaymentMethod, receivedDate: formatLocalDate(new Date()), payerName: "", notes: "", }); setExpandedLineId(lineId); }; const updateField = (field: string, value: any) => { setFormState((prev) => ({ ...prev, [field]: value, })); }; const handleSavePayment = async () => { if (!payment) { toast({ title: "Error", description: "Payment not loaded.", variant: "destructive", }); return; } if (!formState.serviceLineId) { 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 = 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; } const payload: NewTransactionPayload = { paymentId: payment.id, serviceLineTransactions: [ { serviceLineId: formState.serviceLineId, transactionId: formState.transactionId || undefined, paidAmount: Number(formState.paidAmount), adjustedAmount: Number(formState.adjustedAmount) || 0, method: formState.method, receivedDate: parseLocalDate(formState.receivedDate), payerName: formState.payerName?.trim() || undefined, notes: formState.notes?.trim() || undefined, }, ], }; try { await handleEditServiceLine(payload); toast({ title: "Success", description: "Payment Transaction added successfully.", }); setExpandedLineId(null); } catch (err) { console.error(err); toast({ title: "Error", description: "Failed to save payment." }); } }; const handlePayFullDue = async (line: (typeof 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, serviceLineTransactions: [ { serviceLineId: line.id, paidAmount: dueAmount, adjustedAmount: 0, method: paymentMethodOptions.CHECK as PaymentMethod, // Maybe make dynamic later receivedDate: new Date(), }, ], }; try { await handleEditServiceLine(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", }); } }; if (!payment) { return (
{loadingPayment ? "Loading…" : "No payment selected"}
); } return (
Edit Payment View and manage payments applied to service lines. {/* Close button in top-right */}
{/* Claim + Patient Info */}

{payment.claim?.patientName ?? (`${payment.patient?.firstName ?? ""} ${payment.patient?.lastName ?? ""}`.trim() || "Unknown Patient")}

{payment.claimId ? ( Claim #{payment.claimId.toString().padStart(4, "0")} ) : payment.notes?.startsWith("PDF import") ? ( PDF Import ) : ( OCR Imported Payment )} Service Date:{" "} {payment.claim?.serviceDate ? formatDateToHumanReadable(payment.claim.serviceDate) : serviceLines.length > 0 ? formatDateToHumanReadable(serviceLines[0]?.procedureDate) : formatDateToHumanReadable(payment.createdAt)} {payment.icn ? ( ICN : {payment.icn} ) : null}
{/* Payment Summary + Metadata */}
{/* Payment Info */}

Payment Info

Total Billed:{" "} ${Number(payment.totalBilled || 0).toFixed(2)}

Total Paid:{" "} ${Number(payment.totalPaid || 0).toFixed(2)}

Total Due:{" "} ${Number(payment.totalDue || 0).toFixed(2)}

{/* Status Selector */}
{/* Provider Selector */}
{/* Metadata */}

Metadata

Created At:{" "} {payment.createdAt ? formatDateToHumanReadable(payment.createdAt) : "N/A"}

Last Updated At:{" "} {payment.updatedAt ? formatDateToHumanReadable(payment.updatedAt) : "N/A"}

{(payment as any).commissionBatchItems?.length > 0 ? (
✓ Commissioned

Paid as commission on{" "} {formatDateToHumanReadable( (payment as any).commissionBatchItems[0].commissionBatch.createdAt )}

) : (

Not yet commissioned

)}
{/* Service Lines Payments */}

Service Lines

{serviceLines.length > 0 ? ( serviceLines.map((line) => { const isExpanded = expandedLineId === line.id; return (
{/* Top Info */}

Procedure Code:{" "} {line.procedureCode}

{(line as any).icn && (

ICN:{" "} {(line as any).icn}

)} {(line as any).paidCode && (line as any).paidCode !== line.procedureCode && (

Paid Code:{" "} {(line as any).paidCode}

)} {(line as any).allowedAmount != null && (

Allowed:{" "} ${Number((line as any).allowedAmount).toFixed(2)}

)} {(line as any).quad && (

Quad:{" "} {(line as any).quad}

)} {(line as any).arch && (

Arch:{" "} {(line as any).arch}

)} {line.toothNumber && (

Tooth Number:{" "} {line.toothNumber}

)} {line.toothSurface && (

Surface:{" "} {line.toothSurface}

)}

Billed:{" "} ${Number(line.totalBilled || 0).toFixed(2)}

Paid:{" "} ${Number(line.totalPaid || 0).toFixed(2)}

Adjusted:{" "} ${Number(line.totalAdjusted || 0).toFixed(2)}

Due:{" "} ${Number(line.totalDue || 0).toFixed(2)}

{/* Action Buttons */}
{/* Expanded Partial Payment Form */} {isExpanded && (
updateField( "paidAmount", parseFloat(e.target.value) ) } />
updateField( "adjustedAmount", parseFloat(e.target.value) ) } />
{ if (date) { const localDate = formatLocalDate(date); updateField("receivedDate", localDate); } else { updateField("receivedDate", null); } }} disableFuture />
updateField("payerName", e.target.value) } />
updateField("notes", e.target.value) } />
)}
); }) ) : (

No service lines available.

)}
{/* Transactions Overview */}

All Transactions

{payment.serviceLineTransactions.length > 0 ? ( payment.serviceLineTransactions.map((tx) => (
{/* Transaction ID */} {tx.id && (

Transaction ID:{" "} {tx.id}

)} {/* Procedure Code */} {tx.serviceLine?.procedureCode && (

Procedure Code:{" "} {tx.serviceLine.procedureCode}

)} {/* Tooth Number */} {tx.serviceLine?.toothNumber && (

Tooth Number:{" "} {tx.serviceLine.toothNumber}

)} {/* Tooth Surface */} {tx.serviceLine?.toothSurface && (

Surface:{" "} {tx.serviceLine.toothSurface}

)} {/* Paid Amount */}

Paid Amount:{" "} ${Number(tx.paidAmount).toFixed(2)}

{/* Adjusted Amount */} {Number(tx.adjustedAmount) > 0 && (

Adjusted Amount: {" "} ${Number(tx.adjustedAmount).toFixed(2)}

)} {/* Date */}

Date:{" "} {formatDateToHumanReadable(tx.receivedDate)}

{/* Method */}

Method:{" "} {tx.method}

{/* Payer Name */} {tx.payerName && tx.payerName.trim() !== "" && (

Payer Name:{" "} {tx.payerName}

)} {/* Notes */} {tx.notes && tx.notes.trim() !== "" && (

Notes:{" "} {tx.notes}

)}
)) ) : (

No transactions recorded.

)}
{/* Actions */}
); }