feat(patient): edit patient basic info (doctor/clinic/secretary)

Add PATCH /api/v1/patient/{uuid} to update User.realName + UserProfile
demographic/insurance fields (mobile stays immutable, national_code
uniqueness enforced). Expose name/family separately in profile payload.
Add edit modal in my-patients. Update patient.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-12 22:00:33 +03:30
co-authored by Claude Opus 4.8
parent d3d09fe0f4
commit fdce888e0e
4 changed files with 304 additions and 1 deletions
+152 -1
View File
@@ -151,6 +151,15 @@ function MyPatientsPageInner() {
const [newPatientName, setNewPatientName] = useState("");
const mobileInputRef = useRef<HTMLInputElement>(null);
const EMPTY_EDIT = {
name: "", family: "", national_code: "", gender: "", blood_type: "",
marital_status: "", job: "", home_phone: "", work_phone: "", address: "",
basic_insurance_id: "", supplementary_insurance_id: "",
};
const [editOpen, setEditOpen] = useState(false);
const [editForm, setEditForm] = useState<Record<string, string>>(EMPTY_EDIT);
const setEditField = (k: string, v: string) => setEditForm((f) => ({ ...f, [k]: v }));
const servicesTotal = selectedServices.reduce(
(sum, s) => sum + s.price_rials,
0,
@@ -262,6 +271,60 @@ function MyPatientsPageInner() {
}
};
const openEditInfo = () => {
const p = patientProfile;
setEditForm({
name: p?.name ?? "",
family: p?.family ?? "",
national_code: p?.national_code ?? "",
gender: p?.gender ?? "",
blood_type: p?.blood_type ?? "",
marital_status: p?.marital_status ?? "",
job: p?.job ?? "",
home_phone: p?.home_phone ?? "",
work_phone: p?.work_phone ?? "",
address: p?.address ?? "",
basic_insurance_id: p?.basic_insurance_id != null ? String(p.basic_insurance_id) : "",
supplementary_insurance_id: p?.supplementary_insurance_id != null ? String(p.supplementary_insurance_id) : "",
});
setEditOpen(true);
};
const updatePatientMut = useMutation({
mutationFn: (body: object) =>
api.patch(`/api/v1/patient/${selectedRecord!.uuid}`, body),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["patient-detail", selectedRecord?.uuid] });
qc.invalidateQueries({ queryKey: ["patients"] });
setEditOpen(false);
toast.success("اطلاعات بیمار به‌روزرسانی شد");
},
onError: (e: any) => {
toast.error(e?.message || "خطا در به‌روزرسانی اطلاعات بیمار");
},
});
const submitEditInfo = () => {
if (!editForm.name.trim()) {
toast.error("نام بیمار الزامی است");
return;
}
updatePatientMut.mutate({
name: editForm.name.trim(),
family: editForm.family.trim() || null,
national_code: editForm.national_code.trim(),
gender: editForm.gender || null,
blood_type: editForm.blood_type.trim() || null,
marital_status: editForm.marital_status || null,
job: editForm.job.trim() || null,
home_phone: editForm.home_phone.trim() || null,
work_phone: editForm.work_phone.trim() || null,
address: editForm.address.trim() || null,
basic_insurance_id: editForm.basic_insurance_id ? Number(editForm.basic_insurance_id) : null,
supplementary_insurance_id: editForm.supplementary_insurance_id ? Number(editForm.supplementary_insurance_id) : null,
});
};
const createSessionMut = useMutation({
mutationFn: (body: object) =>
api.post(`/api/v1/patient/${selectedRecord!.uuid}/session`, body),
@@ -924,7 +987,12 @@ function MyPatientsPageInner() {
{detailTab === "info" && patientProfile && (
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>اطلاعات بیمار</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
<div style={{ fontWeight: 600, fontSize: 14 }}>اطلاعات بیمار</div>
<button className="cp-btn-secondary" style={{ height: 34, padding: "0 12px", fontSize: 13 }} onClick={openEditInfo}>
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش
</button>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: 12 }}>
{([
["نام کامل", patientProfile.full_name],
@@ -951,6 +1019,89 @@ function MyPatientsPageInner() {
</div>
)}
<Modal
open={editOpen}
title="ویرایش اطلاعات بیمار"
size="lg"
onClose={() => setEditOpen(false)}
footer={
<>
<button className="cp-btn-ghost" onClick={() => setEditOpen(false)}>انصراف</button>
<button className="cp-btn-primary" onClick={submitEditInfo} disabled={updatePatientMut.isPending}>
{updatePatientMut.isPending ? "در حال ذخیره…" : "ذخیره"}
</button>
</>
}
>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 12 }}>
<div>
<label className="cp-label">نام</label>
<input className="cp-input" value={editForm.name} onChange={(e) => setEditField("name", e.target.value)} />
</div>
<div>
<label className="cp-label">نام خانوادگی</label>
<input className="cp-input" value={editForm.family} onChange={(e) => setEditField("family", e.target.value)} />
</div>
<div>
<label className="cp-label">کد ملی</label>
<input className="cp-input" dir="ltr" inputMode="numeric" maxLength={10} value={editForm.national_code} onChange={(e) => setEditField("national_code", e.target.value.replace(/\D/g, ""))} />
</div>
<div>
<label className="cp-label">جنسیت</label>
<select className="cp-select" value={editForm.gender} onChange={(e) => setEditField("gender", e.target.value)}>
<option value=""></option>
<option value="male">مرد</option>
<option value="female">زن</option>
</select>
</div>
<div>
<label className="cp-label">گروه خونی</label>
<input className="cp-input" value={editForm.blood_type} onChange={(e) => setEditField("blood_type", e.target.value)} />
</div>
<div>
<label className="cp-label">وضعیت تأهل</label>
<select className="cp-select" value={editForm.marital_status} onChange={(e) => setEditField("marital_status", e.target.value)}>
<option value=""></option>
<option value="single">مجرد</option>
<option value="married">متأهل</option>
</select>
</div>
<div>
<label className="cp-label">شغل</label>
<input className="cp-input" value={editForm.job} onChange={(e) => setEditField("job", e.target.value)} />
</div>
<div>
<label className="cp-label">تلفن منزل</label>
<input className="cp-input" dir="ltr" value={editForm.home_phone} onChange={(e) => setEditField("home_phone", e.target.value)} />
</div>
<div>
<label className="cp-label">تلفن محل کار</label>
<input className="cp-input" dir="ltr" value={editForm.work_phone} onChange={(e) => setEditField("work_phone", e.target.value)} />
</div>
<div>
<label className="cp-label">بیمه پایه</label>
<select className="cp-select" value={editForm.basic_insurance_id} onChange={(e) => setEditField("basic_insurance_id", e.target.value)}>
<option value=""></option>
{baseInsuranceOptions.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
<div>
<label className="cp-label">بیمه تکمیلی</label>
<select className="cp-select" value={editForm.supplementary_insurance_id} onChange={(e) => setEditField("supplementary_insurance_id", e.target.value)}>
<option value=""></option>
{suppInsuranceOptions.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
</div>
<div style={{ marginTop: 12 }}>
<label className="cp-label">آدرس</label>
<textarea className="cp-textarea" rows={2} value={editForm.address} onChange={(e) => setEditField("address", e.target.value)} />
</div>
<div style={{ marginTop: 12, fontSize: 12, color: "var(--text-3)" }}>
شماره موبایل ({patientProfile?.mobile}) شناسهی حساب بیمار است و از اینجا قابل تغییر نیست.
</div>
</Modal>
{detailTab === "sessions" && (
<div className="card">
<div
+4
View File
@@ -459,6 +459,8 @@ export interface SmsSettings {
export interface PatientProfile {
full_name?: string | null;
name?: string | null;
family?: string | null;
national_code?: string | null;
gender?: string | null;
date_of_birth?: number | null;
@@ -469,7 +471,9 @@ export interface PatientProfile {
home_phone?: string | null;
work_phone?: string | null;
mobile?: string | null;
basic_insurance_id?: number | null;
basic_insurance_name?: string | null;
supplementary_insurance_id?: number | null;
supplementary_insurance_name?: string | null;
}