# فلوی نوبت‌گیری: قفل واقعی ۱۵دقیقه‌ای، فرم «نوبت برای شخص دیگر»، و 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 «برای فرد دیگر» ```jsx {!isForAnother && ( )} ``` ### `SubmitData.js` — بدنه‌ی نوبت بدون بیمار ```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` — تایمر محلی ۶۰۰ ثانیه (قلابی) ```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` بدنه: ```json { "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_at` Unix ثانیه است. ## وظایف ### ۱. فرم «نوبت برای شخص دیگر» (`detail/index.js` + `Form.js`) - دکمه را به یک toggle واقعی تبدیل کن: با `isForAnother=true` فیلدهای بیمار خالی و قابل‌ویرایش شوند (نه داده‌ی mock). - فیلدهای بیمار: نام، نام خانوادگی، شماره موبایل (هر دو اجباری وقتی برای دیگری)، جنسیت (اختیاری)، کد ملی (اختیاری)، علت مراجعه (اختیاری، textarea). - وقتی «برای خودم»: همان رفتار فعلی (اطلاعات پروفایل، قابل‌ویرایش). - اعتبارسنجی حداقلی سمت کلاینت (نام و موبایل بیمار اجباری در حالت «برای دیگری»)؛ خطای فیلد را با همان الگوی `errors` موجود نشان بده. ### ۲. ارسال `patient_*` و `for_self` در `SubmitData.js` - بدنه را گسترش بده: ```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) حساب کن: ```js 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 با پیام توصیفی برای هر قابلیت.