- Add task for completing service mode in clinicpro with detailed objectives and acceptance criteria. - Create architecture documentation for task 00b, outlining involved components and necessary changes. - Develop checklist for task 00b to ensure all requirements are met. - Document implementation notes for task 00b, emphasizing API contract checks and design system adherence. - Update task documentation for task 00b, specifying goals and current issues with service mode.
204 lines
9.9 KiB
Markdown
204 lines
9.9 KiB
Markdown
# معماری — تسک ۰۰ب
|
||
|
||
پروژه: `nobat724_front` · Next.js 15 App Router · MUI v5 + Tailwind · RTL · Vazir
|
||
|
||
## فایلهای درگیر
|
||
|
||
```
|
||
components/appointment/
|
||
├── index.js # ارکستراتور مراحل — تغییر جزئی
|
||
├── service/index.js # ⚠️ بازنویسی با توکن تم
|
||
├── date/index.js # مصرف adaptServiceSlots
|
||
└── detail/SubmitData.js # تغییر جزئی
|
||
|
||
lib/appointmentSlots.js # adaptServiceSlots شیفتآگاه
|
||
services/response.js # endpoint های جدید تسک ۰۰
|
||
|
||
components/dashboard/userAccount/sidebars/turns/
|
||
├── Card.js # + سرویس و مدت
|
||
├── isTurnsDetails/DetailLg.js
|
||
├── isTurnsDetails/DetailSm.js
|
||
└── isTurnsDetails/ButtonData.js # + جابهجایی سرویسآگاه
|
||
```
|
||
|
||
## ۱. بازنویسی `service/index.js` — توکن، نه hex
|
||
|
||
وضعیت فعلی چهار رنگ hard-code دارد و در دارکمود میشکند:
|
||
|
||
```jsx
|
||
// وضعیت فعلی
|
||
<h2 className="text-[16px] font-bold text-[#3B3B3B] mb-4">۱. انتخاب سرویس</h2>
|
||
className={active ? "border-[#5559CE] bg-[#5559CE]/5"
|
||
: "border-gray-200 bg-white hover:border-[#5559CE]"}
|
||
```
|
||
|
||
```jsx
|
||
// هدف — همان ساختار DOM، رنگ از تم
|
||
<h2 className="text-base font-bold text-foreground mb-4">۱. انتخاب سرویس</h2>
|
||
className={active
|
||
? "border-primary bg-primary/5"
|
||
: "border-border bg-surface hover:border-primary"}
|
||
```
|
||
|
||
⚠️ **نام دقیق کلاسها را از `tailwind.config.js` و `mui/index.js` همین پروژه بردار.**
|
||
اسمهای بالا نمونهاند. قاعده: هر رنگی که در بقیهٔ مراحل رزرو (`location/`، `date/`،
|
||
`information/`) استفاده میشود، اینجا هم همان — نه یک پالت جدید.
|
||
|
||
**رفتار عوض نمیشود:** همان toggle، همان ساختار، همان متنها. فقط منبع رنگ.
|
||
|
||
اگر پروژه توکن معادل ندارد (مثلاً `bg-surface` تعریف نشده)، از همان الگویی استفاده کن
|
||
که مرحلهٔ قبلی (`location/index.js`) دارد — نه ساختن توکن جدید در این تسک.
|
||
|
||
## ۲. حذف محاسبهٔ موازی مدت
|
||
|
||
```js
|
||
// ❌ وضعیت فعلی — منبع دوم حقیقت
|
||
const totalMinutes = services
|
||
.filter((s) => draft.includes(s.uuid))
|
||
.reduce((sum, s) => sum + (Number(s.duration_minutes) || 0), 0);
|
||
```
|
||
|
||
مدت باید از پاسخ `appointment-service-slots` بیاید که از قبل `total_duration_minutes` و
|
||
`buffer_minutes` دارد. ولی یک مسئلهٔ ترتیبی هست: مرحلهٔ انتخاب سرویس **پیش از** انتخاب
|
||
روز است، و آن endpoint تاریخ میخواهد.
|
||
|
||
دو گزینه:
|
||
|
||
| گزینه | ارزیابی |
|
||
|---|---|
|
||
| فراخوانی `appointment-service-slots` با تاریخ امروز فقط برای گرفتن مدت | یک درخواست اضافه، و اگر امروز تعطیل باشد پاسخ خالی است ولی `total_duration_minutes` همچنان میآید ✅ |
|
||
| نگهداشتن محاسبهٔ فرانت بهعنوان تخمین + اصلاح در مرحلهٔ بعد | بیمار دو عدد متفاوت میبیند ❌ |
|
||
|
||
**انتخاب: گزینهٔ اول**، با یک تفاوت مهم — مدت **تخمینی** برچسب میگیرد تا وقتی روز
|
||
انتخاب نشده:
|
||
|
||
```js
|
||
// مرحلهٔ انتخاب سرویس
|
||
const { data } = useServiceDuration(doctorUuid, clinicUuid, draft); // hook جدید
|
||
const minutes = data?.total_duration_minutes ?? fallbackSum(draft); // fallback با console.warn
|
||
|
||
<span>مدت تقریبی: {minutes} دقیقه</span> // پیش از انتخاب روز
|
||
<span>مدت نوبت: {minutes} دقیقه</span> // پس از انتخاب روز، از همان پاسخ
|
||
```
|
||
|
||
`fallbackSum` فقط برای بکاند قدیمی است و `console.warn` میزند. حذفش پس از deploy تسک ۰۰.
|
||
|
||
## ۳. `adaptServiceSlots` شیفتآگاه
|
||
|
||
مشکل: همهٔ زمانها در یک تب با برچسب ثابت «زمانهای خالی» جمع میشوند و `end_time`
|
||
اشتباه است (پایانِ آخرین **شروع**، نه پایان نوبت).
|
||
|
||
```js
|
||
// هدف — گروهبندی بر اساس شکاف زمانی، با برچسب واقعی
|
||
export function adaptServiceSlots(slotsResponse) {
|
||
const payload = slotsResponse?.data ?? slotsResponse ?? {};
|
||
const starts = payload.start_times ?? [];
|
||
if (!starts.length) return [];
|
||
|
||
const durationMin = Number(payload.total_duration_minutes) || 0;
|
||
const GAP_THRESHOLD_MIN = 60; // شکاف بیشتر از یک ساعت = شیفت جدا
|
||
|
||
const groups = [];
|
||
let current = null;
|
||
|
||
for (const s of starts) {
|
||
const gapMin = current
|
||
? (s.start - current.slots[current.slots.length - 1].start) / 60
|
||
: Infinity;
|
||
|
||
if (!current || gapMin > GAP_THRESHOLD_MIN) {
|
||
current = { slots: [] };
|
||
groups.push(current);
|
||
}
|
||
current.slots.push({ ...s, is_available: true });
|
||
}
|
||
|
||
return groups.map((g) => {
|
||
const first = g.slots[0];
|
||
const last = g.slots[g.slots.length - 1];
|
||
const endTime = last.end_time ?? addMinutes(last.start_time, durationMin);
|
||
return {
|
||
start_time: first.start_time,
|
||
end_time: endTime,
|
||
label: `${first.start_time} - ${endTime}`, // ← همان قالب حالت اسلاتی
|
||
slots: g.slots,
|
||
};
|
||
});
|
||
}
|
||
```
|
||
|
||
`end_time` هر start از قبل در پاسخ بکاند هست (`getServiceStartTimes` هر آیتم را با
|
||
`end_time` میدهد) — پس `addMinutes` فقط fallback است.
|
||
|
||
**چرا آستانهٔ شکاف و نه اطلاعات شیفت از بکاند؟** پاسخ `appointment-service-slots`
|
||
امروز فقط `start_times` مسطح میدهد و شیفت را نمیگوید. دو راه بود:
|
||
|
||
| راه | ارزیابی |
|
||
|---|---|
|
||
| افزودن گروهبندی شیفت به پاسخ بکاند | درستتر، ولی تغییر قرارداد endpoint که سه کلاینت مصرفش میکنند — و تسک ۰۰ آن را قفل نکرده ولی بازش هم نکرده |
|
||
| **گروهبندی هیوریستیک در فرانت** ✅ | بدون تغییر قرارداد؛ برای شیفت صبح/عصر (شکاف معمولاً ۲-۳ ساعت) دقیق است |
|
||
|
||
انتخاب دوم برای این تسک. اگر بعداً دقت کافی نبود، تسک ۰۶ که `AvailabilityEngine` را
|
||
میسازد میتواند گروهبندی واقعی را در پاسخِ **endpoint جدید** بدهد — بدون دست زدن به
|
||
این یکی. این تصمیم را در `docs/api/appointment.md` سمت بکاند هم یادداشت کن.
|
||
|
||
⛔ `adaptSlots()` (حالت اسلاتی) **یک خط هم** عوض نمیشود.
|
||
|
||
## ۴. سرویس و مدت در پنل کاربر
|
||
|
||
`GET /api/v1/appointments/user` از قبل چه میدهد؟ **پیش از کدنویسی بررسی کن.** اگر
|
||
`service_items` و `service_total_minutes` در پاسخ نیست:
|
||
|
||
- ستون `service_total_minutes` در تسک ۰۰ اضافه شد ✅
|
||
- افزودنش به سریالایزر پاسخ، **بخشی از تسک ۰۰** است (ردیف ۱.۱۶ چکلیستش)
|
||
- اگر جا افتاده، اینجا بهعنوان یک ردیف ⚠️ ثبت و به تسک ۰۰ برگردان
|
||
|
||
```jsx
|
||
// Card.js — دو خط جدید، فقط وقتی داده هست
|
||
{turn.service_items?.length > 0 && (
|
||
<span className="…">{turn.service_items.map((s) => s.name).join("، ")}</span>
|
||
)}
|
||
{turn.service_total_minutes && (
|
||
<span className="…">{turn.service_total_minutes} دقیقه</span>
|
||
)}
|
||
```
|
||
|
||
شرط `&&` اجباری است: نوبت اسلاتی این دو را ندارد و کارتش باید **دقیقاً** مثل امروز
|
||
بماند. نوبت سرویسیِ قدیمی هم ممکن است `service_items` خالی داشته باشد → نام «—».
|
||
|
||
## ۵. جابهجایی سرویسآگاه از پنل
|
||
|
||
`services/response.js` سه متد جدید میگیرد:
|
||
|
||
```js
|
||
serviceReschedule: (uuid, body) =>
|
||
request.post(`api/v1/appointment/${uuid}/service-reschedule`, body, { requireAuth: true }),
|
||
|
||
getServiceSlotsForReschedule: (doctor_uuid, date, service_uuids, exclude_uuid, clinic_uuid) =>
|
||
request.get(
|
||
`api/v1/appointment-service-slots?doctor_uuid=${doctor_uuid}&date=${date}` +
|
||
service_uuids.map((u) => `&service_item_uuids[]=${encodeURIComponent(u)}`).join("") +
|
||
`&exclude_appointment_uuid=${exclude_uuid}` + clinicQuery(clinic_uuid),
|
||
{ requireAuth: true }
|
||
),
|
||
```
|
||
|
||
`ButtonData.js` یک دکمهٔ «جابهجایی» میگیرد که مودال موجود
|
||
(`isTurnsDetails/modal/index.js`) را با کامپوننت انتخاب زمان باز میکند — همان
|
||
`components/appointment/date/` بازاستفاده میشود، نه یک انتخابگر جدید.
|
||
|
||
بیمار **مدت را وارد نمیکند**: `service-reschedule` فقط `start` میگیرد و مدت را از
|
||
سرویسهای موجود نوبت حساب میکند (تسک ۰۰، بخش `ServiceRescheduleService`).
|
||
|
||
## UI — قواعد اجباری
|
||
|
||
رجوع: [_shared/ui-conventions.md](../_shared/ui-conventions.md)، بخش `nobat724_front`
|
||
|
||
- تم MUI از `mui/index.js` — تم جدید نساز
|
||
- فونت فقط Vazir از `app/globals.css`
|
||
- `darkMode: "class"`؛ صفحات عمومی `data-theme`، پنل `class` — هر دو بررسی شوند
|
||
- کامپوننتهای موجود `components/appointment/*` توسعه داده شوند، مسیر موازی نه
|
||
- تاریخ شمسی با `jalali-moment`
|
||
- RTL — `ms-*`/`me-*`
|
||
- هر صفحهای که دست خورد، `generateMetadata` و `await params` سالم بماند
|