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/doctor/[slug]/page.js b/app/doctor/[slug]/page.js index 02170dd..746faf8 100644 --- a/app/doctor/[slug]/page.js +++ b/app/doctor/[slug]/page.js @@ -196,6 +196,7 @@ async function Doctor({ params }) { doctor={doctor} comments={comments} rateAggregate={rateAggregate} + addresses={addresses} slug={slug} /> diff --git a/components/doctor/claim/index.js b/components/doctor/claim/index.js index 951cfb7..b66ec22 100644 --- a/components/doctor/claim/index.js +++ b/components/doctor/claim/index.js @@ -9,8 +9,9 @@ import JalaliDatePicker from "@/components/common/JalaliDatePicker"; import CloseModalD from "@/components/icons/CloseModalD"; 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: "" }; +const initialForm = { national_code: "", birth_date: "", first_name: "", last_name: "", mobile: "" }; /** * احراز مالکیت پروفایل پزشک ایمپورت‌شده (unclaimed). @@ -26,6 +27,9 @@ function ClaimProfileSection({ doctor }) { 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; @@ -45,6 +49,8 @@ function ClaimProfileSection({ doctor }) { if (!form.birth_date) errs.birth_date = "تاریخ تولد را انتخاب کنید"; if (!form.first_name.trim()) errs.first_name = "نام الزامی است"; if (!form.last_name.trim()) errs.last_name = "نام خانوادگی الزامی است"; + const mob = form.mobile.replace(/[۰-۹]/g, (d) => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)); + if (!/^09\d{9}$/.test(mob)) errs.mobile = "شماره موبایل نامعتبر است"; setFieldErrors(errs); return Object.keys(errs).length === 0; }; @@ -54,7 +60,7 @@ function ClaimProfileSection({ doctor }) { setLoading(true); setError(""); try { - await request.postDoctorClaim(doctor.uuid, form); + await request.postDoctorClaim(doctor.uuid, { ...form, altcha }); setLoading(false); setSuccess(true); } catch (err) { @@ -70,12 +76,30 @@ function ClaimProfileSection({ doctor }) { 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 ( <>

- این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده و هنوز - توسط پزشک تأیید و مدیریت نمی‌شود؛ نوبت‌دهی آنلاین آن غیرفعال است. + این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده است و + هنوز توسط پزشک تأیید و مدیریت نمی‌شود. نوبت‌های این پروفایل عمومی و + غیرخاص هستند.

آیا شما این پزشک هستید؟

@@ -111,6 +135,43 @@ function ClaimProfileSection({ doctor }) { + {error && ( +

{error}

+ )} + {confirmDelete ? ( +
+

+ آیا از حذف کامل این پروفایل مطمئن هستید؟ این عمل قابل بازگشت نیست. +

+
+ + +
+
+ ) : ( + + )}
) : !loggedIn ? (
@@ -155,6 +216,15 @@ function ClaimProfileSection({ doctor }) { inputProps={{ maxLength: 10, dir: "ltr" }} fullWidth /> + setField("mobile", e.target.value)} + inputProps={{ maxLength: 11, dir: "ltr" }} + fullWidth + /> setField("birth_date", convertToJalali(e._d))} /> + {error && (

{error}

)}