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