Files
nobat724_front/components/appointment/paying/index.js
T
hamedandClaude Opus 4.8 9912ae06ca feat(api): surface backend error messages globally via toast
Show the backend's Persian error (errors[0].message, e.g. rate-limit
'درخواست‌های زیاد') as a toast for any failed request.* call, from the
axios response interceptor — so failures are no longer silent. 401 still
logs out/redirects without a toast; callers can opt out with
config.skipErrorToast. Drop now-redundant per-caller alerts/toasts in
the booking submit, payment, and OTP userinfo paths.

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

165 lines
5.6 KiB
JavaScript

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";
function Paying({ setStep, appointmentId, expiresAt, doctor, selectedSlot, selectedDate }) {
const [selectedBank, setSelectedBank] = useState("");
const [loading, setLoading] = useState(false);
const computeLeft = () =>
expiresAt
? Math.max(0, Math.floor((expiresAt * 1000 - Date.now()) / 1000))
: 900;
const [timeLeft, setTimeLeft] = useState(computeLeft);
const banks = [
{ id: "mellat", title: "بانک ملت" },
{ id: "sep", title: "سامان (سپ)" }
];
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(computeLeft());
}, 1000);
return () => clearInterval(timer);
}, [expiresAt]);
const formatTime = (seconds) => {
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}:${secs.toString().padStart(2, '0')}`;
};
const handlePayment = async () => {
if (!selectedBank) {
alert("لطفا بانک خود را انتخاب کنید");
return;
}
if (!appointmentId) {
alert("شناسه نوبت یافت نشد");
return;
}
setLoading(true);
try {
const paymentPayload = {
appointment_uuid: appointmentId,
gateway: selectedBank,
frontend_address: `${window.location.origin}/payment/result`,
};
const res = await request.postAppointmentPayment(paymentPayload);
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-[8px] p-[12px] sm:p-[16px] md:p-[20px] lg:p-[24px]`}
>
<p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">
اطلاعات پرداخت
</p>
{selectedSlot?.start && (
<div className="flex justify-between items-center mt-[16px] text-[14px] md:text-[16px]">
<span className="text-[#616161] font-medium">زمان نوبت:</span>
<span className="text-[#3B3B3B] font-bold">
{moment.unix(selectedSlot.start).locale("fa").format("jD jMMMM jYYYY - HH:mm")}
{doctor?.name ? ` با ${doctor.name}` : ""}
</span>
</div>
)}
<div className="mt-[12px] sm:mt-[18px] md:mt-[24px] lg:mt-[30px]">
<div className="flex justify-between items-center mb-4">
<p className="text-[#616161] text-[14px] md:text-[16px] font-medium">
زمان باقیمانده:
</p>
<p className={`text-[14px] md:text-[16px] font-bold ${timeLeft < 60 ? 'text-red-500' : 'text-[#525252]'}`}>
{formatTime(timeLeft)}
</p>
</div>
<FormControl fullWidth className="!mb-4">
<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 className="flex justify-between items-center">
<p className="text-[#616161] text-[14px] md:text-[16px] font-medium">
مبلغ پیش پرداخت (بیعانه) :
</p>
<p className="text-[#525252] text-[14px] md:text-[16px] font-bold">
10،000 تومان
</p>
</div> */}
</div>
<span className="bg-[#D7D7D7] hidden md:block h-px w-full mt-[32px] mb-[25px]"></span>
{timeLeft === 0 && (
<p className="text-red-500 text-[14px] font-medium mb-3">
مهلت پرداخت تمام شد؛ لطفاً دوباره زمان نوبت را انتخاب کنید.
</p>
)}
<ButtonFixed>
<div className="flex items-center justify-end gap-2">
{timeLeft === 0 ? (
<Button
className="!gap-[4px] !mr-auto !flex !px-[12px] !p-[7px] md:!py-[9px] !min-w-[157px]"
onClick={() => setStep(0)}
variant="contained"
>
<p className="text-[#EFEFEF] text-[16px] font-medium">انتخاب مجدد زمان</p>
</Button>
) : (
<Button
className="!gap-[4px] !mr-auto !flex !px-[12px] !p-[7px] md:!py-[9px] !min-w-[157px]"
onClick={handlePayment}
variant="contained"
disabled={loading || !selectedBank}
>
<p className={`${loading && "opacity-0"} text-[#EFEFEF] text-[16px] font-medium`}>
پرداخت نهایی
</p>
<ArrowLeftB />
</Button>
)}
</div>
</ButtonFixed>
</div>
</div>
);
}
export default Paying;