Files
nobat724_front/.claude/prompt/booking-lock-patient-flow.md
hamedandClaude Opus 4.8 08107d6ef2 feat(appointment): real for-another-patient form and booking payload
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>
2026-06-15 18:07:10 +03:30

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_at Unix ثانیه است.

وظایف

۱. فرم «نوبت برای شخص دیگر» (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 با پیام توصیفی برای هر قابلیت.