# اصلاح بخش «اطلاعات تماس» صفحه کلینیک ## پروژه `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 ( ); } ``` `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 ( ); } ``` ### ۳. جلوگیری از کارت خالی در `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/...`) هم آدرس کلینیک را همین‌طور رندر می‌کند، فقط گزارش بده — در این تسک تغییرش نده.