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

161 lines
9.4 KiB
Markdown

# فلوی نوبت‌گیری: قفل واقعی ۱۵دقیقه‌ای، فرم «نوبت برای شخص دیگر»، و 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 && (
<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` — بدنه‌ی نوبت بدون بیمار
```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 با پیام توصیفی برای هر قابلیت.