diff --git a/.claude/prompt/doctor-map-claim-modal.md b/.claude/prompt/doctor-map-claim-modal.md
new file mode 100644
index 0000000..617afac
--- /dev/null
+++ b/.claude/prompt/doctor-map-claim-modal.md
@@ -0,0 +1,118 @@
+# رفع نمایش نقشه صفحه پزشک + بهبود مودال claim (کپچا/موبایل) + حذف پروفایل توسط مالک
+
+## پروژه
+
+`nobat724_front` (سایت عمومی)
+> پرامپت همتا (backend اول): `clinicpro/.claude/prompt/doctor-map-claim-captcha-delete.md` — کپچا و فیلد mobile روی endpoint claim، و اجازهٔ حذف پروفایل به مالک. این پرامپت آن قرارداد را مصرف میکند.
+
+## زمینه
+
+سه موضوع در صفحهٔ پزشک سایت:
+1. نقشه در صفحهٔ پزشک درست نمایش داده نمیشود (مثال: `/doctor/ab747d75-2114-42b8-9e6d-abdaa338edbe`).
+2. مودال «تأیید و مدیریت پروفایل» (claim) از قبل هست ولی طبق سناریو باید متن کادر اطلاعرسانی بهروز شود، **فیلد موبایل** و **کپچای ALTCHA** اضافه شود.
+3. پس از مالکشدن، پزشک باید دکمهٔ **حذف پروفایل** داشته باشد.
+
+## فایلهای مرتبط
+
+| فایل | نقش |
+|------|-----|
+| `app/doctor/[slug]/page.js` | فچ `doctor` + `getDoctorAddresses(doctor.id)` (خط ۲۴، ۱۰۵) |
+| `components/doctor/detailDoctor/cards/locations/index.js` | کارت آدرسها — از `doctor?.address` میخواند (خط ۱۱) |
+| `components/doctor/detailDoctor/cards/locations/Item.js` | رندر iframe گوگل با `data.map.latitude/longitude` (خط ۴۶، ۶۵) |
+| `components/doctor/claim/index.js` | `ClaimProfileSection` موجود (سکشن + مودال) |
+| `components/Altcha.js` | کامپوننت ALTCHA موجود سایت |
+| `services/response.js` | `getDoctorClaimInfo` / `postDoctorClaim` موجود |
+
+## وظیفه ۱ — رفع نمایش نقشه صفحه پزشک
+
+### ریشه (تأییدشده)
+
+کارت آدرسها از **`doctor?.address`** میخواند:
+
+```jsx
+// components/doctor/detailDoctor/cards/locations/index.js:11
+{doctor?.address?.map((item, idx) => ( ))}
+```
+
+اما مختصات نقشه در پاسخِ **جداگانه**ی `GET /api/v1/clinic-pro/doctor-addresses/{id}` است (backend آن را با شکل `map: { latitude, longitude }` برمیگرداند). در صفحه، این پاسخ در متغیر `addresses` فچ میشود و فقط در **JSON-LD** استفاده شده (`app/doctor/[slug]/page.js:105,141`)، ولی به کارت دیداری آدرسها **پاس داده نمیشود**. پس `doctor.address` یا خالی است یا `map` ندارد → `Item` شرط `data?.map?.latitude` را رد میکند → نقشه هرگز نمایش داده نمیشود.
+
+### راهحل
+
+`addresses` (که `map.latitude/longitude` دارد) را به همان کارتی که نقشه را رندر میکند برسان:
+
+- در `app/doctor/[slug]/page.js`، `addresses` را به `DoctorPage`/`detailDoctor` پاس بده (prop جدید یا ادغام در `doctor.address`).
+- در `locations/index.js`، بهجای `doctor?.address` از همان آرایهٔ `addresses` استفاده کن که هر آیتم `map: { latitude, longitude }` و `address`/`telephone` دارد.
+- شکل مصرفی `Item` (`data.map.latitude`, `data.map.longitude`, `data.address`, `data.telephone`) را با شکل خروجی `doctor-addresses` همتراز کن (backend همین کلیدها را میدهد — تأیید در `clinicpro/src/Doctor/Controller/DoctorController.php:166`).
+
+```jsx
+// locations/index.js — نمونه
+export default function Locations({ addresses }) {
+ if (!addresses?.length) return null;
+ return (<>{addresses.map((item, idx) => )}>);
+}
+```
+
+- **edge:** پزشکِ ایمپورتشده (مثل نمونهٔ کاربر) آدرس ندارد → آرایه خالی → کارت اصلاً رندر نشود (نه نقشهٔ خراب). این درست است.
+- **edge:** آدرسِ بدون مختصات (`latitude=null`) → دکمهٔ مسیریابی/iframe نمایش داده نشود، ولی خود آدرس/تلفن نمایش داده شود.
+
+## وظیفه ۲ — بهبود مودال claim (متن، موبایل، کپچا)
+
+`components/doctor/claim/index.js` از قبل سکشن + مودال دارد. تغییرات:
+
+**الف) متن کادر اطلاعرسانی** (طبق سناریو):
+
+> «این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده است و هنوز توسط پزشک تأیید و مدیریت نمیشود. نوبتهای این پروفایل عمومی و غیرخاص هستند.
+> آیا شما این پزشک هستید؟» + دکمهٔ «تأیید و مدیریت این پروفایل».
+
+**ب) فیلد موبایل در فرم:** علاوه بر نام/نامخانوادگی/کد ملی/تاریخ تولد، فیلد **شماره موبایل** اضافه شود (پیشپرشده از کاربر لاگینشده اگر در دسترس است). به بدنهٔ `postDoctorClaim` اضافه شود:
+
+```js
+await request.postDoctorClaim(doctor.uuid, {
+ national_code, birth_date, first_name, last_name, mobile, altcha, // ← mobile و altcha جدید
+});
+```
+
+**ج) کپچای ALTCHA:** از کامپوننت موجود `components/Altcha.js` استفاده کن. تا وقتی کاربر کپچا را حل نکرده، دکمهٔ ارسال **غیرفعال** بماند:
+
+```jsx
+import Altcha from "@/components/Altcha";
+// ...
+const [altcha, setAltcha] = useState("");
+// در فرم:
+
+
+```
+
+- payload کپچا را در بدنهٔ claim بفرست (backend همتا `CaptchaGuard::assertValid` را چک میکند). نام فیلد را با آنچه `CaptchaGuard` انتظار دارد هماهنگ کن (بررسی `AuthController` سایت/بکاند — معمولاً `altcha`).
+- اگر `ALTCHA_ENABLED=false` (dev) بکاند کپچا را نادیده میگیرد؛ ولی UI کپچا را نشان بده تا در prod کار کند.
+
+**stateهای موجود مودال** (loading/error/success/double-submit/پیام خوشآمد) حفظ شوند؛ فقط فیلدها و کپچا اضافه میشوند. خطای `ERR_CAPTCHA_001` از بکاند → پیام «تأیید امنیتی ناموفق بود، دوباره تلاش کنید».
+
+## وظیفه ۳ — حذف پروفایل توسط مالک
+
+پس از claim موفق (پزشک مالک شد)، در صفحهٔ مدیریت پروفایل پزشک (یا همان صفحهٔ پزشک وقتی کاربرِ لاگینشده مالک است) دکمهٔ **«حذف پروفایل»** نمایش داده شود.
+
+- فقط وقتی نمایش داده شود که کاربرِ لاگینشده مالکِ `claimed` این پروفایل باشد (از `owner_status` + تطبیق کاربر). مرجع نهایی مجوز، backend است.
+- کلیک → **دیالوگ تأیید** با پیام هشدار (طبق سناریو: «قبل از حذف، پیام هشدار نمایش داده شود»)، سپس:
+
+```js
+// متد جدید در services/response.js
+deleteDoctor: (uuid) => api.delete(`api/v1/doctor/${uuid}`, { requireAuth: true }),
+```
+
+- backend اجازهٔ حذف مالک را میدهد (پرامپت همتا). خطاها: ۴۰۳ (مالک نیست)، ۴۰۹ (پزشک نوبت ثبتشده دارد) → پیام فارسی مناسب.
+- پس از حذف موفق → هدایت به صفحهٔ اصلی/پنل + toast موفقیت.
+
+## نکات مهم
+
+- **backend اول اجرا شود** (کپچا + فیلد mobile + delete مالک) وگرنه این تغییرات ۴۲۲/۴۰۳ میگیرند.
+- multi-domain: مودال claim کامپوننت مشترک است و روی همهٔ دامنهها/زیردامنهها کار میکند؛ منطق را per-domain تکرار نکن.
+- RTL، فارسی، Vazir، date-picker شمسی موجود؛ کتابخانهٔ جدید اضافه نکن.
+- هیچ درخواستی از فرانت به API سازمان (شاهکار/ثبتاحوال) نرود؛ همه backend.
+- تست:
+ ```bash
+ cd nobat724_front && npm run lint && npm run build
+ # صفحهٔ پزشکِ دارای آدرس با مختصات → نقشه نمایش داده شود؛
+ # پزشک ایمپورتشدهٔ بدون آدرس → کارت نقشه رندر نشود (نه خراب)؛
+ # مودال claim → کپچا اجباری، فیلد موبایل، ارسال موفق؛ دکمهٔ حذف فقط برای مالک.
+ ```
diff --git a/app/about-us/page.js b/app/about-us/page.js
index 9f96861..1cda382 100644
--- a/app/about-us/page.js
+++ b/app/about-us/page.js
@@ -7,7 +7,7 @@ export async function generateMetadata() {
const siteName = matchedCity?.site_name || "نوبت 724";
const title = `درباره ما | ${siteName}`;
const description = `آشنایی با ${siteName}، سیستم آنلاین نوبتدهی پزشکی. هدف ما ارائه خدمات سریع و کارآمد رزرو نوبت پزشکی برای همه مردم است.`;
- const image = "https://nobat724.com/assets/images/logo.png";
+ const image = "/assets/images/og-image.png";
return {
title,
description,
diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js
index d628544..3ea0c06 100644
--- a/app/blog/[slug]/page.js
+++ b/app/blog/[slug]/page.js
@@ -8,7 +8,7 @@ import { getRequestOrigin } from "@/lib/getCanonicalUrl";
import { safeJsonLd } from "@/lib/sanitize";
import { normalizeBlog, imageUrl } from "@/helper";
-const FALLBACK_IMG = "https://nobat724.com/assets/images/logo.png";
+const FALLBACK_IMG = "/assets/images/og-image.png";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const getBlog = cache(async (slug) => {
diff --git a/app/blogs/page.js b/app/blogs/page.js
index 1ae5499..2451a1c 100644
--- a/app/blogs/page.js
+++ b/app/blogs/page.js
@@ -7,7 +7,7 @@ export async function generateMetadata() {
const siteName = matchedCity?.site_name || "نوبت 724";
const title = `مقالات و اخبار پزشکی | ${siteName}`;
const description = `جدیدترین مقالات، اخبار و راهنماهای پزشکی. اطلاعات تخصصی در حوزه سلامت و پزشکی از متخصصان ${siteName}.`;
- const image = "https://nobat724.com/assets/images/logo.png";
+ const image = "/assets/images/og-image.png";
return {
title,
description,
diff --git a/app/clinic/[slug]/page.js b/app/clinic/[slug]/page.js
index 4c3edc2..27d2863 100644
--- a/app/clinic/[slug]/page.js
+++ b/app/clinic/[slug]/page.js
@@ -36,7 +36,7 @@ export async function generateMetadata({ params }) {
const image = clinic.images_clinic?.[0]?.url
? [imageUrl(clinic.images_clinic[0].url)]
- : ["https://nobat724.com/assets/images/logo.png"];
+ : ["/assets/images/og-image.png"];
return {
title,
diff --git a/app/clinics/page.js b/app/clinics/page.js
index 635f29c..85fc67f 100644
--- a/app/clinics/page.js
+++ b/app/clinics/page.js
@@ -27,7 +27,7 @@ export async function generateMetadata({ searchParams }) {
const description = cityName
? `لیست کلینیکها و مراکز درمانی در ${cityName}. رزرو آنلاین نوبت از بهترین مراکز درمانی ${cityName}.`
: `جستجوی کلینیکها و مراکز درمانی در سراسر کشور. رزرو آنلاین نوبت سریع و آسان.`;
- const image = "https://nobat724.com/assets/images/logo.png";
+ const image = "/assets/images/og-image.png";
return {
title,
description,
diff --git a/app/doctor/[slug]/page.js b/app/doctor/[slug]/page.js
index 02170dd..9044c6b 100644
--- a/app/doctor/[slug]/page.js
+++ b/app/doctor/[slug]/page.js
@@ -8,7 +8,7 @@ import { safeJsonLd } from "@/lib/sanitize";
import { imageUrl } from "@/helper";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
-const FALLBACK_IMG = "https://nobat724.com/assets/images/logo.png";
+const FALLBACK_IMG = "/assets/images/og-image.png";
const getDoctor = cache(async (slug) => {
if (!slug || slug === "undefined") return null;
@@ -29,7 +29,8 @@ const getDoctorAddresses = cache(async (doctorId) => {
});
if (!res.ok) return [];
const json = await res.json();
- return json?.data ?? [];
+ // پاسخ double-nested است: { success, data: { data: [...] } }
+ return json?.data?.data ?? json?.data ?? [];
} catch {
return [];
}
@@ -196,6 +197,7 @@ async function Doctor({ params }) {
doctor={doctor}
comments={comments}
rateAggregate={rateAggregate}
+ addresses={addresses}
slug={slug}
/>
diff --git a/app/doctors/page.js b/app/doctors/page.js
index 16adad0..952f9d3 100644
--- a/app/doctors/page.js
+++ b/app/doctors/page.js
@@ -27,7 +27,7 @@ export async function generateMetadata({ searchParams }) {
const description = cityName
? `لیست پزشکان متخصص در ${cityName}. جستجو بر اساس تخصص و منطقه. رزرو آنلاین نوبت پزشکی در ${cityName}.`
: `جستجوی پزشکان متخصص در سراسر کشور. رزرو آنلاین نوبت پزشکی سریع و آسان با نوبت 724.`;
- const image = "https://nobat724.com/assets/images/logo.png";
+ const image = "/assets/images/og-image.png";
return {
title,
description,
diff --git a/app/layout.js b/app/layout.js
index dda5d07..6cc382b 100644
--- a/app/layout.js
+++ b/app/layout.js
@@ -45,13 +45,13 @@ export async function generateMetadata() {
type: "website",
locale: "fa_IR",
siteName: matchedCity?.site_name || repContext?.full_name || "نوبت 724",
- images: ["https://nobat724.com/assets/images/logo.png"],
+ images: ["/assets/images/og-image.png"],
},
twitter: {
card: "summary_large_image",
title,
description,
- images: ["https://nobat724.com/assets/images/logo.png"],
+ images: ["/assets/images/og-image.png"],
},
};
diff --git a/app/specialties/page.js b/app/specialties/page.js
index bc65f85..3993734 100644
--- a/app/specialties/page.js
+++ b/app/specialties/page.js
@@ -10,7 +10,7 @@ export async function generateMetadata() {
? `تخصصهای پزشکی در ${cityName} | ${siteName}`
: `تخصصهای پزشکی | ${siteName}`;
const description = `لیست کامل تخصصهای پزشکی${cityName ? " در " + cityName : ""}. رزرو نوبت از متخصصین مختلف به صورت آنلاین.`;
- const image = "https://nobat724.com/assets/images/logo.png";
+ const image = "/assets/images/og-image.png";
return {
title,
description,
diff --git a/components/doctor/claim/index.js b/components/doctor/claim/index.js
new file mode 100644
index 0000000..a098d9b
--- /dev/null
+++ b/components/doctor/claim/index.js
@@ -0,0 +1,263 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter, usePathname } from "next/navigation";
+import Link from "next/link";
+import { Box, Button, Modal, TextField } from "@mui/material";
+import { styleDefault } from "@/mui";
+import JalaliDatePicker from "@/components/common/JalaliDatePicker";
+import CloseModalD from "@/components/icons/CloseModalD";
+import Cookies from "js-cookie";
+import { convertToJalali, isUserLoggedIn } from "@/helper";
+import { request } from "@/services/response";
+import Altcha from "@/components/Altcha";
+
+const initialForm = { national_code: "", birth_date: "", first_name: "", last_name: "" };
+
+function currentUserMobile() {
+ try {
+ return JSON.parse(Cookies.get("userInfo") || "{}")?.mobile_number || "";
+ } catch {
+ return "";
+ }
+}
+
+/**
+ * احراز مالکیت پروفایل پزشک ایمپورتشده (unclaimed).
+ * کامپوننت مشترک برای دامنهٔ اصلی و همهٔ زیردامنهها/دامنههای نمایندگان —
+ * منطق claim به دامنه وابسته نیست؛ احراز هویت کاملاً سمت سرور انجام میشود.
+ */
+function ClaimProfileSection({ doctor }) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const [open, setOpen] = useState(false);
+ const [form, setForm] = useState(initialForm);
+ const [fieldErrors, setFieldErrors] = useState({});
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [claimed, setClaimed] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const [altcha, setAltcha] = useState(null); // null=در حال آمادهسازی، ""=خاموش، payload=حلشده
+ const [confirmDelete, setConfirmDelete] = useState(false);
+ const [deleting, setDeleting] = useState(false);
+
+ if (doctor?.owner_status !== "unclaimed" || claimed) {
+ return null;
+ }
+
+ const loggedIn = isUserLoggedIn();
+ const userMobile = currentUserMobile();
+
+ const setField = (key, value) => {
+ setForm((f) => ({ ...f, [key]: value }));
+ setFieldErrors((e) => ({ ...e, [key]: undefined }));
+ };
+
+ const validate = () => {
+ const errs = {};
+ const nc = form.national_code.replace(/[۰-۹]/g, (d) => "۰۱۲۳۴۵۶۷۸۹".indexOf(d));
+ if (!/^\d{10}$/.test(nc)) errs.national_code = "کد ملی باید ۱۰ رقم باشد";
+ if (!form.birth_date) errs.birth_date = "تاریخ تولد را انتخاب کنید";
+ if (!form.first_name.trim()) errs.first_name = "نام الزامی است";
+ if (!form.last_name.trim()) errs.last_name = "نام خانوادگی الزامی است";
+ setFieldErrors(errs);
+ return Object.keys(errs).length === 0;
+ };
+
+ const submit = async () => {
+ if (loading || !validate()) return;
+ setLoading(true);
+ setError("");
+ try {
+ await request.postDoctorClaim(doctor.uuid, { ...form, mobile: userMobile, altcha });
+ setLoading(false);
+ setSuccess(true);
+ } catch (err) {
+ setLoading(false);
+ const first = err?.response?.data?.errors?.[0];
+ setError(first?.message || "خطایی رخ داد. لطفاً دوباره تلاش کنید.");
+ }
+ };
+
+ const closeAfterSuccess = () => {
+ setOpen(false);
+ setClaimed(true);
+ router.push("/dashboard");
+ };
+
+ const removeProfile = async () => {
+ if (deleting) return;
+ setDeleting(true);
+ setError("");
+ try {
+ await request.deleteDoctor(doctor.uuid);
+ setDeleting(false);
+ setOpen(false);
+ setClaimed(true);
+ router.push("/");
+ } catch (err) {
+ setDeleting(false);
+ const first = err?.response?.data?.errors?.[0];
+ setError(first?.message || "حذف پروفایل ممکن نشد.");
+ }
+ };
+
+ return (
+ <>
+
+
+ این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده است و
+ هنوز توسط پزشک تأیید و مدیریت نمیشود. نوبتهای این پروفایل عمومی و
+ غیرخاص هستند.
+
+
+
آیا شما این پزشک هستید؟
+
+
+
+
+ !loading && setOpen(false)}>
+
+
+
+
احراز مالکیت پروفایل
+
!loading && setOpen(false)}>
+
+
+
+
+ {success ? (
+
+
+ دکتر {form.first_name} {form.last_name}، به نوبت ۷۲۴ خوش آمدید 🎉
+
+
+ پروفایل شما با موفقیت تأیید شد و اکنون میتوانید اطلاعات پروفایل و
+ تنظیمات نوبتدهی خود را مدیریت کنید.
+
+
+ {error && (
+
{error}
+ )}
+ {confirmDelete ? (
+
+
+ آیا از حذف کامل این پروفایل مطمئن هستید؟ این عمل قابل بازگشت نیست.
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+ ) : !loggedIn ? (
+
+
+ برای احراز مالکیت، ابتدا با شماره موبایل خود وارد شوید. پس از ورود،
+ به همین صفحه بازگردید و دوباره تلاش کنید.
+
+
+
+
+
+ ) : (
+
+
+ اطلاعات هویتی شما بهصورت امن با سامانهٔ ثبت احوال و اطلاعات سازمان
+ نظام پزشکی تطبیق داده میشود.
+