diff --git a/apps/Frontend/src/components/payments/payments-of-patient-table.tsx b/apps/Frontend/src/components/payments/payments-of-patient-table.tsx index ee29ed5..4885aad 100644 --- a/apps/Frontend/src/components/payments/payments-of-patient-table.tsx +++ b/apps/Frontend/src/components/payments/payments-of-patient-table.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { forwardRef, useEffect, useRef, useState } from "react"; import { PatientTable } from "../patients/patient-table"; import { Card, @@ -10,63 +10,118 @@ import { import { Patient } from "@repo/db/types"; import PaymentsRecentTable from "./payments-recent-table"; -export default function PaymentsOfPatientModal() { - const [selectedPatient, setSelectedPatient] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - const [paymentsPage, setPaymentsPage] = useState(1); +type Props = { + initialPatient?: Patient | null; + openInitially?: boolean; + onClose?: () => void; +}; - const handleSelectPatient = (patient: Patient | null) => { - if (patient) { - setSelectedPatient(patient); - setPaymentsPage(1); - setIsModalOpen(true); - } else { - setSelectedPatient(null); - setIsModalOpen(false); - } - }; +const PaymentsOfPatientModal = forwardRef( + ({ initialPatient = null, openInitially = false, onClose }: Props, ref) => { + const [selectedPatient, setSelectedPatient] = useState( + null + ); + const [isModalOpen, setIsModalOpen] = useState(false); + const [paymentsPage, setPaymentsPage] = useState(1); - return ( -
- {/* Payments Section */} - {selectedPatient && ( + // minimal, local scroll + cleanup — put inside PaymentsOfPatientModal + useEffect(() => { + if (!selectedPatient) return; + + const raf = requestAnimationFrame(() => { + const card = document.getElementById("payments-for-patient-card"); + const main = document.querySelector("main"); // your app's scroll container + if (card && main instanceof HTMLElement) { + const parentRect = main.getBoundingClientRect(); + const cardRect = card.getBoundingClientRect(); + const relativeTop = cardRect.top - parentRect.top + main.scrollTop; + const offset = 8; + main.scrollTo({ + top: Math.max(0, relativeTop - offset), + behavior: "smooth", + }); + } + }); + + // cleanup: when selectedPatient changes (ddmodal closes) or component unmounts, + // reset the main scroll to top so other pages are not left scrolled. + return () => { + cancelAnimationFrame(raf); + const main = document.querySelector("main"); + if (main instanceof HTMLElement) { + // immediate reset (no animation) so navigation to other pages starts at top + main.scrollTo({ top: 0, behavior: "auto" }); + } + }; + }, [selectedPatient]); + + // when parent provides an initialPatient and openInitially flag, apply it + useEffect(() => { + if (initialPatient) { + setSelectedPatient(initialPatient); + setPaymentsPage(1); + } + + if (openInitially) { + setIsModalOpen(true); + } + }, [initialPatient, openInitially]); + + const handleSelectPatient = (patient: Patient | null) => { + if (patient) { + setSelectedPatient(patient); + setPaymentsPage(1); + setIsModalOpen(true); + } else { + setSelectedPatient(null); + setIsModalOpen(false); + } + }; + + return ( +
+ {/* Payments Section */} + {selectedPatient && ( + + + + Payments for {selectedPatient.firstName}{" "} + {selectedPatient.lastName} + + + Displaying recent payments for the selected patient. + + + + + + + )} + + {/* Patients Section */} - - Payments for {selectedPatient.firstName}{" "} - {selectedPatient.lastName} - + Patient Records - Displaying recent payments for the selected patient. + Select any patient and View all their recent payments. - - )} +
+ ); + } +); - {/* Patients Section */} - - - Patient Records - - Select any patient and View all their recent payments. - - - - - - -
- ); -} +export default PaymentsOfPatientModal; diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index f024b25..4554b29 100644 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -669,7 +669,7 @@ export default function AppointmentsPage() { }; const handlePayments = (appointmentId: number) => { - console.log(`Processing payments for appointment: ${appointmentId}`); + setLocation(`/payments?appointmentId=${appointmentId}`); }; const handleChartPlan = (appointmentId: number) => { diff --git a/apps/Frontend/src/pages/payments-page.tsx b/apps/Frontend/src/pages/payments-page.tsx index 275d8a9..9d14190 100644 --- a/apps/Frontend/src/pages/payments-page.tsx +++ b/apps/Frontend/src/pages/payments-page.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Card, CardHeader, @@ -17,10 +17,94 @@ import { 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 { apiRequest } from "@/lib/queryClient"; +import { toast } from "@/hooks/use-toast"; export default function PaymentsPage() { const [paymentPeriod, setPaymentPeriod] = useState("all-time"); + // for auto-open from appointment redirect + const [location] = useLocation(); + const [initialPatientForModal, setInitialPatientForModal] = + useState(null); + const [openPatientModalFromAppointment, setOpenPatientModalFromAppointment] = + useState(false); + + // small helper: remove query params silently + const clearUrlParams = (params: string[]) => { + try { + const url = new URL(window.location.href); + let changed = false; + for (const p of params) { + if (url.searchParams.has(p)) { + url.searchParams.delete(p); + changed = true; + } + } + if (changed) + window.history.replaceState({}, document.title, url.toString()); + } catch (e) { + // ignore + } + }; + + // case1: If payments page is opened via appointment-page, /payments?appointmentId=123 -> fetch patient and open modal + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const appointmentIdParam = params.get("appointmentId"); + if (!appointmentIdParam) return; + const appointmentId = Number(appointmentIdParam); + if (!Number.isFinite(appointmentId) || appointmentId <= 0) return; + + let cancelled = false; + + (async () => { + try { + const res = await apiRequest( + "GET", + `/api/appointments/${appointmentId}/patient` + ); + if (!res.ok) { + let body: any = null; + try { + body = await res.json(); + } catch {} + if (!cancelled) { + toast({ + title: "Failed to load patient", + description: + body?.message ?? + body?.error ?? + `Could not fetch patient for appointment ${appointmentId}.`, + variant: "destructive", + }); + } + return; + } + + const data = await res.json(); + const patient = data?.patient ?? data; + if (!cancelled && patient && patient.id) { + setInitialPatientForModal(patient as Patient); + setOpenPatientModalFromAppointment(true); + + // remove query param so reload won't re-open + clearUrlParams(["appointmentId"]); + } + } catch (err) { + if (!cancelled) { + console.error("Error fetching patient for appointment:", err); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [location]); + return (
{/* Header */} @@ -122,7 +206,15 @@ export default function PaymentsPage() { {/* Recent Payments by Patients*/} - + { + // reset the local flags when modal is closed + setOpenPatientModalFromAppointment(false); + setInitialPatientForModal(null); + }} + />
); }