Files
nobat724_front/components/appointment/paying/index.js
T

229 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react";
import moment from "moment-jalaali";
import ArrowLeftB from "@/components/icons/ArrowLeftB";
import { Button, MenuItem, Select, FormControl, InputLabel } from "@mui/material";
import ButtonFixed from "./ButtonFixed";
import { request } from "@/services/response";
const PAYMENT_TTL = 900;
function Paying({ setStep, appointmentId, expiresAt, doctor, data, isForAnother, selectedSlot }) {
const [selectedBank, setSelectedBank] = useState("");
const [gateways, setGateways] = useState([]);
const [loading, setLoading] = useState(false);
const [testMode, setTestMode] = useState(null);
const [feeRials, setFeeRials] = useState(null);
const computeLeft = () =>
expiresAt
? Math.max(0, Math.floor((expiresAt * 1000 - Date.now()) / 1000))
: PAYMENT_TTL;
const [timeLeft, setTimeLeft] = useState(computeLeft);
useEffect(() => {
const timer = setInterval(() => setTimeLeft(computeLeft()), 1000);
return () => clearInterval(timer);
}, [expiresAt]);
useEffect(() => {
request
.getPaymentConfig()
.then((res) => {
const gws = Array.isArray(res?.data?.gateways) ? res.data.gateways : [];
setTestMode(Boolean(res?.data?.test_mode));
setFeeRials(Number(res?.data?.appointment_fee_rials) || 0);
setGateways(gws);
setSelectedBank(gws[0]?.name ?? "");
})
.catch(() => { setTestMode(false); setFeeRials(0); setGateways([]); });
}, []);
const formatTime = (seconds) => {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${s.toString().padStart(2, "0")}`;
};
const rials = feeRials != null ? feeRials.toLocaleString("fa-IR") : "—";
const patientName = isForAnother
? [data?.name?.value, data?.family?.value].filter(Boolean).join(" ").trim()
: data?.name?.value;
const expired = timeLeft === 0;
const progress = Math.max(0, Math.min(100, (timeLeft / PAYMENT_TTL) * 100));
const handlePayment = async () => {
if (!appointmentId) return;
if (!testMode && !selectedBank) return;
setLoading(true);
try {
const res = await request.postAppointmentPayment({
appointment_uuid: appointmentId,
gateway: testMode ? "mellat" : selectedBank,
frontend_address: `${window.location.origin}/payment/result`,
});
// فقط به بک‌اند می‌رویم؛ بک‌اند صلاحیت را تأیید و به درگاه بانک منتقل می‌کند.
// کلاینت هرگز مستقیم به بانک ریکوست نمی‌زند.
const payUrl = res?.data?.pay_url;
if (payUrl) {
window.location.href = payUrl;
return;
}
throw new Error("pay_url missing");
} catch (error) {
console.error("Payment error:", error);
alert("خطا در شروع پرداخت. لطفاً دوباره تلاش کنید.");
setLoading(false);
}
};
return (
<div className="w-full lg:w-[59%]">
<div className="opacity-page bg-[#FFF] border border-solid border-[#EFEFEF] rounded-[12px] p-[16px] md:p-[24px]">
<p className="text-[#3B3B3B] text-[16px] md:text-[20px] font-bold mb-[20px]">
تأیید و پرداخت
</p>
{/* ── خلاصه نوبت ── */}
<div className="rounded-[10px] border border-[#EFEFEF] bg-[#FAFAFA] p-[16px] flex flex-col gap-[12px]">
{doctor?.name && (
<div className="flex justify-between items-center text-[14px] md:text-[15px]">
<span className="text-[#9B9B9B]">پزشک</span>
<span className="text-[#3B3B3B] font-medium">{doctor.name}</span>
</div>
)}
{patientName && (
<div className="flex justify-between items-center text-[14px] md:text-[15px]">
<span className="text-[#9B9B9B]">بیمار</span>
<span className="text-[#3B3B3B] font-medium">{patientName}</span>
</div>
)}
{selectedSlot?.start && (
<div className="flex justify-between items-center text-[14px] md:text-[15px]">
<span className="text-[#9B9B9B]">زمان نوبت</span>
<span className="text-[#3B3B3B] font-medium" dir="rtl">
{moment.unix(selectedSlot.start).locale("fa").format("dddd jD jMMMM jYYYY")}
{" — ساعت "}
{moment.unix(selectedSlot.start).format("HH:mm")}
</span>
</div>
)}
</div>
{/* ── مبلغ ── */}
<div className="flex justify-between items-center mt-[20px] py-[14px] border-y border-[#EFEFEF]">
<span className="text-[#616161] text-[14px] md:text-[16px] font-medium">
مبلغ قابل پرداخت
</span>
<span className="text-[#3B3B3B] text-[18px] md:text-[20px] font-bold">
{rials} <span className="text-[14px] font-normal text-[#616161]">ریال</span>
</span>
</div>
{/* ── شمارش معکوس ── */}
{!expired && (
<div className="mt-[20px]">
<div className="flex justify-between items-center mb-[8px]">
<span className="text-[#616161] text-[13px] md:text-[14px]">
مهلت پرداخت و نگهداری نوبت
</span>
<span
className={`text-[14px] md:text-[16px] font-bold tabular-nums ${
timeLeft < 60 ? "text-[#E0383B]" : "text-[#5559CE]"
}`}
>
{formatTime(timeLeft)}
</span>
</div>
<div className="w-full h-[6px] rounded-full bg-[#EFEFEF] overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-1000 ease-linear ${
timeLeft < 60 ? "bg-[#E0383B]" : "bg-[#5559CE]"
}`}
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
{/* ── درگاه / حالت آزمایشی ── */}
{!expired && (
<div className="mt-[20px]">
{testMode ? (
<div className="flex items-start gap-[8px] rounded-[10px] border border-[#F4B740] bg-[#FFF8E8] p-[12px]">
<span className="text-[18px] leading-none">🧪</span>
<div>
<p className="text-[#8A6100] text-[14px] font-bold">درگاه پرداخت آزمایشی</p>
<p className="text-[#8A6100] text-[12px] mt-[2px] leading-[20px]">
در این حالت پرداخت واقعی انجام نمیشود و نوبت بهصورت آزمایشی تأیید میگردد.
</p>
</div>
</div>
) : gateways.length === 0 ? (
<div className="rounded-[10px] border border-[#F3C0C1] bg-[#FDEEEE] p-[14px]">
<p className="text-[#E0383B] text-[14px] font-medium">
درگاه پرداخت فعالی موجود نیست. لطفاً بعداً تلاش کنید.
</p>
</div>
) : (
<FormControl fullWidth>
<InputLabel id="bank-select-label">انتخاب درگاه پرداخت</InputLabel>
<Select
labelId="bank-select-label"
value={selectedBank}
label="انتخاب درگاه پرداخت"
onChange={(e) => setSelectedBank(e.target.value)}
>
{gateways.map((g) => (
<MenuItem key={g.name} value={g.name}>
{g.label}
</MenuItem>
))}
</Select>
</FormControl>
)}
</div>
)}
{expired && (
<div className="mt-[20px] rounded-[10px] border border-[#F3C0C1] bg-[#FDEEEE] p-[14px]">
<p className="text-[#E0383B] text-[14px] font-medium">
مهلت پرداخت تمام شد و نوبت آزاد شد. لطفاً دوباره زمان نوبت را انتخاب کنید.
</p>
</div>
)}
<ButtonFixed>
<div className="flex items-center justify-end">
{expired ? (
<Button
className="!flex !w-full md:!w-fit !mr-auto !gap-[6px] !px-[16px] !py-[10px] !min-w-[180px]"
onClick={() => setStep(0)}
variant="contained"
>
<span className="text-[#FFF] text-[16px] font-medium">انتخاب مجدد زمان</span>
</Button>
) : (
<Button
className="!flex !w-full md:!w-fit !mr-auto !gap-[6px] !px-[16px] !py-[10px] !min-w-[180px]"
onClick={handlePayment}
variant="contained"
disabled={
loading || testMode === null || feeRials === null ||
(!testMode && !selectedBank)
}
>
<span className={`text-[#FFF] text-[16px] font-medium ${loading ? "opacity-0" : ""}`}>
{testMode ? "پرداخت آزمایشی" : `پرداخت ${rials} ریال`}
</span>
{!loading && <ArrowLeftB />}
</Button>
)}
</div>
</ButtonFixed>
</div>
</div>
);
}
export default Paying;