feat(doctor): claim-profile section + modal for unclaimed IRIMC-imported doctors
- ClaimProfileSection (components/doctor/claim): shown only when
doctor.owner_status === "unclaimed"; banner explains the profile is not
yet managed by the doctor, button "تأیید و مدیریت این پروفایل"
- Modal: login prompt when logged out; otherwise first/last name,
national code, Jalali birth-date (existing JalaliDatePicker) — posts to
POST api/v1/doctor/{uuid}/claim (identity verified server-side via API.ir;
no client call to API.ir, no token exposure)
- States: loading, per-field validation, server error (Persian envelope
message), double-submit guard, success welcome message + redirect
- Shared component across main domain and all representative subdomains
- services/response.js: getDoctorClaimInfo / postDoctorClaim
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } 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 { convertToJalali, isUserLoggedIn } from "@/helper";
|
||||||
|
import { request } from "@/services/response";
|
||||||
|
|
||||||
|
const initialForm = { national_code: "", birth_date: "", first_name: "", last_name: "" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* احراز مالکیت پروفایل پزشک ایمپورتشده (unclaimed).
|
||||||
|
* کامپوننت مشترک برای دامنهٔ اصلی و همهٔ زیردامنهها/دامنههای نمایندگان —
|
||||||
|
* منطق claim به دامنه وابسته نیست؛ احراز هویت کاملاً سمت سرور انجام میشود.
|
||||||
|
*/
|
||||||
|
function ClaimProfileSection({ doctor }) {
|
||||||
|
const router = useRouter();
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (doctor?.owner_status !== "unclaimed" || claimed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loggedIn = isUserLoggedIn();
|
||||||
|
|
||||||
|
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);
|
||||||
|
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");
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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" 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
|
||||||
|
/>
|
||||||
|
<JalaliDatePicker
|
||||||
|
value={form.birth_date}
|
||||||
|
placeholder="تاریخ تولد (شمسی)"
|
||||||
|
error={fieldErrors.birth_date}
|
||||||
|
stepwise
|
||||||
|
onChange={(e) => setField("birth_date", convertToJalali(e._d))}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-[#e0394a] text-[13px] leading-6 text-center">{error}</p>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="!rounded-[12px]"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
{loading ? "در حال بررسی هویت…" : "تأیید هویت و تصاحب پروفایل"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClaimProfileSection;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -40,6 +41,7 @@ function DoctorPage({ doctor, comments, rateAggregate, slug }) {
|
|||||||
/>
|
/>
|
||||||
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
||||||
</div>
|
</div>
|
||||||
|
<ClaimProfileSection doctor={doctor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ 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 }),
|
||||||
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