feat(payment redirect aptmpt page) - added button
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||||
import { PatientTable } from "../patients/patient-table";
|
import { PatientTable } from "../patients/patient-table";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -10,11 +10,63 @@ import {
|
|||||||
import { Patient } from "@repo/db/types";
|
import { Patient } from "@repo/db/types";
|
||||||
import PaymentsRecentTable from "./payments-recent-table";
|
import PaymentsRecentTable from "./payments-recent-table";
|
||||||
|
|
||||||
export default function PaymentsOfPatientModal() {
|
type Props = {
|
||||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
initialPatient?: Patient | null;
|
||||||
|
openInitially?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaymentsOfPatientModal = forwardRef<HTMLDivElement, Props>(
|
||||||
|
({ initialPatient = null, openInitially = false, onClose }: Props, ref) => {
|
||||||
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [paymentsPage, setPaymentsPage] = useState(1);
|
const [paymentsPage, setPaymentsPage] = useState(1);
|
||||||
|
|
||||||
|
// 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) => {
|
const handleSelectPatient = (patient: Patient | null) => {
|
||||||
if (patient) {
|
if (patient) {
|
||||||
setSelectedPatient(patient);
|
setSelectedPatient(patient);
|
||||||
@@ -30,7 +82,7 @@ export default function PaymentsOfPatientModal() {
|
|||||||
<div className="space-y-8 py-8">
|
<div className="space-y-8 py-8">
|
||||||
{/* Payments Section */}
|
{/* Payments Section */}
|
||||||
{selectedPatient && (
|
{selectedPatient && (
|
||||||
<Card>
|
<Card id="payments-for-patient-card">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
Payments for {selectedPatient.firstName}{" "}
|
Payments for {selectedPatient.firstName}{" "}
|
||||||
@@ -69,4 +121,7 @@ export default function PaymentsOfPatientModal() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PaymentsOfPatientModal;
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ export default function AppointmentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePayments = (appointmentId: number) => {
|
const handlePayments = (appointmentId: number) => {
|
||||||
console.log(`Processing payments for appointment: ${appointmentId}`);
|
setLocation(`/payments?appointmentId=${appointmentId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChartPlan = (appointmentId: number) => {
|
const handleChartPlan = (appointmentId: number) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -17,10 +17,94 @@ import {
|
|||||||
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
||||||
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
||||||
import PaymentOCRBlock from "@/components/payments/payment-ocr-block";
|
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() {
|
export default function PaymentsPage() {
|
||||||
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
|
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
|
||||||
|
|
||||||
|
// for auto-open from appointment redirect
|
||||||
|
const [location] = useLocation();
|
||||||
|
const [initialPatientForModal, setInitialPatientForModal] =
|
||||||
|
useState<Patient | null>(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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -122,7 +206,15 @@ export default function PaymentsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Recent Payments by Patients*/}
|
{/* Recent Payments by Patients*/}
|
||||||
<PaymentsOfPatientModal />
|
<PaymentsOfPatientModal
|
||||||
|
initialPatient={initialPatientForModal}
|
||||||
|
openInitially={openPatientModalFromAppointment}
|
||||||
|
onClose={() => {
|
||||||
|
// reset the local flags when modal is closed
|
||||||
|
setOpenPatientModalFromAppointment(false);
|
||||||
|
setInitialPatientForModal(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user