From 1522ee8b73c88c9f4bb0d923910a9c882567e6aa Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Sun, 19 Jul 2026 16:44:06 +0330
Subject: [PATCH] feat: enhance clinic contact information display with new
helper functions for phone, city, and state
---
.claude/prompt/clinic-contact-info-fix.md | 268 ++++++++++++++++++
app/clinic/[slug]/page.js | 18 +-
.../clinic/components/contact/Detail.js | 16 +-
components/clinic/components/contact/index.js | 36 +--
.../detailDoctor/cards/locations/Item.js | 2 +-
lib/clinicContact.js | 51 ++++
6 files changed, 366 insertions(+), 25 deletions(-)
create mode 100644 .claude/prompt/clinic-contact-info-fix.md
create mode 100644 lib/clinicContact.js
diff --git a/.claude/prompt/clinic-contact-info-fix.md b/.claude/prompt/clinic-contact-info-fix.md
new file mode 100644
index 0000000..a6ffb83
--- /dev/null
+++ b/.claude/prompt/clinic-contact-info-fix.md
@@ -0,0 +1,268 @@
+# اصلاح بخش «اطلاعات تماس» صفحه کلینیک
+
+## پروژه
+
+`nobat724_front`
+
+## زمینه
+
+صفحه عمومی کلینیک (`/clinic/[uuid]`) یک کارت «اطلاعات تماس» دارد که روزهای کاری، تلفن، آدرس و نقشه را نشان میدهد. روی نمونه واقعی:
+
+`http://karaj-nobat.localhost:3000/clinic/bcb00726-2343-4d63-90c6-d0175cc74591`
+
+پاسخ واقعی API (`GET /api/v1/clinic/{uuid}` روی `https://clinic-pro.ddev.site`) این است:
+
+```json
+{
+ "name": "کلینیک تست QA",
+ "title": "کلینیک تست QA",
+ "phone": "03531234567",
+ "phone_number": "03531234567",
+ "city": [{ "id": "105", "name": "کرج", "parent": "5" }],
+ "state": [{ "id": "5", "name": "البرز" }],
+ "location": "یزد، خیابان تست QA",
+ "map": { "latitude": null, "longitude": null },
+ "24_7": false,
+ "field_working_days": null
+}
+```
+
+اطلاعات تماسی که روی صفحه رندر میشود با این داده همخوان نیست.
+
+## مشکل / هدف
+
+۱. **شهر/استان اصلاً نمایش داده نمیشود.** API فیلدهای `city[0].name` و `state[0].name` را میفرستد ولی UI فقط رشتهٔ آزاد `location` را چاپ میکند. نتیجه: کاربر روی دامنهٔ کرج آدرس «یزد، خیابان تست QA» میبیند بدون هیچ نشانهای از شهر واقعی کلینیک (کرج/البرز).
+
+۲. **تلفن خام رندر میشود.** `03531234567` بدون هیچ جداکنندهای نمایش داده میشود و مقدار `href="tel:..."` هم مستقیم از همان رشته ساخته میشود (اگر مقدار DB فاصله یا `-` داشته باشد، `tel:` خراب میشود).
+
+۳. **ناسازگاری منبع تلفن بین UI و JSON-LD.** کامپوننت `Detail.js` از `phone_number || phone` میخواند اما JSON-LD در `page.js` فقط `clinic.phone` را میخواند. اگر یکی پر و دیگری خالی باشد، صفحه و structured data دو چیز متفاوت میگویند.
+
+۴. **`PostalAddress` در JSON-LD ناقص است.** فقط `streetAddress` و `addressCountry` دارد؛ `addressLocality` (شهر) و `addressRegion` (استان) ندارند در حالی که دادهاش موجود است.
+
+۵. **کارت خالی.** وقتی `field_working_days` و `phone` و `location` همه null باشند و مختصات هم نباشد، `Detail` مقدار `null` برمیگرداند و `mapQuery` خالی است — ولی کارت `
` همچنان رندر میشود و یک باکس خالی روی صفحه میماند.
+
+## فایلهای مرتبط
+
+| فایل | نقش |
+|------|-----|
+| `nobat724_front/components/clinic/components/contact/Detail.js` | رندر ردیفهای روزهای کاری / تلفن / آدرس |
+| `nobat724_front/components/clinic/components/contact/index.js` | کارت اطلاعات تماس + نقشه + دکمه مسیریابی |
+| `nobat724_front/components/clinic/components/contact/ContentDetail.js` | ردیف `head`/`detail` (تغییر ندارد) |
+| `nobat724_front/app/clinic/[slug]/page.js` | JSON-LD نوع `MedicalClinic` (فیلدهای `telephone` و `address`) |
+
+## وضعیت فعلی
+
+`components/clinic/components/contact/Detail.js`:
+
+```jsx
+function Detail({ data }) {
+ const workingDays = data?.["24_7"]
+ ? "۲۴ ساعته، تمام روزهای هفته"
+ : data?.field_working_days?.trim();
+ const phone = (data?.phone_number || data?.phone || "").trim();
+ const address = data?.location?.trim();
+
+ if (!workingDays && !phone && !address) return null;
+
+ return (
+
+ {workingDays && (
+
+ )}
+ {phone && (
+
+ {phone}
+
+ }
+ />
+ )}
+ {address && }
+
+ );
+}
+```
+
+`app/clinic/[slug]/page.js` (بخش JSON-LD):
+
+```js
+...(clinic.phone && { telephone: clinic.phone }),
+...(clinic.location && {
+ address: {
+ "@type": "PostalAddress",
+ streetAddress: clinic.location,
+ addressCountry: "IR",
+ },
+}),
+```
+
+## وظایف
+
+### ۱. ساخت helper مشترک برای اطلاعات تماس کلینیک
+
+فایل جدید `nobat724_front/lib/clinicContact.js` بساز تا هم UI و هم JSON-LD از یک منبع بخوانند (رفع مشکل ۳):
+
+```js
+// lib/clinicContact.js
+
+/** API هر دو کلید را میفرستد؛ یکی ممکن است null باشد. یک منبع واحد. */
+export function getClinicPhone(clinic) {
+ return (clinic?.phone_number || clinic?.phone || "").toString().trim();
+}
+
+/** فقط رقم — برای href="tel:" تا فاصله/خط تیرهٔ داخل DB لینک را خراب نکند. */
+export function telHref(phone) {
+ const digits = (phone || "").replace(/[^\d+]/g, "");
+ return digits ? `tel:${digits}` : null;
+}
+
+/** 03531234567 → +983531234567 برای schema.org telephone */
+export function toE164Ir(phone) {
+ const d = (phone || "").replace(/\D/g, "");
+ if (!d) return null;
+ if (d.startsWith("98")) return `+${d}`;
+ if (d.startsWith("0")) return `+98${d.slice(1)}`;
+ return `+98${d}`;
+}
+
+export function getClinicCity(clinic) {
+ return clinic?.city?.[0]?.name?.trim() || null;
+}
+
+export function getClinicState(clinic) {
+ return clinic?.state?.[0]?.name?.trim() || null;
+}
+
+/**
+ * آدرس نمایشی: «استان، شهر — نشانی».
+ * اگر خودِ location قبلاً نام شهر را داشته باشد دوباره تکرار نمیشود.
+ */
+export function getClinicAddress(clinic) {
+ const street = clinic?.location?.trim() || "";
+ const city = getClinicCity(clinic);
+ const state = getClinicState(clinic);
+
+ const parts = [];
+ if (state && !street.includes(state)) parts.push(state);
+ if (city && !street.includes(city)) parts.push(city);
+ if (street) parts.push(street);
+
+ return parts.length ? parts.join("، ") : null;
+}
+
+/** آیا اصلاً چیزی برای نمایش در کارت تماس هست؟ */
+export function hasClinicContact(clinic) {
+ return Boolean(
+ clinic?.["24_7"] ||
+ clinic?.field_working_days?.trim() ||
+ getClinicPhone(clinic) ||
+ getClinicAddress(clinic) ||
+ (clinic?.map?.latitude && clinic?.map?.longitude)
+ );
+}
+```
+
+### ۲. اصلاح `Detail.js`
+
+- تلفن و آدرس را از helper بگیر.
+- `tel:` را از `telHref()` بساز؛ اگر null بود فقط متن ساده رندر کن (نه لینک شکسته).
+- شهر/استان را در ردیف آدرس نشان بده.
+
+```jsx
+import ContentDetail from "./ContentDetail";
+import { getClinicPhone, telHref, getClinicAddress } from "@/lib/clinicContact";
+
+function Detail({ data }) {
+ const workingDays = data?.["24_7"]
+ ? "۲۴ ساعته، تمام روزهای هفته"
+ : data?.field_working_days?.trim();
+ const phone = getClinicPhone(data);
+ const href = telHref(phone);
+ const address = getClinicAddress(data);
+
+ if (!workingDays && !phone && !address) return null;
+
+ return (
+
+ {workingDays && }
+ {phone && (
+
+ {phone}
+
+ ) : (
+ {phone}
+ )
+ }
+ />
+ )}
+ {address && }
+
+ );
+}
+```
+
+### ۳. جلوگیری از کارت خالی در `contact/index.js`
+
+در ابتدای `Contact`، اگر `hasClinicContact(data)` نادرست بود `null` برگردان تا باکس border-dar خالی رندر نشود:
+
+```jsx
+import { hasClinicContact } from "@/lib/clinicContact";
+
+function Contact({ data }) {
+ const [open, setOpen] = useState(false);
+ // ...
+ if (!hasClinicContact(data)) return null;
+ // ...
+}
+```
+
+> هوکها باید **قبل** از این return صدا زده شوند (قانون hooks) — `useState` را بالای شرط نگه دار.
+
+همچنین در ساخت `mapQuery`، بهجای `data.location` از `getClinicAddress(data)` استفاده کن تا وقتی مختصات نیست، کوئری نقشه شامل شهر/استان باشد و پین به شهر درست بیفتد (نمونهٔ فعلی: `location = "یزد، خیابان تست QA"` ولی شهر واقعی «کرج» است — بدون شهر، نقشه یزد را نشان میدهد).
+
+### ۴. تکمیل JSON-LD در `app/clinic/[slug]/page.js`
+
+```js
+import {
+ getClinicPhone,
+ toE164Ir,
+ getClinicCity,
+ getClinicState,
+} from "@/lib/clinicContact";
+
+// ...
+const clinicPhone = getClinicPhone(clinic);
+const clinicCity = getClinicCity(clinic);
+const clinicState = getClinicState(clinic);
+
+const jsonLd = clinic ? {
+ // ...
+ ...(clinicPhone && { telephone: toE164Ir(clinicPhone) }),
+ ...((clinic.location || clinicCity) && {
+ address: {
+ "@type": "PostalAddress",
+ ...(clinic.location && { streetAddress: clinic.location.trim() }),
+ ...(clinicCity && { addressLocality: clinicCity }),
+ ...(clinicState && { addressRegion: clinicState }),
+ addressCountry: "IR",
+ },
+ }),
+ // ...
+} : null;
+```
+
+## نکات مهم
+
+- **Server/Client:** `contact/index.js` کلاینت است (`useState`)؛ `lib/clinicContact.js` باید pure و بدون وابستگی به `next/headers` بماند تا هم در Server Component (`page.js`) و هم در Client Component قابل import باشد.
+- **مقدار `null` رشتهای:** API برای فیلدهای پرنشده `null` میفرستد؛ هیچجا مستقیم داخل template string نگذار (کامنت موجود در `Detail.js` همین را هشدار میدهد) — همهٔ helperها باید `null` برگردانند نه رشتهٔ خالیِ درجشده.
+- **`24_7` کلید عددیشروع است** — همیشه با bracket notation (`clinic["24_7"]`) خوانده شود.
+- **تکرار شهر:** بعضی رکوردها نام شهر را داخل خود `location` دارند؛ منطق `getClinicAddress` باید تکرار را حذف کند (تست: `location="کرج، بلوار..."` + `city="کرج"` → خروجی نباید «کرج، کرج، بلوار...» باشد).
+- **JSON-LD sanitize:** خروجی همچنان باید از `safeJsonLd()` عبور کند (الگوی فعلی `page.js`).
+- **تغییر backend لازم نیست** — همهٔ فیلدها (`city`, `state`, `phone`, `phone_number`, `location`) در پاسخ فعلی API موجودند.
+- **بررسی رگرسیون:** اگر صفحهٔ پزشک (`components/doctor/...`) هم آدرس کلینیک را همینطور رندر میکند، فقط گزارش بده — در این تسک تغییرش نده.
diff --git a/app/clinic/[slug]/page.js b/app/clinic/[slug]/page.js
index d0a87e2..a1865a4 100644
--- a/app/clinic/[slug]/page.js
+++ b/app/clinic/[slug]/page.js
@@ -9,6 +9,12 @@ import { extractEntityCityId } from "@/lib/domainHelpers";
import { isThinClinic } from "@/lib/entityQuality";
import { safeJsonLd } from "@/lib/sanitize";
import { imageUrl } from "@/helper";
+import {
+ getClinicPhone,
+ toE164Ir,
+ getClinicCity,
+ getClinicState,
+} from "@/lib/clinicContact";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -114,6 +120,10 @@ async function Clinic({ params, searchParams }) {
? Object.values(clinic.social_media).filter(Boolean)
: [];
+ const clinicPhone = getClinicPhone(clinic);
+ const clinicCity = getClinicCity(clinic);
+ const clinicState = getClinicState(clinic);
+
const jsonLd = clinic
? {
"@context": "https://schema.org",
@@ -127,11 +137,13 @@ async function Clinic({ params, searchParams }) {
name: clinic.title,
},
}),
- ...(clinic.phone && { telephone: clinic.phone }),
- ...(clinic.location && {
+ ...(clinicPhone && { telephone: toE164Ir(clinicPhone) }),
+ ...((clinic.location || clinicCity) && {
address: {
"@type": "PostalAddress",
- streetAddress: clinic.location,
+ ...(clinic.location && { streetAddress: clinic.location.trim() }),
+ ...(clinicCity && { addressLocality: clinicCity }),
+ ...(clinicState && { addressRegion: clinicState }),
addressCountry: "IR",
},
}),
diff --git a/components/clinic/components/contact/Detail.js b/components/clinic/components/contact/Detail.js
index 85ab20b..ac3a17a 100644
--- a/components/clinic/components/contact/Detail.js
+++ b/components/clinic/components/contact/Detail.js
@@ -1,4 +1,5 @@
import ContentDetail from "./ContentDetail";
+import { getClinicPhone, telHref, getClinicAddress } from "@/lib/clinicContact";
function Detail({ data }) {
// API برای فیلدهای پرنشده null میفرستد؛ درج مستقیم در template رشتهٔ «null»
@@ -6,8 +7,9 @@ function Detail({ data }) {
const workingDays = data?.["24_7"]
? "۲۴ ساعته، تمام روزهای هفته"
: data?.field_working_days?.trim();
- const phone = (data?.phone_number || data?.phone || "").trim();
- const address = data?.location?.trim();
+ const phone = getClinicPhone(data);
+ const href = telHref(phone);
+ const address = getClinicAddress(data);
if (!workingDays && !phone && !address) return null;
@@ -20,9 +22,13 @@ function Detail({ data }) {
- {phone}
-
+ href ? (
+
+ {phone}
+
+ ) : (
+ {phone}
+ )
}
/>
)}
diff --git a/components/clinic/components/contact/index.js b/components/clinic/components/contact/index.js
index ed76b1a..8d5e59d 100644
--- a/components/clinic/components/contact/index.js
+++ b/components/clinic/components/contact/index.js
@@ -1,4 +1,5 @@
import { useState } from "react";
+import dynamic from "next/dynamic";
import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
import { Button } from "@mui/material";
@@ -6,20 +7,25 @@ import Detail from "./Detail";
import RoutingC from "@/components/icons/RoutingC";
import ModalOpenLocation from "@/app/component/openLocation";
import CustomLoading from "@/app/component/loading/Custom";
+import { hasClinicContact } from "@/lib/clinicContact";
+
+// همان نقشهٔ صفحهٔ پزشک: Leaflet روی تایلهای OpenStreetMap. embed گوگلمپ
+// در ایران بارگذاری نمیشد و کادر نقشه خالی میماند.
+const MapView = dynamic(() => import("@/components/common/MapView"), {
+ ssr: false,
+});
function Contact({ data }) {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
- // بدون مختصات و بدون نشانی، کوئری نقشه خالی میماند و iframe نقطهٔ بیربط
- // نشان میدهد؛ در آن حالت نقشه و دکمهٔ مسیریابی اصلاً معنا ندارند.
+ // نقشه و مسیریابی هر دو مختصات میخواهند — لینکهای مسیریابی (بلد/گوگل/ویز)
+ // بدون lat/lng به `undefined,undefined` میرسند. با نشانیِ متنی تنها، هیچکدام
+ // معنا ندارد و فقط ردیف آدرس نمایش داده میشود.
const hasCoords = Boolean(data?.map?.latitude && data?.map?.longitude);
- const mapQuery = hasCoords
- ? `${data.map.latitude},${data.map.longitude}`
- : data?.location?.trim()
- ? encodeURIComponent(data.location.trim())
- : "";
+
+ if (!hasClinicContact(data)) return null;
return (
- {mapQuery &&
}
- {mapQuery &&
+ {hasCoords &&
-
+
diff --git a/components/doctor/detailDoctor/cards/locations/Item.js b/components/doctor/detailDoctor/cards/locations/Item.js
index 4b146a1..7c87607 100644
--- a/components/doctor/detailDoctor/cards/locations/Item.js
+++ b/components/doctor/detailDoctor/cards/locations/Item.js
@@ -9,7 +9,7 @@ import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
import RoutingC from "@/components/icons/RoutingC";
// Leaflet به window نیاز دارد → فقط کلاینت
-const MapView = dynamic(() => import("./MapView"), { ssr: false });
+const MapView = dynamic(() => import("@/components/common/MapView"), { ssr: false });
function Item({ data, bookable = true }) {
const [isOpen, setIsOpen] = useState(false);
diff --git a/lib/clinicContact.js b/lib/clinicContact.js
new file mode 100644
index 0000000..e7cde0d
--- /dev/null
+++ b/lib/clinicContact.js
@@ -0,0 +1,51 @@
+// API هر دو کلید phone و phone_number را میفرستد و یکی ممکن است null باشد؛
+// UI و JSON-LD باید از یک منبع بخوانند تا صفحه و structured data یکی بگویند.
+export function getClinicPhone(clinic) {
+ return (clinic?.phone_number || clinic?.phone || "").toString().trim();
+}
+
+// مقدار DB ممکن است فاصله یا خط تیره داشته باشد و لینک tel: را خراب کند.
+export function telHref(phone) {
+ const digits = (phone || "").replace(/[^\d+]/g, "");
+ return digits ? `tel:${digits}` : null;
+}
+
+export function toE164Ir(phone) {
+ const d = (phone || "").replace(/\D/g, "");
+ if (!d) return null;
+ if (d.startsWith("98")) return `+${d}`;
+ if (d.startsWith("0")) return `+98${d.slice(1)}`;
+ return `+98${d}`;
+}
+
+export function getClinicCity(clinic) {
+ return clinic?.city?.[0]?.name?.trim() || null;
+}
+
+export function getClinicState(clinic) {
+ return clinic?.state?.[0]?.name?.trim() || null;
+}
+
+// نشانی آزادِ بعضی رکوردها خودش نام شهر یا استان را دارد؛ در آن حالت تکرار نشود.
+export function getClinicAddress(clinic) {
+ const street = clinic?.location?.trim() || "";
+ const city = getClinicCity(clinic);
+ const state = getClinicState(clinic);
+
+ const parts = [];
+ if (state && !street.includes(state)) parts.push(state);
+ if (city && !street.includes(city)) parts.push(city);
+ if (street) parts.push(street);
+
+ return parts.length ? parts.join("، ") : null;
+}
+
+export function hasClinicContact(clinic) {
+ return Boolean(
+ clinic?.["24_7"] ||
+ clinic?.field_working_days?.trim() ||
+ getClinicPhone(clinic) ||
+ getClinicAddress(clinic) ||
+ (clinic?.map?.latitude && clinic?.map?.longitude)
+ );
+}