# امتیاز چندبُعدی + نظرات غنی (نویسنده، لایک/دیسلایک، پاسخ) برای صفحه پزشک ## پروژه `clinicpro` (Backend — منبع حقیقت). > **Cross-repo:** سایت عمومی `nobat724_front` این قرارداد را مصرف می‌کند تا UI غنیِ نظرات/امتیاز صفحه پزشک را پر کند (پرامپت همتا: `nobat724_front/.claude/prompt/rating-rich-ui-wiring.md`). این پرامپت **اول** اجرا شود. > > این کار قانونِ «نوبت تایید‌شده در ۳۰ روز گذشته» را که قبلاً اضافه شد **نگه می‌دارد**؛ فقط مدل داده‌ی امتیاز/نظر را غنی‌تر می‌کند. ## زمینه و چرایی UI صفحه پزشک در `nobat724_front` (طراحی موجود و تأییدشده) یک نمای غنی دارد که باید حفظ شود: - چارت امتیاز با **۵ بُعد** درصدی: زمان انتظار در مطب، تشخیص درست، برخورد مناسب پزشک، نظافت مطب، مهارت پزشک. - یک **دایره‌ی «مجموع رضایت کاربران»** (درصد). - امتیاز ستاره‌ای کلی (`point` از ۵). - هر نظر با **نام و عکس نویسنده**، **لایک و دیسلایک** (با تعداد و وضعیت کاربر فعلی)، و **پاسخ‌ها (replies)**. مدل فعلی بک‌اند خیلی ساده است (تک‌`score` ۱–۵، نظرِ بدون نویسنده/دیسلایک/پاسخ)، پس UI نمی‌تواند پر شود. هدف: بک‌اند این قرارداد غنی را تأمین کند. ## قرارداد هدف (دقیقاً آنچه UI انتظار دارد) ### الف) `GET /api/v1/rate/{doctorUuid}` (عمومی) — تجمیع امتیاز ```json { "success": true, "data": { "point": 4.4, "satisfaction": 89, "averages": [ { "name": "waiting_time_at_clinic", "label": "زمان انتظار در مطب", "progress": 70 }, { "name": "accuracy_of_diagnosis", "label": "تشخیص درست", "progress": 88 }, { "name": "doctor_behavior", "label": "برخورد مناسب پزشک", "progress": 90 }, { "name": "clinic_cleanliness", "label": "نظافت مطب", "progress": 72 }, { "name": "doctor_expertise", "label": "مهارت پزشک", "progress": 90 } ] } } ``` - `progress` هر بُعد: میانگین آن بُعد روی همه‌ی امتیازها، به‌صورت درصد ۰–۱۰۰ (گرد). - `point`: میانگین کلیِ همه‌ی ابعاد، روی مقیاس ۰–۵ (مثلاً `mean(progress)/20`). - `satisfaction`: همان میانگین کلی به‌صورت درصد ۰–۱۰۰. - اگر هیچ امتیازی نباشد: `point=0`, `satisfaction=0`, ابعاد با `progress=0`. ### ب) `POST /api/v1/rate` (AUTH + گارد واجد بودن موجود) — ثبت امتیاز چندبُعدی Request body: ```json { "doctor_uuid": "…", "waiting_time_at_clinic": 80, "accuracy_of_diagnosis": 100, "doctor_behavior": 100, "clinic_cleanliness": 60, "doctor_expertise": 100 } ``` - هر بُعد عددی ۰–۱۰۰ (UI با Rating ۵ ستاره مقدار `progress = stars*20` می‌فرستد). Validation: هر کدام بین ۰ و ۱۰۰. - Upsert per (user, doctor): اگر امتیاز قبلی بود، به‌روزرسانی شود. ### ج) `GET /api/v1/comments/{doctorUuid}` (عمومی) — هر آیتم نظر ```json { "uuid": "…", "comment": "متن نظر", "created": 1700000000, "parent": null, "author": { "real_name": "میثم امیری", "picture": [{ "url": "/path.jpg" }] }, "like_status": { "like_count": 6, "dislike_count": 1, "current_user_like": { "like": false, "dislike": false } }, "replies": [ /* همان ساختار، به‌صورت تو در تو */ ] } ``` - کلید متن `comment` است (نه `body`). تاریخ `created` (نه `created_at`). - `author.real_name` از `User.realName`؛ `author.picture` آرایه‌ای از `{url}` (اگر کاربر عکس ندارد، آرایه‌ی خالی یا با url پیش‌فرض — هماهنگ با `imageUrl()` فرانت که fallback دارد). - `current_user_like`: اگر درخواست با توکن باشد، وضعیت رأی کاربر فعلی؛ اگر بدون توکن، هر دو `false`. - `replies`: نظرهایی که `parent` آن‌ها این نظر است؛ فقط نظرهای ریشه (parent=null) در سطح بالا برگردند و پاسخ‌ها داخل `replies` تو در تو بیایند. ### د) `POST /api/v1/comment` (AUTH + گارد واجد بودن موجود) — ثبت نظر/پاسخ Request body: ```json { "doctor_uuid": "…", "comment": "متن", "parent": "" } ``` - کلید `comment` (نه `body`). اگر `parent` داده شد، نظرِ پاسخ زیر آن والد ساخته شود. - نکته‌ی سازگاری: فرانت در برخی نقاط `doctor_id`/`comment` می‌فرستد؛ ولی قرارداد رسمی ما `doctor_uuid` است — در پرامپت فرانت همتا، فرستادن `doctor_uuid` تضمین می‌شود. بک‌اند فقط `doctor_uuid` را بپذیرد. ### ه) `POST /api/v1/like/{commentUuid}` (AUTH) — رأی لایک/دیسلایک Request body: ```json { "value": 1 } // 1 = like ، -1 = dislike ``` - toggle: اگر همان رأی دوباره زده شد حذف شود؛ اگر رأی مخالف بود جایگزین شود. - Response: `{ success, data: { like_count, dislike_count, current_user_like: { like, dislike } } }`. ## فایل‌های مرتبط | فایل | کار | |---|---| | `src/Rating/Entity/Rate.php` | جایگزینی تک‌`score` با ۵ ستون بُعدی + متد میانگین‌ها | | `src/Rating/Entity/Comment.php` | افزودن `parent` (self ManyToOne)، `replies` (OneToMany)، `toArray` غنی با author/like_status/replies | | `src/Rating/Entity/Like.php` | افزودن `value` (۱ یا ۱-) برای تفکیک like/dislike | | `src/Rating/Repository/RateRepository.php` | `getAggregate(Doctor): array` (point/satisfaction/averages) | | `src/Rating/Repository/CommentRepository.php` | `findApprovedRootsByDoctor` (parent IS NULL)، شمارش لایک/دیسلایک | | `src/Rating/Repository/LikeRepository.php` | تطبیق با `value` | | `src/Rating/Controller/RatingController.php` | به‌روزرسانی `rate`/`getAverage`/`createComment`/`listComments`/`toggleLike` طبق قرارداد بالا؛ گارد واجد بودن حفظ شود؛ `#[CurrentUser] ?User $user` برای خواندن وضعیت رأی کاربر در endpointهای عمومی | | `src/Auth/Entity/User.php` | بررسی وجود `realName` و فیلد عکس (picture/avatar) — برای ساخت `author` | | migrations | افزودن ستون‌های Rate، `parent_id` در comments، `value` در likes | | `docs/api/rating.md` | بازنویسی کامل قراردادها | ## وضعیت فعلی (کد واقعی) `Rate` فقط `score` دارد (۱–۵). `Comment::toArray()` فقط `{uuid, doctor_uuid, user_uuid, body, status, likes(count), created_at}` می‌دهد — بدون author/like_status/replies/parent. `Like` رأی دودویی بدون جهت دارد (toggleLike فقط وجود/عدم وجود). `getAverage` فقط `{average}` می‌دهد. > ابعاد و labelها (منبع، از فرانت قدیمی `data/progress_detail.json`): > ``` > waiting_time_at_clinic → زمان انتظار در مطب > accuracy_of_diagnosis → تشخیص درست > doctor_behavior → برخورد مناسب پزشک > clinic_cleanliness → نظافت مطب > doctor_expertise → مهارت پزشک > ``` > labelها را به‌صورت ثابت (مثلاً یک const map در `Rate` یا یک enum/array در Controller) نگه‌دار و در `averages` همراه `name` برگردان. ## وظایف ### ۱. Entity `Rate` چندبُعدی - پنج ستون `smallint` (۰–۱۰۰): `waitingTimeAtClinic`, `accuracyOfDiagnosis`, `doctorBehavior`, `clinicCleanliness`, `doctorExpertise`. - constructor و setterها مقادیر را به ۰–۱۰۰ clamp کنند. - متد کمکی `overallPercent(): float` = میانگین ۵ بُعد. - `score` قدیمی را حذف کن (یا تبدیل کن). migration بساز. ### ۲. Entity `Comment` — parent/replies + toArray غنی - `#[ORM\ManyToOne(targetEntity: Comment::class)] private ?Comment $parent` با ستون `parent_id` nullable + `onDelete: CASCADE`. - `#[ORM\OneToMany(mappedBy: 'parent', targetEntity: Comment::class)] private Collection $replies`. - constructor یک پارامتر اختیاری `?Comment $parent = null` بگیرد. - `toArray(?User $currentUser = null): array` غنی طبق قرارداد (ج) — شامل `author`, `like_status`, `replies` (بازگشتی، فقط replies تأییدشده). ### ۳. Entity `Like` جهت‌دار - ستون `value` (`smallint`: ۱ یا ۱-). - constructor `value` بگیرد. Repository متدی برای شمارش `like_count`/`dislike_count` یک نظر و یافتن رأی کاربر فعلی. ### ۴. Repository ها - `RateRepository::getAggregate(Doctor $d): array` → `['point'=>…, 'satisfaction'=>…, 'averages'=>[…]]` با DQL AVG روی هر ستون (اگر صفر رکورد، همه ۰). - `CommentRepository::findApprovedRootsByDoctor(Doctor $d): Comment[]` (status=approved AND parent IS NULL). - `LikeRepository`: `countByComment`, `findByUserAndComment` سازگار با `value`. ### ۵. Controller - `rate()`: پنج بُعد را بخوان/validate (۰–۱۰۰)، گارد `hasRecentConfirmed` را حفظ کن، upsert. - `getAverage()`: خروجی `getAggregate` را برگردان (point/satisfaction/averages). - `createComment()`: `comment` + `parent` اختیاری؛ گارد حفظ شود. - `listComments()`: ریشه‌ها را با `toArray($currentUser)` برگردان؛ کاربر فعلی را با `#[CurrentUser] ?User $user` بخوان (روی endpoint عمومی، nullable). - `toggleLike()`: `value` بخوان (۱/۱-)؛ toggle/replace؛ شمارش‌ها را برگردان. > چون `listComments` و `getAverage` عمومی‌اند ولی برای `current_user_like` به کاربر اختیاری نیاز دارند: `#[CurrentUser] ?User $user = null` کار می‌کند چون firewall عمومی توکن را پردازش نمی‌کند — در این صورت همیشه null خواهد بود. **اگر می‌خواهی وضعیت رأی کاربر در لیست عمومی نمایش داده شود، این endpointها باید توکن اختیاری را بپذیرند.** ساده‌ترین راه بدون پیچیدگی firewall: یک پارامتر کوئری یا هدر بررسی نشود؛ به‌جایش فرانت پس از لاگین، رأی‌ها را سمت کلاینت مدیریت کند. **تصمیم پیش‌فرض:** `current_user_like` را در لیست عمومی همیشه `{like:false,dislike:false}` برگردان و به‌روزرسانی واقعی را به پاسخِ `toggleLike` بسپار (UI optimistic). این از تغییر firewall جلوگیری می‌کند. ### ۶. Migration ها بعد از تغییر Entityها: `doctrine:migrations:diff` و `migrate`. داده‌ی موجود `rates.score` در صورت وجود → می‌توان در migration به ابعاد map کرد (یا چون داده‌ی واقعی نیست، drop/recreate ساده‌تر است). ### ۷. مستندسازی `docs/api/rating.md` همه‌ی قراردادهای الف–ه را با مثال JSON واقعی بازنویسی کن. ## نکات مهم - **گارد واجد بودن (`hasRecentConfirmed`) روی `rate` و `comment` حفظ شود** — قبلاً اضافه و تست شده. - `User.realName` و فیلد عکس را قبل از استفاده در `author` تأیید کن (فایل `src/Auth/Entity/User.php`). اگر عکس روی User نیست، `picture: []` برگردان (فرانت fallback دارد). - تاریخ‌ها Unix ثانیه؛ کلید `created` (نه `created_at`) در نظرها — مطابق UI. - پاسخ‌ها (`replies`) فقط تأییدشده‌ها؛ عمق بازگشت یک سطح کافی است (UI تو در توی عمیق ندارد). - همه پاسخ‌ها از `BaseController`. - بعد از تغییر route/Entity: `cache:clear` + `migrate` + تست با `debug:router`. - `docs/api/rating.md` در همین session به‌روز شود (Standing Rule).