Merge branch 'feature/doctor-claim'
This commit is contained in:
@@ -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) => ( <Item data={item} /> ))}
|
||||||
|
```
|
||||||
|
|
||||||
|
اما مختصات نقشه در پاسخِ **جداگانه**ی `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) => <Item key={idx} data={item} />)}</>);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **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("");
|
||||||
|
// در فرم:
|
||||||
|
<Altcha onVerified={setAltcha} />
|
||||||
|
<Button disabled={loading || !altcha} onClick={submit}>تأیید هویت و تصاحب پروفایل</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 → کپچا اجباری، فیلد موبایل، ارسال موفق؛ دکمهٔ حذف فقط برای مالک.
|
||||||
|
```
|
||||||
@@ -7,7 +7,7 @@ export async function generateMetadata() {
|
|||||||
const siteName = matchedCity?.site_name || "نوبت 724";
|
const siteName = matchedCity?.site_name || "نوبت 724";
|
||||||
const title = `درباره ما | ${siteName}`;
|
const title = `درباره ما | ${siteName}`;
|
||||||
const description = `آشنایی با ${siteName}، سیستم آنلاین نوبتدهی پزشکی. هدف ما ارائه خدمات سریع و کارآمد رزرو نوبت پزشکی برای همه مردم است.`;
|
const description = `آشنایی با ${siteName}، سیستم آنلاین نوبتدهی پزشکی. هدف ما ارائه خدمات سریع و کارآمد رزرو نوبت پزشکی برای همه مردم است.`;
|
||||||
const image = "https://nobat724.com/assets/images/logo.png";
|
const image = "/assets/images/og-image.png";
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getRequestOrigin } from "@/lib/getCanonicalUrl";
|
|||||||
import { safeJsonLd } from "@/lib/sanitize";
|
import { safeJsonLd } from "@/lib/sanitize";
|
||||||
import { normalizeBlog, imageUrl } from "@/helper";
|
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 API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
const getBlog = cache(async (slug) => {
|
const getBlog = cache(async (slug) => {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ export async function generateMetadata() {
|
|||||||
const siteName = matchedCity?.site_name || "نوبت 724";
|
const siteName = matchedCity?.site_name || "نوبت 724";
|
||||||
const title = `مقالات و اخبار پزشکی | ${siteName}`;
|
const title = `مقالات و اخبار پزشکی | ${siteName}`;
|
||||||
const description = `جدیدترین مقالات، اخبار و راهنماهای پزشکی. اطلاعات تخصصی در حوزه سلامت و پزشکی از متخصصان ${siteName}.`;
|
const description = `جدیدترین مقالات، اخبار و راهنماهای پزشکی. اطلاعات تخصصی در حوزه سلامت و پزشکی از متخصصان ${siteName}.`;
|
||||||
const image = "https://nobat724.com/assets/images/logo.png";
|
const image = "/assets/images/og-image.png";
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export async function generateMetadata({ params }) {
|
|||||||
|
|
||||||
const image = clinic.images_clinic?.[0]?.url
|
const image = clinic.images_clinic?.[0]?.url
|
||||||
? [imageUrl(clinic.images_clinic[0].url)]
|
? [imageUrl(clinic.images_clinic[0].url)]
|
||||||
: ["https://nobat724.com/assets/images/logo.png"];
|
: ["/assets/images/og-image.png"];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export async function generateMetadata({ searchParams }) {
|
|||||||
const description = cityName
|
const description = cityName
|
||||||
? `لیست کلینیکها و مراکز درمانی در ${cityName}. رزرو آنلاین نوبت از بهترین مراکز درمانی ${cityName}.`
|
? `لیست کلینیکها و مراکز درمانی در ${cityName}. رزرو آنلاین نوبت از بهترین مراکز درمانی ${cityName}.`
|
||||||
: `جستجوی کلینیکها و مراکز درمانی در سراسر کشور. رزرو آنلاین نوبت سریع و آسان.`;
|
: `جستجوی کلینیکها و مراکز درمانی در سراسر کشور. رزرو آنلاین نوبت سریع و آسان.`;
|
||||||
const image = "https://nobat724.com/assets/images/logo.png";
|
const image = "/assets/images/og-image.png";
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { safeJsonLd } from "@/lib/sanitize";
|
|||||||
import { imageUrl } from "@/helper";
|
import { imageUrl } from "@/helper";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
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) => {
|
const getDoctor = cache(async (slug) => {
|
||||||
if (!slug || slug === "undefined") return null;
|
if (!slug || slug === "undefined") return null;
|
||||||
@@ -29,7 +29,8 @@ const getDoctorAddresses = cache(async (doctorId) => {
|
|||||||
});
|
});
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
return json?.data ?? [];
|
// پاسخ double-nested است: { success, data: { data: [...] } }
|
||||||
|
return json?.data?.data ?? json?.data ?? [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -196,6 +197,7 @@ async function Doctor({ params }) {
|
|||||||
doctor={doctor}
|
doctor={doctor}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
rateAggregate={rateAggregate}
|
rateAggregate={rateAggregate}
|
||||||
|
addresses={addresses}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export async function generateMetadata({ searchParams }) {
|
|||||||
const description = cityName
|
const description = cityName
|
||||||
? `لیست پزشکان متخصص در ${cityName}. جستجو بر اساس تخصص و منطقه. رزرو آنلاین نوبت پزشکی در ${cityName}.`
|
? `لیست پزشکان متخصص در ${cityName}. جستجو بر اساس تخصص و منطقه. رزرو آنلاین نوبت پزشکی در ${cityName}.`
|
||||||
: `جستجوی پزشکان متخصص در سراسر کشور. رزرو آنلاین نوبت پزشکی سریع و آسان با نوبت 724.`;
|
: `جستجوی پزشکان متخصص در سراسر کشور. رزرو آنلاین نوبت پزشکی سریع و آسان با نوبت 724.`;
|
||||||
const image = "https://nobat724.com/assets/images/logo.png";
|
const image = "/assets/images/og-image.png";
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|||||||
+2
-2
@@ -45,13 +45,13 @@ export async function generateMetadata() {
|
|||||||
type: "website",
|
type: "website",
|
||||||
locale: "fa_IR",
|
locale: "fa_IR",
|
||||||
siteName: matchedCity?.site_name || repContext?.full_name || "نوبت 724",
|
siteName: matchedCity?.site_name || repContext?.full_name || "نوبت 724",
|
||||||
images: ["https://nobat724.com/assets/images/logo.png"],
|
images: ["/assets/images/og-image.png"],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images: ["https://nobat724.com/assets/images/logo.png"],
|
images: ["/assets/images/og-image.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function generateMetadata() {
|
|||||||
? `تخصصهای پزشکی در ${cityName} | ${siteName}`
|
? `تخصصهای پزشکی در ${cityName} | ${siteName}`
|
||||||
: `تخصصهای پزشکی | ${siteName}`;
|
: `تخصصهای پزشکی | ${siteName}`;
|
||||||
const description = `لیست کامل تخصصهای پزشکی${cityName ? " در " + cityName : ""}. رزرو نوبت از متخصصین مختلف به صورت آنلاین.`;
|
const description = `لیست کامل تخصصهای پزشکی${cityName ? " در " + cityName : ""}. رزرو نوبت از متخصصین مختلف به صورت آنلاین.`;
|
||||||
const image = "https://nobat724.com/assets/images/logo.png";
|
const image = "/assets/images/og-image.png";
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="w-full mt-[16px] p-[20px] border border-solid border-[#E9ECEF] rounded-[16px] bg-[#FFF8ED]">
|
||||||
|
<p className="text-[#3B3B3B] text-[14px] leading-7">
|
||||||
|
این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده است و
|
||||||
|
هنوز توسط پزشک تأیید و مدیریت نمیشود. نوبتهای این پروفایل عمومی و
|
||||||
|
غیرخاص هستند.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between flex-wrap gap-3 mt-[12px]">
|
||||||
|
<p className="text-[#3B3B3B] text-[16px] font-bold">آیا شما این پزشک هستید؟</p>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="!rounded-[12px] !px-[20px]"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
تأیید و مدیریت این پروفایل
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={() => !loading && setOpen(false)}>
|
||||||
|
<Box sx={styleDefault} className="!py-[24px] !px-[28px] !max-w-[440px] !w-[92vw]">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span></span>
|
||||||
|
<p className="text-[#3B3B3B] text-[18px] font-medium">احراز مالکیت پروفایل</p>
|
||||||
|
<div className="cursor-pointer" onClick={() => !loading && setOpen(false)}>
|
||||||
|
<CloseModalD />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{success ? (
|
||||||
|
<div className="flex flex-col items-center gap-[16px] mt-[24px] text-center">
|
||||||
|
<p className="text-[#15a35a] text-[18px] font-bold">
|
||||||
|
دکتر {form.first_name} {form.last_name}، به نوبت ۷۲۴ خوش آمدید 🎉
|
||||||
|
</p>
|
||||||
|
<p className="text-[#3B3B3B] text-[14px] leading-7">
|
||||||
|
پروفایل شما با موفقیت تأیید شد و اکنون میتوانید اطلاعات پروفایل و
|
||||||
|
تنظیمات نوبتدهی خود را مدیریت کنید.
|
||||||
|
</p>
|
||||||
|
<Button variant="contained" className="!rounded-[12px] w-full" onClick={closeAfterSuccess}>
|
||||||
|
ورود به پنل مدیریت
|
||||||
|
</Button>
|
||||||
|
{error && (
|
||||||
|
<p className="text-[#e0394a] text-[13px] leading-6">{error}</p>
|
||||||
|
)}
|
||||||
|
{confirmDelete ? (
|
||||||
|
<div className="w-full flex flex-col gap-[8px]">
|
||||||
|
<p className="text-[#e0394a] text-[13px] leading-6">
|
||||||
|
آیا از حذف کامل این پروفایل مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-[8px]">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
className="!rounded-[12px] flex-1"
|
||||||
|
disabled={deleting}
|
||||||
|
onClick={removeProfile}
|
||||||
|
>
|
||||||
|
{deleting ? "در حال حذف…" : "بله، حذف کن"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
className="!rounded-[12px] flex-1"
|
||||||
|
disabled={deleting}
|
||||||
|
onClick={() => setConfirmDelete(false)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-[#e0394a] text-[13px] hover:underline"
|
||||||
|
onClick={() => setConfirmDelete(true)}
|
||||||
|
>
|
||||||
|
حذف این پروفایل
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : !loggedIn ? (
|
||||||
|
<div className="flex flex-col items-center gap-[16px] mt-[24px] text-center">
|
||||||
|
<p className="text-[#3B3B3B] text-[14px] leading-7">
|
||||||
|
برای احراز مالکیت، ابتدا با شماره موبایل خود وارد شوید. پس از ورود،
|
||||||
|
به همین صفحه بازگردید و دوباره تلاش کنید.
|
||||||
|
</p>
|
||||||
|
<Link href={`/login?redirect=${encodeURIComponent(pathname)}`} className="w-full">
|
||||||
|
<Button variant="contained" className="!rounded-[12px] w-full">
|
||||||
|
ورود / ثبتنام با موبایل
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-[14px] mt-[24px]">
|
||||||
|
<p className="text-[#6B7280] text-[13px] leading-6">
|
||||||
|
اطلاعات هویتی شما بهصورت امن با سامانهٔ ثبت احوال و اطلاعات سازمان
|
||||||
|
نظام پزشکی تطبیق داده میشود.
|
||||||
|
</p>
|
||||||
|
<TextField
|
||||||
|
label="نام"
|
||||||
|
value={form.first_name}
|
||||||
|
error={!!fieldErrors.first_name}
|
||||||
|
helperText={fieldErrors.first_name}
|
||||||
|
onChange={(e) => setField("first_name", e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="نام خانوادگی"
|
||||||
|
value={form.last_name}
|
||||||
|
error={!!fieldErrors.last_name}
|
||||||
|
helperText={fieldErrors.last_name}
|
||||||
|
onChange={(e) => setField("last_name", e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="کد ملی"
|
||||||
|
value={form.national_code}
|
||||||
|
error={!!fieldErrors.national_code}
|
||||||
|
helperText={fieldErrors.national_code}
|
||||||
|
onChange={(e) => setField("national_code", e.target.value)}
|
||||||
|
inputProps={{ maxLength: 10, dir: "ltr" }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="شماره موبایل"
|
||||||
|
value={userMobile}
|
||||||
|
helperText="شمارهٔ حساب کاربری شما (غیرقابل ویرایش)"
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
|
inputProps={{ dir: "ltr" }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<JalaliDatePicker
|
||||||
|
value={form.birth_date}
|
||||||
|
placeholder="تاریخ تولد (شمسی)"
|
||||||
|
error={fieldErrors.birth_date}
|
||||||
|
stepwise
|
||||||
|
onChange={(e) => setField("birth_date", convertToJalali(e._d))}
|
||||||
|
/>
|
||||||
|
<Altcha onVerified={setAltcha} />
|
||||||
|
{error && (
|
||||||
|
<p className="text-[#e0394a] text-[13px] leading-6 text-center">{error}</p>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="!rounded-[12px]"
|
||||||
|
disabled={loading || altcha === null}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
{loading ? "در حال بررسی هویت…" : "تأیید هویت و تصاحب پروفایل"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClaimProfileSection;
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import ArrowBottomGrayD from "@/components/icons/ArrowBottomGrayD";
|
import ArrowBottomGrayD from "@/components/icons/ArrowBottomGrayD";
|
||||||
import ModalOpenLocation from "@/app/component/openLocation";
|
import ModalOpenLocation from "@/app/component/openLocation";
|
||||||
import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
|
import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
|
||||||
import RoutingC from "@/components/icons/RoutingC";
|
import RoutingC from "@/components/icons/RoutingC";
|
||||||
|
|
||||||
|
// Leaflet به window نیاز دارد → فقط کلاینت
|
||||||
|
const MapView = dynamic(() => import("./MapView"), { ssr: false });
|
||||||
|
|
||||||
function Item({ data }) {
|
function Item({ data }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -64,13 +68,10 @@ function Item({ data }) {
|
|||||||
</div>
|
</div>
|
||||||
{isOpen && data?.map?.latitude && data?.map?.longitude && (
|
{isOpen && data?.map?.latitude && data?.map?.longitude && (
|
||||||
<div className="w-full relative h-[219px] sm:h-[302px] md:h-[385px] lg:h-[470px] mt-[24px]">
|
<div className="w-full relative h-[219px] sm:h-[302px] md:h-[385px] lg:h-[470px] mt-[24px]">
|
||||||
<iframe
|
<MapView
|
||||||
src={`https://maps.google.com/maps?q=${data.map.latitude},${data.map.longitude}&z=14&output=embed`}
|
latitude={data.map.latitude}
|
||||||
className="w-full rounded-lg mt-4 h-full"
|
longitude={data.map.longitude}
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
/>
|
||||||
allowFullScreen=""
|
|
||||||
loading="lazy"
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
<ModalOpenLocation
|
<ModalOpenLocation
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { MapContainer, TileLayer, Marker } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||||||
|
import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
|
||||||
|
import markerShadow from "leaflet/dist/images/marker-shadow.png";
|
||||||
|
|
||||||
|
// آیکون پیشفرض Leaflet را از پکیج bundle میکنیم (بدون CDN؛ سازگار با CSP).
|
||||||
|
const icon = L.icon({
|
||||||
|
iconUrl: markerIcon.src ?? markerIcon,
|
||||||
|
iconRetinaUrl: markerIcon2x.src ?? markerIcon2x,
|
||||||
|
shadowUrl: markerShadow.src ?? markerShadow,
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
shadowSize: [41, 41],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function MapView({ latitude, longitude }) {
|
||||||
|
const lat = parseFloat(latitude);
|
||||||
|
const lng = parseFloat(longitude);
|
||||||
|
if (Number.isNaN(lat) || Number.isNaN(lng)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapContainer
|
||||||
|
center={[lat, lng]}
|
||||||
|
zoom={15}
|
||||||
|
scrollWheelZoom={false}
|
||||||
|
style={{ height: "100%", width: "100%", borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
attribution='© <a href="https://openstreetmap.org">OpenStreetMap</a>'
|
||||||
|
/>
|
||||||
|
<Marker position={[lat, lng]} icon={icon} />
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import CustomLoading from "@/app/component/loading/Custom";
|
import CustomLoading from "@/app/component/loading/Custom";
|
||||||
import Item from "./Item";
|
import Item from "./Item";
|
||||||
|
|
||||||
function Locations({ doctor }) {
|
function Locations({ addresses }) {
|
||||||
|
if (!addresses?.length) return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[#525252] text-[16px] md:text-[18px] lg:text-[20px] font-bold">
|
<p className="text-[#525252] text-[16px] md:text-[18px] lg:text-[20px] font-bold">
|
||||||
موقعیت مکانی{" "}
|
موقعیت مکانی{" "}
|
||||||
</p>
|
</p>
|
||||||
<ul className="flex w-full mt-[24px] flex-col justify-center items-start">
|
<ul className="flex w-full mt-[24px] flex-col justify-center items-start">
|
||||||
{doctor?.address?.map((item, idx) => (
|
{addresses.map((item, idx) => (
|
||||||
<CustomLoading key={idx} width="full" height={40}>
|
<CustomLoading key={idx} width="full" height={40}>
|
||||||
<li className="w-full">
|
<li className="w-full">
|
||||||
<Item data={item} />
|
<Item data={item} />
|
||||||
{doctor?.address.length > idx + 1 && (
|
{addresses.length > idx + 1 && (
|
||||||
<span className="w-full h-px bg-[#EFEFEF] block my-[12px] md:my-[14px] lg:my-[16px]"></span>
|
<span className="w-full h-px bg-[#EFEFEF] block my-[12px] md:my-[14px] lg:my-[16px]"></span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import AboutDcotor from "./cards/AboutDcotor";
|
|||||||
import Comments from "./cards/comments";
|
import Comments from "./cards/comments";
|
||||||
import Locations from "./cards/locations";
|
import Locations from "./cards/locations";
|
||||||
|
|
||||||
function DetailDoctor({ doctor, comments, rateAggregate }) {
|
function DetailDoctor({ doctor, comments, rateAggregate, addresses }) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full lg:w-[59%]">
|
<div className="w-full lg:w-[59%]">
|
||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
@@ -14,7 +14,7 @@ function DetailDoctor({ doctor, comments, rateAggregate }) {
|
|||||||
<Title doctor={doctor} />
|
<Title doctor={doctor} />
|
||||||
<Link />
|
<Link />
|
||||||
<AboutDcotor doctor={doctor} />
|
<AboutDcotor doctor={doctor} />
|
||||||
<Locations doctor={doctor} />
|
<Locations addresses={addresses} />
|
||||||
<Comments
|
<Comments
|
||||||
doctor={doctor}
|
doctor={doctor}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import AppointmentList from "./appointmentList";
|
import AppointmentList from "./appointmentList";
|
||||||
|
import ClaimProfileSection from "./claim";
|
||||||
import DetailDoctor from "./detailDoctor";
|
import DetailDoctor from "./detailDoctor";
|
||||||
import Share from "./detailDoctor/Share";
|
import Share from "./detailDoctor/Share";
|
||||||
import CustomLoading from "@/app/component/loading/Custom";
|
import CustomLoading from "@/app/component/loading/Custom";
|
||||||
|
|
||||||
function DoctorPage({ doctor, comments, rateAggregate, slug }) {
|
function DoctorPage({ doctor, comments, rateAggregate, addresses, slug }) {
|
||||||
return (
|
return (
|
||||||
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
|
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -37,9 +38,11 @@ function DoctorPage({ doctor, comments, rateAggregate, slug }) {
|
|||||||
doctor={doctor}
|
doctor={doctor}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
rateAggregate={rateAggregate}
|
rateAggregate={rateAggregate}
|
||||||
|
addresses={addresses}
|
||||||
/>
|
/>
|
||||||
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
||||||
</div>
|
</div>
|
||||||
|
<ClaimProfileSection doctor={doctor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,12 @@ function SendReq({
|
|||||||
if (setStep) {
|
if (setStep) {
|
||||||
setStep(3);
|
setStep(3);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/";
|
// بازگشت به صفحهٔ مبدأ اگر ?redirect= داده شده (فقط مسیر داخلی امن)
|
||||||
|
const redirect = new URLSearchParams(window.location.search).get("redirect");
|
||||||
|
window.location.href =
|
||||||
|
redirect && redirect.startsWith("/") && !redirect.startsWith("//")
|
||||||
|
? redirect
|
||||||
|
: "/";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsError(true);
|
setIsError(true);
|
||||||
|
|||||||
+2
-2
@@ -681,8 +681,8 @@ export function timeAgo(timestamp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isUserLoggedIn() {
|
export function isUserLoggedIn() {
|
||||||
const userInfo = Cookies.get("access_token");
|
// access_token در memory است نه کوکی؛ userInfo کوکیِ non-httpOnly است که هنگام لاگین ست میشود.
|
||||||
return !!userInfo;
|
return !!Cookies.get("userInfo");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertTimestampToPersianDateSimple(timestamp) {
|
export function convertTimestampToPersianDateSimple(timestamp) {
|
||||||
|
|||||||
Generated
+33
@@ -33,6 +33,7 @@
|
|||||||
"jalali-moment": "^3.3.11",
|
"jalali-moment": "^3.3.11",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.4",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"next": "^16.2.10",
|
"next": "^16.2.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
"react-easy-crop": "^5.5.6",
|
"react-easy-crop": "^5.5.6",
|
||||||
"react-google-map-picker": "^1.2.3",
|
"react-google-map-picker": "^1.2.3",
|
||||||
"react-images-uploading": "^3.1.7",
|
"react-images-uploading": "^3.1.7",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"styled-components": "^6.1.19",
|
"styled-components": "^6.1.19",
|
||||||
@@ -1745,6 +1747,17 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-leaflet/core": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-spring/animated": {
|
"node_modules/@react-spring/animated": {
|
||||||
"version": "9.7.5",
|
"version": "9.7.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -6359,6 +6372,12 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -9489,6 +9508,20 @@
|
|||||||
"version": "19.2.1",
|
"version": "19.2.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-leaflet": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"jalali-moment": "^3.3.11",
|
"jalali-moment": "^3.3.11",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.4",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"next": "^16.2.10",
|
"next": "^16.2.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"react-easy-crop": "^5.5.6",
|
"react-easy-crop": "^5.5.6",
|
||||||
"react-google-map-picker": "^1.2.3",
|
"react-google-map-picker": "^1.2.3",
|
||||||
"react-images-uploading": "^3.1.7",
|
"react-images-uploading": "^3.1.7",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"styled-components": "^6.1.19",
|
"styled-components": "^6.1.19",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -76,6 +76,10 @@ export const request = {
|
|||||||
getDoctorServices: () =>
|
getDoctorServices: () =>
|
||||||
api.get("api/v1/categorys/doctor_services", removeTokenHead),
|
api.get("api/v1/categorys/doctor_services", removeTokenHead),
|
||||||
postDoctorComment: (data) => api.post("api/v1/comment", data, { requireAuth: true }),
|
postDoctorComment: (data) => api.post("api/v1/comment", data, { requireAuth: true }),
|
||||||
|
getDoctorClaimInfo: (uuid) => api.get(`api/v1/doctor/${uuid}/claim-info`),
|
||||||
|
postDoctorClaim: (uuid, data) =>
|
||||||
|
api.post(`api/v1/doctor/${uuid}/claim`, data, { requireAuth: true }),
|
||||||
|
deleteDoctor: (uuid) => api.delete(`api/v1/doctor/${uuid}`, { requireAuth: true }),
|
||||||
getDoctorRate: (uuid) => api.get(`api/v1/rate/${uuid}`),
|
getDoctorRate: (uuid) => api.get(`api/v1/rate/${uuid}`),
|
||||||
getRateEligibility: (uuid) =>
|
getRateEligibility: (uuid) =>
|
||||||
api.get(`api/v1/rate/${uuid}/eligibility`, { requireAuth: true }),
|
api.get(`api/v1/rate/${uuid}/eligibility`, { requireAuth: true }),
|
||||||
|
|||||||
Reference in New Issue
Block a user