feat(doctor profile): implement unclaimed doctor handling and UI adjustments

This commit is contained in:
hamed
2026-07-12 10:00:03 +03:30
parent ce05e464c4
commit 42ac6645d5
5 changed files with 195 additions and 25 deletions
@@ -0,0 +1,142 @@
# صفحه عمومی پزشک — رفتار الزامی برای پروفایل بدون صاحب (unclaimed)
## پروژه
`nobat724_front` (سایت عمومی) — **cross-repo**. پرامپت همتای backend:
`clinicpro/.claude/prompt/unclaimed-doctor-null-rating.md`
آن پرامپت backend را **اول** اجرا کن؛ بعد این. backend فیلد `point`/`satisfaction` را برای پروفایل غیر-claimed به `null` برمی‌گرداند و `owner_status` را در پاسخ می‌گذارد.
## زمینه
پزشکانِ ایمپورت‌شده از نظام پزشکی با `owner_status = "unclaimed"` می‌آیند: بدون عکس واقعی، بدون بیوگرافی، بدون آدرس/تلفن واقعی، صفر نظر. اما صفحه‌ی عمومی همان UI پزشک عادی را نشان می‌دهد — امتیاز پیش‌فرض جعلی، تیک تأیید، دکمه‌ی نوبت‌دهی مرده، ایندکس‌شدنِ صفحه‌ی نازک. این هم گمراه‌کننده است، هم برای سئو مضر (near-duplicate + thin content).
هدف: وقتی `doctor.owner_status !== "claimed"` مجموعه‌ای از اصلاحات الزامی اعمال شود؛ و به‌محض claimed شدن، صفحه دقیقاً به حالت عادی برگردد (همه‌ی این شرط‌ها فقط با یک flag گیت می‌شوند، پس خودکار برمی‌گردند).
> شرط واحد در همه‌ی وظایف: `const isUnclaimed = doctor?.owner_status !== "claimed";` (یعنی `unclaimed` یا `pending_transfer`). پروفایل موجودِ عادی `owner_status = "claimed"` دارد → هیچ تغییری نمی‌بیند.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `app/doctor/[slug]/page.js` | Server Component؛ fetch (`getDoctor` L1322)، `generateMetadata` L3980، JSON-LD `aggregateRating` L109161 |
| `components/doctor/detailDoctor/Title.js` | نمایش امتیاز/رضایت (L69–78)، تیک تأیید `TickCircle` (L5558) |
| `components/doctor/claim/index.js` | بخش claim موجود (بنر «این پروفایل بر اساس اطلاعات عمومی...»، گیت با `owner_status !== "unclaimed"` L44) |
| `components/doctor/detailDoctor/cards/comments/index.js` | بلوک نظرات |
| `app/sitemap.js` | `getDoctorUrls` L98–112 — همه‌ی پزشکان بدون فیلتر |
| `app/robots.js` | robots سراسری |
## وضعیت فعلی
نمایش امتیاز (`Title.js` L69–78) — همیشه، حتی وقتی `point` جعلی است:
```jsx
<span>امتیاز: {doctor?.point}</span>
...
<span>{doctor?.satisfaction}% رضایت کاربران</span>
```
تیک تأیید (`Title.js` L5558) — **بدون هیچ شرطی** برای همه‌ی پزشکان:
```jsx
<TickCircle className="hidden lg:flex ..." />
<span>کد نظام پزشکی: {doctor?.medical_system_code}</span>
```
JSON-LD AggregateRating (`page.js` L123130) — فقط با `point > 0` گیت شده:
```jsx
...(Number(doctor.point) > 0 && {
aggregateRating: {
"@type": "AggregateRating",
ratingValue: doctor.point,
bestRating: "5",
ratingCount: comments?.length || 1,
},
}),
```
`generateMetadata` (L3980) — **هیچ `robots`/noindex ندارد**.
`sitemap.js` `getDoctorUrls` (L98–112) — برای هر پزشکِ دارای uuid یک `/doctor/${d.uuid}` می‌سازد، بدون فیلتر claim.
## وظایف
### ۱. مخفی‌کردن امتیاز/رضایت برای unclaimed (مهم‌ترین)
در `Title.js`، بلوک امتیاز و رضایت (L69–78) فقط وقتی نمایش داده شود که امتیاز واقعی وجود دارد. چون backend اکنون `point`/`satisfaction` را `null` می‌کند:
```jsx
{Number(doctor?.point) > 0 && (
// بلوک «امتیاز: ...» و «...% رضایت کاربران»
)}
```
پس برای unclaimed (که `point === null`) اصلاً رندر نمی‌شود. مطمئن شو placeholder «بدون نمره»/«۰ رضایت» جایگزین نمی‌شود — کل بلوک حذف شود.
### ۲. حذف JSON-LD AggregateRating برای unclaimed
با null شدن `point` در backend، شرط موجود `Number(doctor.point) > 0` (page.js L123) خودش `aggregateRating` را حذف می‌کند. **تأیید کن** که با `point = null` این شرط false می‌شود و بلوک `review[]` (L131–138) هم وقتی نظر واقعی نیست خالی/حذف است. در صورت نیاز شرط را صریح‌تر کن:
```jsx
...(!isUnclaimed && Number(doctor.point) > 0 && { aggregateRating: {...} }),
```
### ۳. تیک تأیید فقط برای claimed
در `Title.js` L5558، `TickCircle` را مشروط کن:
```jsx
{!isUnclaimed && <TickCircle className="hidden lg:flex ..." />}
```
کد نظام پزشکی بماند، فقط علامت ✓ سبز برای غیر-claimed حذف شود.
### ۴. noindex روی صفحه‌ی unclaimed
در `generateMetadata` (`page.js` L39–80) وقتی پروفایل claimed نیست، robots را noindex کن:
```jsx
const isUnclaimed = doctor?.owner_status !== "claimed";
return {
// ... title/description/openGraph موجود
robots: isUnclaimed
? { index: false, follow: true }
: undefined,
};
```
(`follow: true` تا لینک‌های داخلی دنبال شوند ولی خود صفحه‌ی نازک ایندکس نشود.)
### ۵. خارج‌کردن unclaimed از sitemap
در `app/sitemap.js` `getDoctorUrls` (L98–112)، فقط پزشکان claimed را وارد کن:
```js
.filter((d) => d?.uuid && d?.owner_status === "claimed")
.map((d) => ({ url: `${base}/doctor/${d.uuid}`, ... }))
```
مطمئن شو پاسخ list (`/api/v1/doctors`, `toListArray`) فیلد `owner_status` دارد (طبق پرامپت backend دارد).
### ۶. برچسب «تأییدنشده» + CTA تصاحب
بخش claim موجود است (`components/doctor/claim/index.js`, mount در `components/doctor/index.js` L45) و با `owner_status === "unclaimed"` نمایش داده می‌شود. کارها:
- یک برچسب کوچک «تأییدنشده» نزدیک نام/تیتر در `Title.js` برای `isUnclaimed` اضافه کن (متنی خنثی، مثلاً badge خاکستری «پروفایل تأییدنشده»).
- بنر claim فعلی گیتش `!== "unclaimed"` است؛ اگر `pending_transfer` هم باید CTA ببیند، شرط را به `owner_status !== "claimed"` گسترش بده (هماهنگ با `isUnclaimed`). در غیر این‌صورت بدون تغییر بماند.
### ۷. مخفی‌کردن بلوک‌های خالی در حالت unclaimed
- بلوک نظرات (`cards/comments/index.js`): وقتی `isUnclaimed` و صفر نظر واقعی، به‌جای «۰ از ۵ / بدون نمره» boilerplate، یا کل بلوک را مخفی کن یا یک متن زمینه‌ای «هنوز نظری ثبت نشده» نشان بده. تکرارِ پنج‌بار «بدون نمره» حذف شود.
- بلوک موقعیت مکانی (`cards/locations/index.js`) already وقتی آدرس واقعی نیست `null` برمی‌گرداند (L5) — نیازی به تغییر نیست، فقط تأیید کن آدرسِ خودکار «مطب دکتر ...» بدون مختصات، map/تلفن جعلی نشان نمی‌دهد.
### ۸. (اختیاری، اولویت پایین) اسلاگ کلیدواژه‌دار
فعلاً slug === uuid (page.js L15, L113؛ sitemap L105؛ CTA `ItemAppointment.js` L57). تغییر به اسلاگ خوانا (مثل `/doctor/سیده-شهلا-حسینی-پاتولوژی-یاسوج`) تغییر بزرگ و ریسک‌دار است (باید redirect uuid→slug، تغییر canonical، هماهنگی با backend برای resolve اسلاگ). **در این پرامپت پیاده نکن** — فقط به‌عنوان کار جدا یادداشت کن. اگر انجام شد، canonical و sitemap و لینک‌های داخلی همه باید هماهنگ شوند و uuid همچنان resolve بماند.
## نکات مهم
- **پاسخ API سه‌لایه است**: `getDoctor` با `json.data.data` می‌خواند (page.js L21). فیلد `owner_status` روی همین object داخلی است.
- **همه‌ی شرط‌ها با یک flag**: `owner_status !== "claimed"`. به‌محض claim شدن (backend → `'claimed'`), همه‌ی این تغییرات خودکار غیرفعال و صفحه عادی می‌شود. هیچ منطق «reset» جدا لازم نیست.
- App Router: `generateMetadata` باید `params` را `await` کند (الگوی پروژه).
- دکمه‌ی نوبت‌دهی («نوبت‌دهی غیرفعال») با `doctor.active`/`free_turn` گیت می‌شود، **نه** با claim. پزشکان ایمپورت‌شده در backend `active_doctor_appointment = true` دارند ولی slot واقعی ندارند → همان «غیرفعال» درست است. تغییرش نده مگر کاربر بخواهد.
- استایل: MUI v5 + Tailwind، RTL، Vazir. badge «تأییدنشده» را با همان توکن‌های رنگی خنثی پروژه بساز.
- بعد از تغییر: `npm run build` برای صحت (بدون خطای type/lint).