From 42ac6645d56c0cb13c9b32aef306743e237682e7 Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Sun, 12 Jul 2026 10:00:03 +0330
Subject: [PATCH] feat(doctor profile): implement unclaimed doctor handling and
UI adjustments
---
.../prompt/unclaimed-doctor-public-page.md | 142 ++++++++++++++++++
app/doctor/[slug]/page.js | 5 +-
app/sitemap.js | 2 +-
components/doctor/detailDoctor/Title.js | 55 ++++---
.../detailDoctor/cards/comments/index.js | 16 +-
5 files changed, 195 insertions(+), 25 deletions(-)
create mode 100644 .claude/prompt/unclaimed-doctor-public-page.md
diff --git a/.claude/prompt/unclaimed-doctor-public-page.md b/.claude/prompt/unclaimed-doctor-public-page.md
new file mode 100644
index 0000000..21539d6
--- /dev/null
+++ b/.claude/prompt/unclaimed-doctor-public-page.md
@@ -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` L13–22)، `generateMetadata` L39–80، JSON-LD `aggregateRating` L109–161 |
+| `components/doctor/detailDoctor/Title.js` | نمایش امتیاز/رضایت (L69–78)، تیک تأیید `TickCircle` (L55–58) |
+| `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
+امتیاز: {doctor?.point}
+...
+{doctor?.satisfaction}% رضایت کاربران
+```
+
+تیک تأیید (`Title.js` L55–58) — **بدون هیچ شرطی** برای همهی پزشکان:
+
+```jsx
+
+کد نظام پزشکی: {doctor?.medical_system_code}
+```
+
+JSON-LD AggregateRating (`page.js` L123–130) — فقط با `point > 0` گیت شده:
+
+```jsx
+...(Number(doctor.point) > 0 && {
+ aggregateRating: {
+ "@type": "AggregateRating",
+ ratingValue: doctor.point,
+ bestRating: "5",
+ ratingCount: comments?.length || 1,
+ },
+}),
+```
+
+`generateMetadata` (L39–80) — **هیچ `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` L55–58، `TickCircle` را مشروط کن:
+
+```jsx
+{!isUnclaimed && }
+```
+
+کد نظام پزشکی بماند، فقط علامت ✓ سبز برای غیر-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).
diff --git a/app/doctor/[slug]/page.js b/app/doctor/[slug]/page.js
index 9044c6b..1b7eccb 100644
--- a/app/doctor/[slug]/page.js
+++ b/app/doctor/[slug]/page.js
@@ -57,10 +57,12 @@ export async function generateMetadata({ params }) {
`رزرو نوبت آنلاین دکتر ${doctor.name}${specialtyNames ? " متخصص " + specialtyNames : ""}${doctor.address ? " | " + doctor.address : ""}`.trim()
).slice(0, 160);
const image = imageUrl(doctor.img?.[0]?.url) || FALLBACK_IMG;
+ const isUnclaimed = doctor.owner_status !== "claimed";
return {
title,
description,
+ ...(isUnclaimed && { robots: { index: false, follow: true } }),
openGraph: {
title,
description,
@@ -106,6 +108,7 @@ async function Doctor({ params }) {
const addresses = doctor ? await getDoctorAddresses(doctor.id) : [];
const specialtyNames = doctor?.specialties?.map((s) => s.name).join(" و ") || "";
+ const isUnclaimed = doctor?.owner_status !== "claimed";
const jsonLd = doctor
? {
"@context": "https://schema.org",
@@ -120,7 +123,7 @@ async function Doctor({ params }) {
url: imageUrl(doctor.img[0].url),
},
}),
- ...(Number(doctor.point) > 0 && {
+ ...(!isUnclaimed && Number(doctor.point) > 0 && {
aggregateRating: {
"@type": "AggregateRating",
ratingValue: doctor.point,
diff --git a/app/sitemap.js b/app/sitemap.js
index 9ab07d2..0d9fd3c 100644
--- a/app/sitemap.js
+++ b/app/sitemap.js
@@ -98,7 +98,7 @@ function cityFilterParams(scope) {
async function getDoctorUrls(baseUrl, scope) {
const doctors = await fetchAllPages('/api/v1/doctors', cityFilterParams(scope));
return doctors
- .filter((d) => d?.uuid)
+ .filter((d) => d?.uuid && d?.owner_status === 'claimed')
.map((d) =>
withLastModified(
{
diff --git a/components/doctor/detailDoctor/Title.js b/components/doctor/detailDoctor/Title.js
index 15b43ab..973d1bf 100644
--- a/components/doctor/detailDoctor/Title.js
+++ b/components/doctor/detailDoctor/Title.js
@@ -20,6 +20,8 @@ const SOCIAL_ICONS = {
};
function Title({ doctor }) {
+ const isUnclaimed = doctor?.owner_status !== "claimed";
+ const hasRating = Number(doctor?.point) > 0;
return (
@@ -31,9 +33,16 @@ function Title({ doctor }) {
-
- {doctor?.name}
-
+
+
+ {doctor?.name}
+
+ {isUnclaimed && (
+
+ پروفایل تأییدنشده
+
+ )}
+
@@ -53,9 +62,11 @@ function Title({ doctor }) {
)}
-
-
-
+ {!isUnclaimed && (
+
+
+
+ )}
کد نظام پزشکی: {doctor?.medical_system_code}
@@ -63,22 +74,24 @@ function Title({ doctor }) {
-
-
-
-
-
- امتیاز: {doctor?.point}
-
+ {hasRating && (
+
+
+
+
+
+ امتیاز: {doctor?.point}
+
+
+
+
+
+ {doctor?.satisfaction}% رضایت کاربران
+
+
-
-
-
- {doctor?.satisfaction}% رضایت کاربران
-
-
-
-
+
+ )}
{doctor?.social_media && Object.values(doctor.social_media).some(Boolean) && (
{Object.entries(doctor.social_media).map(([key, url]) => {
diff --git a/components/doctor/detailDoctor/cards/comments/index.js b/components/doctor/detailDoctor/cards/comments/index.js
index 018c7a7..0c31ddb 100644
--- a/components/doctor/detailDoctor/cards/comments/index.js
+++ b/components/doctor/detailDoctor/cards/comments/index.js
@@ -5,6 +5,10 @@ import Comment from "@/app/component/comment";
import ModalAnswer from "./modal/ModalAnswer";
function Comments({ doctor, comments, rateAggregate }) {
+ const isUnclaimed = doctor?.owner_status !== "claimed";
+ const hasComments = comments?.length > 0;
+ const hideRatingBlock = isUnclaimed && !hasComments;
+
return (
-
-
+ {hideRatingBlock ? (
+
+ هنوز نظری برای این پزشک ثبت نشده است.
+
+ ) : (
+ <>
+
+
+ >
+ )}
);
}