feat(admin): show user profile on the user detail page

Add a read-only پروفایل کاربر section to UserDetailPage that fetches
GET /api/v1/user-profile/{uuid} (admins are authorized) and renders the
patient profile — personal fields, insurance, and medical history from
other — with loading skeleton and an empty state when the user has no
profile. Response is double-nested (data.data).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-15 22:08:18 +03:30
co-authored by Claude Opus 4.8
parent 94d9f88cb2
commit d9c0729fbb
+97
View File
@@ -32,6 +32,35 @@ interface AdminUserDetail {
updated_at: string;
}
interface UserProfileData {
uuid: string;
national_code: string | null;
gender: string | null;
date_of_birth: number | null;
blood_type: string | null;
marital_status: string | null;
education: string | null;
job: string | null;
fathers_name: string | null;
address: string | null;
home_phone: string | null;
work_phone: string | null;
basic_insurance_id: number | null;
supplementary_insurance_id: number | null;
other: Record<string, unknown[]> | null;
}
const GENDER_LABELS: Record<string, string> = { male: 'مرد', female: 'زن' };
const MARITAL_LABELS: Record<string, string> = { married: 'متاهل', single: 'مجرد' };
const MEDICAL_SECTIONS: { key: string; label: string }[] = [
{ key: 'allergies', label: 'آلرژی‌ها' },
{ key: 'medications', label: 'داروهای مصرفی' },
{ key: 'surgeries', label: 'جراحی‌ها' },
{ key: 'family_history', label: 'سابقه خانوادگی بیماری' },
{ key: 'disease', label: 'بیماری‌ها' },
];
// ── Helpers ───────────────────────────────────────────────────────────────
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
@@ -192,6 +221,15 @@ export default function UserDetailPage() {
const user: AdminUserDetail | undefined = (data?.data as any)?.data ?? data?.data;
const profileQ = useQuery({
queryKey: ['admin-user-profile', uuid],
queryFn: () => api.get<ApiResponse<any>>(`/api/v1/user-profile/${uuid}`),
enabled: !!uuid,
retry: false,
});
const profile: UserProfileData | undefined =
(profileQ.data as any)?.data?.data ?? (profileQ.data as any)?.data;
// ── Edit form ──
const { register, handleSubmit, reset, formState: { errors } } = useForm<EditForm>({
@@ -476,6 +514,65 @@ export default function UserDetailPage() {
</div>
</div>
{/* ── User Profile (read-only) ──────────────────────────── */}
<div className="cp-card p-6 space-y-4">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">پروفایل کاربر</h2>
{profileQ.isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{Array.from({ length: 6 }).map((_, i) => <div key={i} className="h-16 rounded-xl skeleton" />)}
</div>
) : !profile ? (
<p className="text-sm text-slate-500 dark:text-slate-400 py-4">
این کاربر هنوز پروفایلی تکمیل نکرده است.
</p>
) : (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<InfoCard icon={UserIcon} label="نام پدر" value={profile.fathers_name} />
<InfoCard icon={ClipboardDocumentIcon} label="کد ملی" value={profile.national_code} mono copyable />
<InfoCard icon={UserIcon} label="جنسیت" value={profile.gender ? GENDER_LABELS[profile.gender] ?? profile.gender : null} />
<InfoCard icon={CalendarIcon} label="تاریخ تولد" value={profile.date_of_birth ? formatDate(profile.date_of_birth as any) : null} />
<InfoCard icon={UserIcon} label="گروه خونی" value={profile.blood_type} />
<InfoCard icon={UserIcon} label="وضعیت تأهل" value={profile.marital_status ? MARITAL_LABELS[profile.marital_status] ?? profile.marital_status : null} />
<InfoCard icon={UserIcon} label="تحصیلات" value={profile.education} />
<InfoCard icon={UserIcon} label="شغل" value={profile.job} />
<InfoCard icon={PhoneIcon} label="تلفن منزل" value={profile.home_phone} mono />
<InfoCard icon={PhoneIcon} label="تلفن محل کار" value={profile.work_phone} mono />
<InfoCard icon={ShieldCheckIcon} label="بیمه پایه" value={profile.basic_insurance_id != null ? String(profile.basic_insurance_id) : null} />
<InfoCard icon={ShieldCheckIcon} label="بیمه تکمیلی" value={profile.supplementary_insurance_id != null ? String(profile.supplementary_insurance_id) : null} />
<InfoCard icon={UserIcon} label="آدرس" value={profile.address} />
</div>
{/* Medical history */}
<div className="pt-2 border-t border-slate-100 dark:border-gray-700 space-y-3">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">سوابق پزشکی</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{MEDICAL_SECTIONS.map(({ key, label }) => {
const items = Array.isArray(profile.other?.[key]) ? (profile.other![key] as unknown[]) : [];
return (
<div key={key} className="p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60">
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-1.5">{label}</p>
{items.length === 0 ? (
<p className="text-sm text-slate-400 dark:text-slate-500">موردی ثبت نشده</p>
) : (
<ul className="flex flex-wrap gap-1.5">
{items.map((it, i) => (
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-white dark:bg-gray-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-gray-600">
{typeof it === 'string' ? it : (it as any)?.name ?? (it as any)?.title ?? JSON.stringify(it)}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
</>
)}
</div>
{/* ── Edit Modal ────────────────────────────────────────── */}
<Modal
open={editOpen}