fix(appointment): real status filter + required account fields
- My-appointments tabs sent invalid status values (reserved/waiting_for_payment/ ...) that don't exist in the backend, so every tab but "all" returned empty. Map tabs to real statuses (pending/confirmed/completed/cancelled_by_user/ expired) so booked appointments show up. - Booking form now validates required account fields before proceeding to payment (national_code 10 digits, name, family, gender, basic_insurance), in both self and other-person modes, surfacing per-field errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# اجباریکردن فیلدهای اطلاعات حساب کاربری در صفحه نوبتگیری
|
||||
|
||||
## پروژه
|
||||
|
||||
`nobat724_front` (سایت عمومی).
|
||||
|
||||
> کاملاً frontend است. هیچ تغییری در بکاند لازم نیست — فقط اعتبارسنجی سمت کلاینت قبل از ادامه به مرحلهی پرداخت.
|
||||
|
||||
## زمینه
|
||||
|
||||
در صفحهی نوبتگیری (`/appointment/[doctorId]`)، بخش «اطلاعات حساب کاربری» (و در حالت «شخص دیگر»، «اطلاعات کاربر جدید») قبل از رفتن به مرحلهی پرداخت هیچ اعتبارسنجی اجباری ندارد؛ `SubmitData.handleSubmit` فقط `setErrors({})` میکند و مستقیم به API میفرستد، و خطاها فقط از پاسخ سرور برمیگردند. در نتیجه کاربر میتواند بدون نام/کد ملی/جنسیت/بیمه نوبت بگیرد و رکورد ناقص ثبت شود (همان مشکلی که باعث شد نام کاربر خالی بماند).
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
این پنج فیلد در **هر دو حالت** (نوبت برای خود کاربر و برای شخص دیگر) **اجباری** شوند و تا تکمیل نشدن، فرم به مرحلهی بعد نرود و خطای فیلدی نمایش دهد:
|
||||
- کد ملی (`national_code`) — ۱۰ رقم
|
||||
- نام (`name`)
|
||||
- نام خانوادگی (`family`)
|
||||
- جنسیت (`gender`)
|
||||
- نوع بیمه (`basic_insurance`)
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `components/appointment/detail/SubmitData.js` | `handleSubmit` — جای افزودن اعتبارسنجی اجباری قبل از API |
|
||||
| `components/appointment/detail/Form.js` | فیلدها از قبل `error={errors?.<field>}` میگیرند (نیازی به تغییر ساختار نیست) |
|
||||
| `components/appointment/detail/index.js` | `errors` state و `emptyPatientData()` (شکل فیلدها) |
|
||||
|
||||
## وضعیت فعلی (کد واقعی)
|
||||
|
||||
`SubmitData.handleSubmit` — بدون اعتبارسنجی اجباری:
|
||||
```js
|
||||
const handleSubmit = async () => {
|
||||
const isChanged = JSON.stringify(prevData) !== JSON.stringify(data);
|
||||
setErrors({});
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!isForAnother && isChanged) { /* PATCH/POST profile */ }
|
||||
if (doctor && selectedSlot) { /* postAppointment */ }
|
||||
...
|
||||
} catch (error) {
|
||||
if (error?.response?.status === 409) { setStep(0); ... }
|
||||
setErrors(error.response.data); // فقط خطای سرور
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
شکل دادهی هر فیلد (از `detail/index.js`):
|
||||
```js
|
||||
// data.<field> = { value: <...>, isEdit: <bool> }
|
||||
// gender.value: رشته "male"/"female" (یا "" )
|
||||
// basic_insurance.value: آبجکت { id } یا "" (در Form با findBasicInsuranceVal مپ میشود)
|
||||
// national_code/name/family.value: رشته
|
||||
```
|
||||
|
||||
`Form.js` از قبل `error` را به هر فیلد پاس میدهد:
|
||||
```jsx
|
||||
<EditField ... name="national_code" error={errors?.national_code} />
|
||||
<EditField ... name="name" error={errors?.name} />
|
||||
<EditField ... name="family" error={errors?.family} />
|
||||
<DefaultSelect ... name="gender" error={errors?.gender} />
|
||||
<DefaultSelect ... name="basic_insurance" error={errors?.basic_insurance} />
|
||||
```
|
||||
> یعنی فقط کافی است `SubmitData` یک آبجکت `errors` با کلیدهای همین فیلدها بسازد و `setErrors` کند؛ نمایش خطا خودکار کار میکند.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. تابع اعتبارسنجی اجباری در `SubmitData`
|
||||
|
||||
یک `validate(data)` بساز که آبجکت خطا (کلید فیلد → پیام فارسی) برگرداند. در ابتدای `handleSubmit`، قبل از `setLoading(true)` و قبل از هر فراخوانی API، اجرا شود؛ اگر خطا داشت `setErrors(...)` و `return` (توقف).
|
||||
|
||||
```js
|
||||
const validate = () => {
|
||||
const errs = {};
|
||||
const nationalCode = (data?.national_code?.value || "").trim();
|
||||
if (!data?.name?.value?.trim()) errs.name = "نام الزامی است";
|
||||
if (!data?.family?.value?.trim()) errs.family = "نام خانوادگی الزامی است";
|
||||
if (!nationalCode) errs.national_code = "کد ملی الزامی است";
|
||||
else if (!/^\d{10}$/.test(nationalCode)) errs.national_code = "کد ملی باید ۱۰ رقم باشد";
|
||||
// gender.value رشته است
|
||||
if (!data?.gender?.value) errs.gender = "جنسیت الزامی است";
|
||||
// basic_insurance.value آبجکت {id} یا "" است
|
||||
if (!data?.basic_insurance?.value?.id) errs.basic_insurance = "نوع بیمه الزامی است";
|
||||
return errs;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errs = validate();
|
||||
if (Object.keys(errs).length > 0) {
|
||||
setErrors(errs);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
setLoading(true);
|
||||
// ... بقیه بدون تغییر
|
||||
};
|
||||
```
|
||||
|
||||
- این اعتبارسنجی در **هر دو حالت** اجرا شود (هم `isForAnother=false` هم `true`) — یعنی خارج از شرط `if (!isForAnother && isChanged)` و قبل از آن.
|
||||
- `DefaultSelect` (جنسیت/بیمه) باید پیام خطا را نمایش دهد؛ تأیید کن `error` prop در `DefaultSelect` به helperText/حالت خطای MUI وصل است (اگر نیست، اضافه کن — ولی `EditField` این را دارد).
|
||||
|
||||
### ۲. هماهنگی شکل داده
|
||||
|
||||
- `gender.value` رشتهی `"male"`/`"female"` است (از `buildProfileData`/`DefaultSelect`)، پس بررسی `!data.gender.value` درست است.
|
||||
- `basic_insurance.value` یا آبجکت `{ id }` است (از پروفایل) یا کل آیتم بیمه (از انتخاب کاربر در `DefaultSelect`) — هر دو `id` دارند، پس `?.value?.id` درست کار میکند.
|
||||
- کد ملی ممکن است از پروفایلِ تأییدشده `isEdit:false` (قفل) ولی پر باشد — این حالت معتبر است (مقدار دارد، خطا نمیدهد).
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **بکاند تغییر نمیکند**؛ این صرفاً gate سمت کلاینت است. خطاهای سرور (`setErrors(error.response.data)`) در catch دستنخورده بماند.
|
||||
- اعتبارسنجی باید **قبل از** ساخت/ارسال نوبت و پروفایل اجرا شود تا رکورد ناقص ثبت نشود.
|
||||
- پیامها فارسی، فیلدها همان کلیدهایی که `Form.js` انتظار دارد (`name`, `family`, `national_code`, `gender`, `basic_insurance`).
|
||||
- در حالت «شخص دیگر»، فیلد `phone` هم در فرم هست؛ اگر میخواهی آن را هم اجباری کنی به validate اضافه کن — ولی طبق درخواست فقط این پنج فیلد لازماند.
|
||||
- App Router، RTL، MUI v5؛ از `EditField`/`DefaultSelect` موجود استفاده کن، کامپوننت جدید نساز.
|
||||
- بعد از تغییر: `npm run build` بدون خطا؛ دستی تست کن — با فیلدهای خالی دکمهی ادامه نباید جلو برود و باید زیر هر فیلد خطا نشان دهد؛ با تکمیل همه، باید به مرحلهی پرداخت برود.
|
||||
@@ -0,0 +1,93 @@
|
||||
# رفع فیلتر وضعیت در «نوبتهای من» (نوبت ثبتشده نمایش داده نمیشود)
|
||||
|
||||
## پروژه
|
||||
|
||||
`nobat724_front` (سایت عمومی، داشبورد کاربر).
|
||||
|
||||
> این باگ کاملاً frontend است. بکاند درست کار میکند: `GET /api/v1/appointments/user?status=<status>` نوبتهای کاربر فعلی را برمیگرداند و فیلتر `status` را روی مقادیر واقعی نوبت اعمال میکند.
|
||||
|
||||
## زمینه
|
||||
|
||||
کاربر در nobat724 نوبت گرفته (وضعیت `confirmed`) ولی در «نوبتهای من» (تبهای تاریخچه نوبت) چیزی نمایش داده نمیشود. ریشهیابی نشان داد بکاند نوبت را درست برمیگرداند؛ مشکل این است که تبهای فیلتر در frontend مقادیر `status` نادرستی به API میفرستند که با وضعیتهای واقعی بکاند مطابقت ندارند، پس هر تب جز «همه» نتیجهی خالی میدهد.
|
||||
|
||||
تأیید با دادهی واقعی:
|
||||
- `GET /api/v1/appointments/user?status=reserved` → **۰ نوبت** (مقدار `reserved` در بکاند وجود ندارد).
|
||||
- `GET /api/v1/appointments/user?status=confirmed` → **۴ نوبت** (مقدار درست).
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
تبهای `Head.js` این مقادیر را میفرستند:
|
||||
`waiting_for_payment`, `reserved`, `visited`, `auto_cancel_unpaid`, `completed`
|
||||
|
||||
اما وضعیتهای واقعی نوبت در بکاند (همانهایی که `List.js` در `STATUS_LABELS` میشناسد و `Appointment` entity تعریف میکند):
|
||||
`pending`, `confirmed`, `completed`, `cancelled_by_user`, `cancelled_by_doctor`, `expired`, `no_show`
|
||||
|
||||
هدف: نگاشت تبها به مقادیر واقعی + هماهنگکردن برچسبها، تا فیلتر درست کار کند.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `components/dashboard/userAccount/sidebars/turns/Head.js` | تبهای فیلتر — `listTab` و `statusMap` نادرست |
|
||||
| `components/dashboard/userAccount/sidebars/turns/List.js` | `STATUS_LABELS` (مرجع مقادیر واقعی — درست است) |
|
||||
| `components/dashboard/userAccount/sidebars/turns/index.js` | `Turns` — `status` را به `getMyAppointments` پاس میدهد (درست است، تغییر لازم ندارد) |
|
||||
|
||||
## وضعیت فعلی (کد واقعی)
|
||||
|
||||
`Head.js` — نگاشت نادرست:
|
||||
```jsx
|
||||
const listTab = ["همه", "در انتظار پرداخت", "رزرو شده", "ویزیت شده", "لغو خودکار", "تکمیل شده"];
|
||||
|
||||
const statusMap = {
|
||||
0: undefined, // all
|
||||
1: "waiting_for_payment", // ❌ بکاند: pending
|
||||
2: "reserved", // ❌ بکاند: confirmed
|
||||
3: "visited", // ❌ بکاند: completed (یا no_show)
|
||||
4: "auto_cancel_unpaid", // ❌ بکاند: expired
|
||||
5: "completed", // فقط این یکی تصادفاً درست است
|
||||
};
|
||||
```
|
||||
|
||||
`List.js` — مقادیر واقعی (مرجع درست):
|
||||
```jsx
|
||||
const STATUS_LABELS = {
|
||||
pending: "در انتظار پرداخت",
|
||||
confirmed: "تأیید شده",
|
||||
completed: "انجام شده",
|
||||
cancelled_by_user: "لغو شده",
|
||||
cancelled_by_doctor: "لغو توسط پزشک",
|
||||
expired: "منقضی شده",
|
||||
no_show: "عدم مراجعه",
|
||||
};
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. اصلاح نگاشت تبها در `Head.js`
|
||||
`listTab` و `statusMap` را با وضعیتهای واقعی بکاند هماهنگ کن. تبهای پیشنهادی (برچسب فارسی ← مقدار واقعی API):
|
||||
```jsx
|
||||
const listTab = ["همه", "در انتظار پرداخت", "تأیید شده", "انجام شده", "لغو شده", "منقضی شده"];
|
||||
|
||||
const statusMap = {
|
||||
0: undefined, // همه
|
||||
1: "pending",
|
||||
2: "confirmed",
|
||||
3: "completed",
|
||||
4: "cancelled_by_user",
|
||||
5: "expired",
|
||||
};
|
||||
```
|
||||
- برچسبها باید با `STATUS_LABELS` در `List.js` یکدست باشند (مثلاً `confirmed` → «تأیید شده»).
|
||||
- اگر میخواهی «لغو توسط پزشک» و «عدم مراجعه» هم فیلتر شوند، میتوانی تب اضافه کنی؛ ولی چون فیلتر بکاند فقط یک مقدار `status` میگیرد، هر تب باید دقیقاً یک مقدار معتبر بفرستد.
|
||||
|
||||
### ۲. تأیید جریان
|
||||
- `index.js`: `if (status) params.status = status;` — وقتی `undefined` باشد (تب «همه») پارامتر ارسال نمیشود و همهی نوبتها میآیند. این درست است؛ تغییر نده.
|
||||
- مطمئن شو تب پیشفرض «همه» (index 0 → `undefined`) است تا در اولین بارگذاری همهی نوبتها (از جمله `confirmed`) نمایش داده شوند.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **بکاند تغییر نمیکند.** مقادیر معتبر `status` دقیقاً ثابتهای `Appointment` هستند: `pending`, `confirmed`, `completed`, `cancelled_by_user`, `cancelled_by_doctor`, `expired`, `no_show`. هر مقدار دیگری → لیست خالی.
|
||||
- پاسخ `appointments/user` دابلنِست است (`success(['data'=>...])`)؛ `index.js` درست با `response.data.data` میخواند — دست نزن.
|
||||
- تاریخ/ساعت در `List.js` با `convertTimestampToJalali`/`convertTimestampToTime` از `slot_start` (Unix ثانیه) ساخته میشود — درست است.
|
||||
- App Router، RTL، فونت Vazir، MUI v5؛ از کامپوننت `Tabs` موجود استفاده کن.
|
||||
- بعد از تغییر: `npm run build` بدون خطا؛ در داشبورد همهی تبها را تست کن (بهویژه «تأیید شده» که باید نوبت confirmed را نشان دهد، و «همه» که همه را نشان دهد).
|
||||
@@ -34,7 +34,26 @@ function SubmitData({ setStep, data, prevData, setErrors, doctor, isForAnother,
|
||||
}
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const errs = {};
|
||||
const nationalCode = (data?.national_code?.value || "").trim();
|
||||
if (!data?.name?.value?.trim()) errs.name = "نام الزامی است";
|
||||
if (!data?.family?.value?.trim()) errs.family = "نام خانوادگی الزامی است";
|
||||
if (!nationalCode) errs.national_code = "کد ملی الزامی است";
|
||||
else if (!/^\d{10}$/.test(nationalCode))
|
||||
errs.national_code = "کد ملی باید ۱۰ رقم باشد";
|
||||
if (!data?.gender?.value) errs.gender = "جنسیت الزامی است";
|
||||
if (!data?.basic_insurance?.value?.id) errs.basic_insurance = "نوع بیمه الزامی است";
|
||||
return errs;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errs = validate();
|
||||
if (Object.keys(errs).length > 0) {
|
||||
setErrors(errs);
|
||||
return;
|
||||
}
|
||||
|
||||
const isChanged = JSON.stringify(prevData) !== JSON.stringify(data);
|
||||
|
||||
setErrors({});
|
||||
|
||||
@@ -3,20 +3,20 @@ import Tabs from "@/app/component/Tabs";
|
||||
const listTab = [
|
||||
"همه",
|
||||
"در انتظار پرداخت",
|
||||
"رزرو شده",
|
||||
"ویزیت شده",
|
||||
"لغو خودکار",
|
||||
"تکمیل شده",
|
||||
"تأیید شده",
|
||||
"انجام شده",
|
||||
"لغو شده",
|
||||
"منقضی شده",
|
||||
];
|
||||
|
||||
function Head({ status, setStatus }) {
|
||||
const statusMap = {
|
||||
0: undefined, // all
|
||||
1: "waiting_for_payment",
|
||||
2: "reserved",
|
||||
3: "visited",
|
||||
4: "auto_cancel_unpaid",
|
||||
5: "completed",
|
||||
1: "pending",
|
||||
2: "confirmed",
|
||||
3: "completed",
|
||||
4: "cancelled_by_user",
|
||||
5: "expired",
|
||||
};
|
||||
|
||||
const handleChange = (e, value) => {
|
||||
|
||||
Reference in New Issue
Block a user