Replace the mock for-another button with a real toggle: switching keeps the user's own data, clears the form to editable patient fields (phone becomes an input, adds علت مراجعه), and can switch back. SubmitData now sends for_self plus patient_* only when booking for someone else, skips the self-profile PATCH/POST in that case, stores the booking expires_at, and surfaces a clearer 409 message. Thread appointmentExpiresAt through the wizard to the payment step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.4 KiB
فلوی نوبتگیری: قفل واقعی ۱۵دقیقهای، فرم «نوبت برای شخص دیگر»، و UX وضعیتمحور
پروژه
nobat724_front — سایت عمومی. بعد از پرامپت backend اجرا شود.
Cross-repo: وابسته به اصلاحات backend در
clinicpro/.claude/prompt/appointment-lock-patient-confirm.md(فیلدهایpatient_*وfor_selfدرPOST /api/v1/appointment،expires_atدر پاسخ، تأیید پس از پرداخت). اگر آنها هنوز نیستند، اول backend را اجرا کن.
زمینه
صفحهی /appointment/[doctorId] یک wizard چندمرحلهای است (components/appointment/): انتخاب تاریخ/ساعت → لاگین OTP → تکمیل اطلاعات (Detail) → پرداخت (Paying). در اصلاحات قبلی فلو به API واقعی وصل شد (ساخت نوبت در SubmitData.js، پرداخت در paying/index.js). اما:
- تایمر پرداخت قلابی است:
paying/index.jsیکtimeLeft = 600(۱۰ دقیقه) شمارش معکوس محلی دارد که به قفل واقعی backend وصل نیست. باید ۱۵ دقیقه و بر مبنایexpires_atواقعی نوبت باشد. - «نوبت برای شخص دیگر» فقط mock بود: در
detail/index.jsدکمهی «دریافت نوبت برای فرد دیگر» دادهی ثابت ست میکند و فیلدهای واقعی بیمار (نام، موبایل، جنسیت، کد ملی، علت) را نمیگیرد و به backend نمیفرستد. - بدنهی ساخت نوبت فیلد بیمار/
for_selfندارد.
مشکل / هدف
۱. فرم «نوبت برای شخص دیگر» با فیلدهای واقعی بیمار، و ارسال patient_* + for_self در ساخت نوبت.
۲. تایمر پرداخت بر مبنای expires_at واقعی نوبت (۱۵ دقیقه)؛ اتمام تایمر → نوبت منقضی، پیام مناسب، بازگشت به انتخاب ساعت.
۳. UX وضعیتمحور: پیامهای واضح برای رزرو همزمان (۴۰۹)، انقضا، و موفقیت.
فایلهای مرتبط
| فایل | نقش |
|---|---|
components/appointment/index.js |
state کل wizard (data, isForAnother, selectedSlot, appointmentId) |
components/appointment/detail/index.js |
فرم Detail + دکمهی «برای فرد دیگر» (الان mock) |
components/appointment/detail/Form.js |
رندر فیلدها |
components/appointment/detail/SubmitData.js |
request.postAppointment — بدنه باید patient_*/for_self بگیرد و expires_at را نگه دارد |
components/appointment/paying/index.js |
تایمر و پرداخت — تایمر باید از expires_at بیاید |
services/response.js |
postAppointment (بدنه از caller) |
وضعیت فعلی (کد واقعی)
detail/index.js — دکمهی mock «برای فرد دیگر»
{!isForAnother && (
<Button onClick={() => {
setIsForAnother(true);
setData({
phone: { value: "09121056987", isEdit: true },
codemeli: { value: "1741025645", isEdit: true },
name: { value: "ساغر صابری نژاد", isEdit: true },
});
}}>
<AddCircleBlueA />
<p>دریافت نوبت برای فرد دیگر</p>
</Button>
)}
SubmitData.js — بدنهی نوبت بدون بیمار
const appointmentPayload = {
doctor_uuid: doctor.uuid,
slot_start: selectedSlot.start,
slot_end: selectedSlot.end,
note: "",
};
const res = await request.postAppointment(appointmentPayload);
const appointmentUuid = res?.data?.uuid;
if (appointmentUuid) setAppointmentId(appointmentUuid);
paying/index.js — تایمر محلی ۶۰۰ ثانیه (قلابی)
const [timeLeft, setTimeLeft] = useState(600); // 10 minutes
useEffect(() => {
if (timeLeft === 0) return;
const timer = setInterval(() => setTimeLeft((p) => p - 1), 1000);
return () => clearInterval(timer);
}, [timeLeft]);
قرارداد backend (بعد از اجرای پرامپت همتا)
POST /api/v1/appointment بدنه:
{
"doctor_uuid": "...", "slot_start": 1781933400, "slot_end": 1781934600,
"for_self": true,
"patient_name": "...", "patient_mobile": "09...", "patient_national_code": "...",
"patient_gender": "male", "patient_reason": "...", "note": ""
}
پاسخ 201: { success, data: { uuid, status:"pending", expires_at: 1781934300, patient_name, ... } }
خطاها: 409 (اسلات همزمان رزرو شد)، 422 (فیلد بیمار ناقص وقتی for_self=false).
interceptor یکبار باز میکند → داده در
res.data.expires_atUnix ثانیه است.
وظایف
۱. فرم «نوبت برای شخص دیگر» (detail/index.js + Form.js)
- دکمه را به یک toggle واقعی تبدیل کن: با
isForAnother=trueفیلدهای بیمار خالی و قابلویرایش شوند (نه دادهی mock). - فیلدهای بیمار: نام، نام خانوادگی، شماره موبایل (هر دو اجباری وقتی برای دیگری)، جنسیت (اختیاری)، کد ملی (اختیاری)، علت مراجعه (اختیاری، textarea).
- وقتی «برای خودم»: همان رفتار فعلی (اطلاعات پروفایل، قابلویرایش).
- اعتبارسنجی حداقلی سمت کلاینت (نام و موبایل بیمار اجباری در حالت «برای دیگری»)؛ خطای فیلد را با همان الگوی
errorsموجود نشان بده.
۲. ارسال patient_* و for_self در SubmitData.js
- بدنه را گسترش بده:
const appointmentPayload = {
doctor_uuid: doctor.uuid,
slot_start: selectedSlot.start,
slot_end: selectedSlot.end,
for_self: !isForAnother,
note: data?.patient_reason?.value || "",
...(isForAnother ? {
patient_name: data?.name?.value,
patient_mobile: data?.phone?.value,
patient_national_code: data?.national_code?.value || "",
patient_gender: data?.gender?.value?.id || data?.gender?.value || "",
patient_reason: data?.patient_reason?.value || "",
} : {}),
};
isForAnotherرا بهSubmitDataپاس بده (الان درContainer/Detailهست).expires_atپاسخ را نگهدار و به مرحلهی پرداخت پاس بده (state جدیدappointmentExpiresAtدرcomponents/appointment/index.js).- خطای
409: پیام «این زمان همین لحظه توسط کاربر دیگری رزرو شد» + بازگشت به مرحلهی انتخاب ساعت (setStep(0)) و تازهسازی اسلاتها. - خطای
422: پیامهای فیلد بیمار.
۳. تایمر پرداخت بر مبنای expires_at (paying/index.js)
- بهجای
useState(600)، باقیمانده را ازexpiresAt(prop) حساب کن:
const computeLeft = () => Math.max(0, Math.floor((expiresAt * 1000 - Date.now()) / 1000));
const [timeLeft, setTimeLeft] = useState(computeLeft);
useEffect(() => {
const t = setInterval(() => setTimeLeft(computeLeft()), 1000);
return () => clearInterval(t);
}, [expiresAt]);
- وقتی
timeLeft === 0: دکمهی پرداخت غیرفعال، پیام «مهلت پرداخت تمام شد؛ لطفاً دوباره زمان نوبت را انتخاب کنید»، و دکمهای برای بازگشت به مرحلهی ۰. - اگر
expiresAtنبود (سازگاری)، fallback به ۹۰۰ ثانیه از زمان mount.
۴. UX نهایی
- در مرحلهی پرداخت، نام بیمار و زمان نوبت را نمایش بده (از همان دادهی نوبت).
- بعد از پرداخت موفق (بازگشت از درگاه به
/payment/resultیا step success)، پیام تأیید + اشاره به ارسال SMS.
نکات مهم
- وابستگی cross-repo: بدون فیلدهای
patient_*/for_selfوexpires_atدر backend این کار کامل نیست. اگر نبود متوقف شو. - پرداختکننده ≠ بیمار: کاربر لاگینشده پرداختکننده است (از کوکی/توکن، خودکار)؛ فیلدهای بیمار جدا ارسال میشوند. این تفکیک را در UI هم روشن نشان بده.
- زمانها Unix ثانیه:
expires_at * 1000برای مقایسه باDate.now(). - عدم رگرسیون: فلوی OTP (step 1/2)، تقویم دوماهه، و گرید ساعت دستنخورده بمانند. منطق فعلی
SubmitDataبرای PATCH/POST پروفایلِ کاربرِ «برای خودم» را حفظ کن (اطلاعات خودِ کاربر هنوز در پروفایلش ذخیره میشود؛ ولی فیلدهای بیمارِ نوبت جداگانه به نوبت میروند). - RTL/Jalali/multi-domain حفظ شوند؛ فونت/کتابخانهی جدید اضافه نکن.
- تست:
npm run build؛ سپس دستی: «برای دیگری» با نام/موبایل بیمار → نوبت ساخته شود؛ تایمر از ~۱۵:۰۰ بشمارد؛ رزرو همزمان روی یک اسلات → پیام ۴۰۹. سپس commit با پیام توصیفی برای هر قابلیت.