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:
hamed
2026-07-11 11:44:40 +03:30
co-authored by Claude Opus 4.8
parent 2fd0eec156
commit d8ab26b4b7
3 changed files with 189 additions and 0 deletions
+184
View File
@@ -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;
+2
View File
@@ -1,5 +1,6 @@
import Link from "next/link";
import AppointmentList from "./appointmentList";
import ClaimProfileSection from "./claim";
import DetailDoctor from "./detailDoctor";
import Share from "./detailDoctor/Share";
import CustomLoading from "@/app/component/loading/Custom";
@@ -40,6 +41,7 @@ function DoctorPage({ doctor, comments, rateAggregate, slug }) {
/>
<AppointmentList doctor={doctor} doctorSlug={slug} />
</div>
<ClaimProfileSection doctor={doctor} />
</div>
);
}
+3
View File
@@ -76,6 +76,9 @@ export const request = {
getDoctorServices: () =>
api.get("api/v1/categorys/doctor_services", removeTokenHead),
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}`),
getRateEligibility: (uuid) =>
api.get(`api/v1/rate/${uuid}/eligibility`, { requireAuth: true }),