feat(patietn tabular view modal) - redirect to payment-view added
This commit is contained in:
@@ -160,15 +160,15 @@ export const getPatientFinancialRowsFn = async (
|
||||
offset = 0
|
||||
): Promise<{ rows: FinancialRow[]; totalCount: number }> => {
|
||||
try {
|
||||
// counts
|
||||
const [[{ count_claims }], [{ count_payments_without_claim }]] =
|
||||
// Count claims and orphan payments
|
||||
const [[{ count_claims }], [{ count_orphan_payments }]] =
|
||||
(await Promise.all([
|
||||
db.$queryRaw`SELECT COUNT(1) AS count_claims FROM "Claim" c WHERE c."patientId" = ${patientId}`,
|
||||
db.$queryRaw`SELECT COUNT(1) AS count_payments_without_claim FROM "Payment" p WHERE p."patientId" = ${patientId} AND p."claimId" IS NULL`,
|
||||
db.$queryRaw`SELECT COUNT(1) AS count_orphan_payments FROM "Payment" p WHERE p."patientId" = ${patientId} AND p."claimId" IS NULL`,
|
||||
])) as any;
|
||||
|
||||
const totalCount =
|
||||
Number(count_claims ?? 0) + Number(count_payments_without_claim ?? 0);
|
||||
Number(count_claims ?? 0) + Number(count_orphan_payments ?? 0);
|
||||
|
||||
const rawRows = (await db.$queryRaw`
|
||||
WITH claim_rows AS (
|
||||
@@ -185,6 +185,12 @@ export const getPatientFinancialRowsFn = async (
|
||||
(
|
||||
SELECT (pat."firstName" || ' ' || pat."lastName") FROM "Patient" pat WHERE pat.id = c."patientId" LIMIT 1
|
||||
) AS patient_name,
|
||||
|
||||
-- linked_payment_id (NULL if none). Schema has unique Payment.claimId so LIMIT 1 is safe.
|
||||
(
|
||||
SELECT p2.id FROM "Payment" p2 WHERE p2."claimId" = c.id LIMIT 1
|
||||
) AS linked_payment_id,
|
||||
|
||||
(
|
||||
SELECT coalesce(json_agg(
|
||||
json_build_object(
|
||||
@@ -201,29 +207,13 @@ export const getPatientFinancialRowsFn = async (
|
||||
)
|
||||
), '[]'::json)
|
||||
FROM "ServiceLine" sl2 WHERE sl2."claimId" = c.id
|
||||
) AS service_lines,
|
||||
(
|
||||
SELECT coalesce(json_agg(
|
||||
json_build_object(
|
||||
'id', p2.id,
|
||||
'totalBilled', p2."totalBilled",
|
||||
'totalPaid', p2."totalPaid",
|
||||
'totalAdjusted', p2."totalAdjusted",
|
||||
'totalDue', p2."totalDue",
|
||||
'status', p2.status::text,
|
||||
'createdAt', p2."createdAt",
|
||||
'icn', p2.icn,
|
||||
'notes', p2.notes
|
||||
)
|
||||
), '[]'::json)
|
||||
FROM "Payment" p2 WHERE p2."claimId" = c.id
|
||||
) AS payments
|
||||
) AS service_lines
|
||||
FROM "Claim" c
|
||||
LEFT JOIN "ServiceLine" sl ON sl."claimId" = c.id
|
||||
WHERE c."patientId" = ${patientId}
|
||||
GROUP BY c.id
|
||||
),
|
||||
payment_rows AS (
|
||||
orphan_payment_rows AS (
|
||||
SELECT
|
||||
'PAYMENT'::text AS type,
|
||||
p.id,
|
||||
@@ -237,7 +227,10 @@ export const getPatientFinancialRowsFn = async (
|
||||
(
|
||||
SELECT (pat."firstName" || ' ' || pat."lastName") FROM "Patient" pat WHERE pat.id = p."patientId" LIMIT 1
|
||||
) AS patient_name,
|
||||
-- aggregate service lines that belong to this payment (if any)
|
||||
|
||||
-- this payment's id is the linked_payment_id
|
||||
p.id AS linked_payment_id,
|
||||
|
||||
(
|
||||
SELECT coalesce(json_agg(
|
||||
json_build_object(
|
||||
@@ -254,28 +247,15 @@ export const getPatientFinancialRowsFn = async (
|
||||
)
|
||||
), '[]'::json)
|
||||
FROM "ServiceLine" sl3 WHERE sl3."paymentId" = p.id
|
||||
) AS service_lines,
|
||||
json_build_array(
|
||||
json_build_object(
|
||||
'id', p.id,
|
||||
'totalBilled', p."totalBilled",
|
||||
'totalPaid', p."totalPaid",
|
||||
'totalAdjusted', p."totalAdjusted",
|
||||
'totalDue', p."totalDue",
|
||||
'status', p.status::text,
|
||||
'createdAt', p."createdAt",
|
||||
'icn', p.icn,
|
||||
'notes', p.notes
|
||||
)
|
||||
) AS payments
|
||||
) AS service_lines
|
||||
FROM "Payment" p
|
||||
WHERE p."patientId" = ${patientId} AND p."claimId" IS NULL
|
||||
)
|
||||
SELECT type, id, date, created_at, status, total_billed, total_paid, total_adjusted, total_due, patient_name, service_lines, payments
|
||||
SELECT type, id, date, created_at, status, total_billed, total_paid, total_adjusted, total_due, patient_name, linked_payment_id, service_lines
|
||||
FROM (
|
||||
SELECT * FROM claim_rows
|
||||
UNION ALL
|
||||
SELECT * FROM payment_rows
|
||||
SELECT * FROM orphan_payment_rows
|
||||
) t
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
@@ -294,7 +274,9 @@ export const getPatientFinancialRowsFn = async (
|
||||
total_due: Number(r.total_due ?? 0),
|
||||
patient_name: r.patient_name ?? null,
|
||||
service_lines: r.service_lines ?? [],
|
||||
payments: r.payments ?? [],
|
||||
linked_payment_id: r.linked_payment_id
|
||||
? Number(r.linked_payment_id)
|
||||
: null,
|
||||
}));
|
||||
|
||||
return { rows, totalCount };
|
||||
|
||||
@@ -111,8 +111,20 @@ export function PatientFinancialsModal({
|
||||
}
|
||||
|
||||
function gotoRow(r: FinancialRow) {
|
||||
if (r.type === "CLAIM") navigate(`/claims/${r.id}`);
|
||||
else navigate(`/payments/${r.id}`);
|
||||
// If there's an explicit linked payment id, navigate to that payment
|
||||
if (r.linked_payment_id) {
|
||||
navigate(`/payments?paymentId=${r.linked_payment_id}`);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a PAYMENT row but somehow has no linked id, fallback to its id
|
||||
if (r.type === "PAYMENT") {
|
||||
navigate(`/payments?paymentId=${r.id}`);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
@@ -125,8 +137,14 @@ export function PatientFinancialsModal({
|
||||
setOffset((page - 1) * limit);
|
||||
}
|
||||
|
||||
const startItem = useMemo(() => Math.min(offset + 1, totalCount || 0), [offset, totalCount]);
|
||||
const endItem = useMemo(() => Math.min(offset + limit, totalCount || 0), [offset, limit, totalCount]);
|
||||
const startItem = useMemo(
|
||||
() => Math.min(offset + 1, totalCount || 0),
|
||||
[offset, totalCount]
|
||||
);
|
||||
const endItem = useMemo(
|
||||
() => Math.min(offset + limit, totalCount || 0),
|
||||
[offset, limit, totalCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -139,7 +157,11 @@ export function PatientFinancialsModal({
|
||||
{patientName ? (
|
||||
<>
|
||||
<span className="font-medium">{patientName}</span>{" "}
|
||||
{patientPID && <span className="text-muted-foreground">• PID-{String(patientPID).padStart(4, "0")}</span>}
|
||||
{patientPID && (
|
||||
<span className="text-muted-foreground">
|
||||
• PID-{String(patientPID).padStart(4, "0")}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
"Claims, payments and balances for this patient."
|
||||
@@ -148,7 +170,11 @@ export function PatientFinancialsModal({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -181,7 +207,10 @@ export function PatientFinancialsModal({
|
||||
</TableRow>
|
||||
) : rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={8}
|
||||
className="text-center py-8 text-muted-foreground"
|
||||
>
|
||||
No records found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -194,9 +223,10 @@ export function PatientFinancialsModal({
|
||||
|
||||
const procedureCodes =
|
||||
(r.service_lines || [])
|
||||
.map((sl: any) => sl.procedureCode ?? sl.procedureCode)
|
||||
.map((sl: any) => sl.procedureCode)
|
||||
.filter(Boolean)
|
||||
.join(", ") || (r.payments?.length ? "No Codes Given" : "-");
|
||||
.join(", ") ||
|
||||
(r.linked_payment_id ? "No Codes Given" : "-");
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
@@ -204,13 +234,29 @@ export function PatientFinancialsModal({
|
||||
className="cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => gotoRow(r)}
|
||||
>
|
||||
<TableCell className="font-medium">{r.type}</TableCell>
|
||||
<TableCell>{r.date ? new Date(r.date).toLocaleDateString() : "-"}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{procedureCodes}</TableCell>
|
||||
<TableCell className="text-right">{billed.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">{paid.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">{adjusted.toFixed(2)}</TableCell>
|
||||
<TableCell className={`text-right ${totalDue > 0 ? "text-red-600" : "text-green-600"}`}>
|
||||
<TableCell className="font-medium">
|
||||
{r.type}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{r.date
|
||||
? new Date(r.date).toLocaleDateString()
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{procedureCodes}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{billed.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{paid.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{adjusted.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right ${totalDue > 0 ? "text-red-600" : "text-green-600"}`}
|
||||
>
|
||||
{totalDue.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell>{r.status ?? "-"}</TableCell>
|
||||
@@ -243,7 +289,9 @@ export function PatientFinancialsModal({
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing <span className="font-medium">{startItem}</span>–<span className="font-medium">{endItem}</span> of <span className="font-medium">{totalCount}</span>
|
||||
Showing <span className="font-medium">{startItem}</span>–
|
||||
<span className="font-medium">{endItem}</span> of{" "}
|
||||
<span className="font-medium">{totalCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -257,7 +305,11 @@ export function PatientFinancialsModal({
|
||||
e.preventDefault();
|
||||
if (currentPage > 1) setPage(currentPage - 1);
|
||||
}}
|
||||
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
||||
className={
|
||||
currentPage === 1
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
@@ -287,7 +339,11 @@ export function PatientFinancialsModal({
|
||||
e.preventDefault();
|
||||
if (currentPage < totalPages) setPage(currentPage + 1);
|
||||
}}
|
||||
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
|
||||
className={
|
||||
currentPage === totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
formatLocalDate,
|
||||
parseLocalDate,
|
||||
} from "@/utils/dateUtils";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
PaymentStatus,
|
||||
paymentStatusOptions,
|
||||
@@ -31,34 +31,59 @@ 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 } from "@tanstack/react-query";
|
||||
|
||||
type PaymentEditModalProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onClose: () => void;
|
||||
onEditServiceLine: (payload: NewTransactionPayload) => void;
|
||||
|
||||
// Keeping callbacks optional — if provided parent handlers will be used
|
||||
onEditServiceLine?: (payload: NewTransactionPayload) => void;
|
||||
isUpdatingServiceLine?: boolean;
|
||||
onUpdateStatus: (paymentId: number, status: PaymentStatus) => void;
|
||||
|
||||
onUpdateStatus?: (paymentId: number, status: PaymentStatus) => void;
|
||||
isUpdatingStatus?: boolean;
|
||||
payment: PaymentWithExtras | null;
|
||||
|
||||
// Either pass a full payment object OR a paymentId (or both)
|
||||
payment?: PaymentWithExtras | null;
|
||||
paymentId?: number | null;
|
||||
};
|
||||
|
||||
export default function PaymentEditModal({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onClose,
|
||||
payment,
|
||||
onEditServiceLine,
|
||||
isUpdatingServiceLine,
|
||||
isUpdatingServiceLine: propUpdatingServiceLine,
|
||||
onUpdateStatus,
|
||||
isUpdatingStatus,
|
||||
isUpdatingStatus: propUpdatingStatus,
|
||||
payment: paymentProp,
|
||||
paymentId: paymentIdProp,
|
||||
}: PaymentEditModalProps) {
|
||||
if (!payment) return null;
|
||||
|
||||
const [expandedLineId, setExpandedLineId] = useState<number | null>(null);
|
||||
const [paymentStatus, setPaymentStatus] = React.useState<PaymentStatus>(
|
||||
payment.status
|
||||
// Local payment state: prefer prop but fetch if paymentId is provided
|
||||
const [payment, setPayment] = useState<PaymentWithExtras | null>(
|
||||
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<number | null>(null);
|
||||
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(
|
||||
(paymentProp ?? null)?.status ?? ("PENDING" as PaymentStatus)
|
||||
);
|
||||
|
||||
const [formState, setFormState] = useState(() => {
|
||||
return {
|
||||
serviceLineId: 0,
|
||||
@@ -72,10 +97,191 @@ export default function PaymentEditModal({
|
||||
};
|
||||
});
|
||||
|
||||
const serviceLines =
|
||||
payment.claim?.serviceLines ?? payment.serviceLines ?? [];
|
||||
// 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]);
|
||||
|
||||
const handleEditServiceLine = (lineId: number) => {
|
||||
// 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);
|
||||
@@ -109,6 +315,14 @@ export default function PaymentEditModal({
|
||||
};
|
||||
|
||||
const handleSavePayment = async () => {
|
||||
if (!payment) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Payment not loaded.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formState.serviceLineId) {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -178,7 +392,7 @@ export default function PaymentEditModal({
|
||||
};
|
||||
|
||||
try {
|
||||
await onEditServiceLine(payload);
|
||||
await handleEditServiceLine(payload);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Payment Transaction added successfully.",
|
||||
@@ -224,7 +438,7 @@ export default function PaymentEditModal({
|
||||
};
|
||||
|
||||
try {
|
||||
await onEditServiceLine(payload);
|
||||
await handleEditServiceLine(payload);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Full due amount ($${dueAmount.toFixed(
|
||||
@@ -241,6 +455,18 @@ export default function PaymentEditModal({
|
||||
}
|
||||
};
|
||||
|
||||
if (!payment) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-8 text-center">
|
||||
{loadingPayment ? "Loading…" : "No payment selected"}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
@@ -352,7 +578,7 @@ export default function PaymentEditModal({
|
||||
size="sm"
|
||||
disabled={isUpdatingStatus}
|
||||
onClick={() =>
|
||||
payment && onUpdateStatus(payment.id, paymentStatus)
|
||||
payment && handleUpdateStatus(payment.id, paymentStatus)
|
||||
}
|
||||
>
|
||||
{isUpdatingStatus ? "Updating..." : "Update Status"}
|
||||
@@ -432,7 +658,7 @@ export default function PaymentEditModal({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditServiceLine(line.id)}
|
||||
onClick={() => handleEditServiceLineClick(line.id)}
|
||||
>
|
||||
{isExpanded ? "Cancel" : "Pay Partially"}
|
||||
</Button>
|
||||
|
||||
@@ -18,9 +18,10 @@ import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
||||
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
||||
import PaymentOCRBlock from "@/components/payments/payment-ocr-block";
|
||||
import { useLocation } from "wouter";
|
||||
import { Patient } from "@repo/db/types";
|
||||
import { Patient, PaymentWithExtras } from "@repo/db/types";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import PaymentEditModal from "@/components/payments/payment-edit-modal";
|
||||
|
||||
export default function PaymentsPage() {
|
||||
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
|
||||
@@ -32,6 +33,10 @@ export default function PaymentsPage() {
|
||||
const [openPatientModalFromAppointment, setOpenPatientModalFromAppointment] =
|
||||
useState(false);
|
||||
|
||||
// Payment edit modal state (opens directly when ?paymentId=)
|
||||
const [paymentIdToEdit, setPaymentIdToEdit] = useState<number | null>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
|
||||
// small helper: remove query params silently
|
||||
const clearUrlParams = (params: string[]) => {
|
||||
try {
|
||||
@@ -105,6 +110,20 @@ export default function PaymentsPage() {
|
||||
};
|
||||
}, [location]);
|
||||
|
||||
// NEW: detect paymentId query param -> open edit modal (modal will fetch by id)
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const paymentIdParam = params.get("paymentId");
|
||||
if (!paymentIdParam) return;
|
||||
const paymentId = Number(paymentIdParam);
|
||||
if (!Number.isFinite(paymentId) || paymentId <= 0) return;
|
||||
|
||||
// Open modal with paymentId and clear params
|
||||
setPaymentIdToEdit(paymentId);
|
||||
setIsEditModalOpen(true);
|
||||
clearUrlParams(["paymentId", "patientId"]);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
@@ -215,6 +234,17 @@ export default function PaymentsPage() {
|
||||
setInitialPatientForModal(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Payment Edit Modal — modal will fetch payment by id and handle its own save/update */}
|
||||
<PaymentEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
onOpenChange={(v) => setIsEditModalOpen(v)}
|
||||
onClose={() => {
|
||||
setIsEditModalOpen(false);
|
||||
setPaymentIdToEdit(null);
|
||||
}}
|
||||
paymentId={paymentIdToEdit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user