Files
nobat724_front/.claude/prompt/clinic-contact-info-fix.md

269 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# اصلاح بخش «اطلاعات تماس» صفحه کلینیک
## پروژه
`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` خالی است — ولی کارت `<div className="border ... rounded-[16px] ...">` همچنان رندر می‌شود و یک باکس خالی روی صفحه می‌ماند.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `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 (
<ul className="flex flex-col items-start justify-start gap-[16px]">
{workingDays && (
<ContentDetail head="روزهای کاری: " detail={workingDays} />
)}
{phone && (
<ContentDetail
head="تلفن: "
detail={
<a href={`tel:${phone}`} dir="ltr" className="hover:text-[#5559CE]">
{phone}
</a>
}
/>
)}
{address && <ContentDetail head="آدرس: " detail={address} />}
</ul>
);
}
```
`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 (
<ul className="flex flex-col items-start justify-start gap-[16px]">
{workingDays && <ContentDetail head="روزهای کاری: " detail={workingDays} />}
{phone && (
<ContentDetail
head="تلفن: "
detail={
href ? (
<a href={href} dir="ltr" className="hover:text-[#5559CE]">
{phone}
</a>
) : (
<span dir="ltr">{phone}</span>
)
}
/>
)}
{address && <ContentDetail head="آدرس: " detail={address} />}
</ul>
);
}
```
### ۳. جلوگیری از کارت خالی در `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/...`) هم آدرس کلینیک را همین‌طور رندر می‌کند، فقط گزارش بده — در این تسک تغییرش نده.