Files
nobat724_front/components/doctor/claim/index.js
T
hamedandClaude Opus 4.8 4fed9a8c57 feat(doctor): fix map render + claim modal captcha/mobile + owner delete
- Doctor page map: the visible locations card read doctor.address (empty
  from the detail endpoint) while coordinates live in the separately
  fetched addresses. Thread `addresses` (with map.latitude/longitude) down
  page → DoctorPage → DetailDoctor → Locations; card hidden when empty
- Claim modal: updated info-box text ("نوبت‌های این پروفایل عمومی و غیرخاص
  هستند")، added mobile field (validated, must match account), added ALTCHA
  widget (submit disabled until captcha resolves; payload sent as `altcha`)
- Owner delete: services.deleteDoctor + a guarded two-step "حذف این پروفایل"
  in the claim success screen (owner enforced server-side; 403/409 shown)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:04:28 +03:30

256 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
import Altcha from "@/components/Altcha";
const initialForm = { national_code: "", birth_date: "", first_name: "", last_name: "", mobile: "" };
/**
* احراز مالکیت پروفایل پزشک ایمپورت‌شده (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);
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 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 = "نام خانوادگی الزامی است";
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;
};
const submit = async () => {
if (loading || !validate()) return;
setLoading(true);
setError("");
try {
await request.postDoctorClaim(doctor.uuid, { ...form, 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" 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={form.mobile}
error={!!fieldErrors.mobile}
helperText={fieldErrors.mobile || "باید با شماره حساب کاربری شما یکی باشد"}
onChange={(e) => setField("mobile", e.target.value)}
inputProps={{ maxLength: 11, 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;