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 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user