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>
161 lines
9.4 KiB
Markdown
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 با پیام توصیفی برای هر قابلیت.
|