Files
nobat724_front/components/appointment/paying/index.js
T
hamedandClaude Opus 4.8 d114730768 feat(appointment): redesign payment step with test-mode and real amount
Rebuild the payment screen as a clear confirm-and-pay card: appointment
summary (doctor, patient, Jalali date/time), a prominent amount row
(15,000 تومان from the backend's 150,000 rials), and a countdown with a
progress bar tied to the booking's real expires_at. Fetch
/api/v1/payment/config: in test mode show a 'درگاه آزمایشی' notice and a
'پرداخت آزمایشی' button (gateway select hidden, since the backend forces
MockGateway); otherwise show the bank gateway select. Expired state
offers re-selecting a time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:36:55 +03:30

214 lines
8.3 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 APPOINTMENT_AMOUNT_RIALS = 150000;
const PAYMENT_TTL = 900;
const banks = [
{ id: "mellat", title: "بانک ملت" },
{ id: "sep", title: "سامان (سپ)" },
];
function Paying({ setStep, appointmentId, expiresAt, doctor, data, isForAnother, selectedSlot }) {
const [selectedBank, setSelectedBank] = useState("mellat");
const [loading, setLoading] = useState(false);
const [testMode, setTestMode] = 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) => setTestMode(Boolean(res?.data?.test_mode)))
.catch(() => setTestMode(false));
}, []);
const formatTime = (seconds) => {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${s.toString().padStart(2, "0")}`;
};
const toman = (APPOINTMENT_AMOUNT_RIALS / 10).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;
setLoading(true);
try {
const res = await request.postAppointmentPayment({
appointment_uuid: appointmentId,
gateway: testMode ? "mellat" : selectedBank,
frontend_address: `${window.location.origin}/payment/result`,
});
const redirectUrl = res?.data?.redirect_url;
if (redirectUrl) {
window.location.href = redirectUrl;
} else {
setStep((prev) => prev + 1);
}
} catch (error) {
console.error("Payment error:", error);
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">
{toman} <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>
) : (
<FormControl fullWidth>
<InputLabel id="bank-select-label">انتخاب درگاه پرداخت</InputLabel>
<Select
labelId="bank-select-label"
value={selectedBank}
label="انتخاب درگاه پرداخت"
onChange={(e) => setSelectedBank(e.target.value)}
>
{banks.map((bank) => (
<MenuItem key={bank.id} value={bank.id}>
{bank.title}
</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}
>
<span className={`text-[#FFF] text-[16px] font-medium ${loading ? "opacity-0" : ""}`}>
{testMode ? "پرداخت آزمایشی" : `پرداخت ${toman} تومان`}
</span>
{!loading && <ArrowLeftB />}
</Button>
)}
</div>
</ButtonFixed>
</div>
</div>
);
}
export default Paying;