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;
|
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 ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
|
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 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 ──
|
// ── Edit form ──
|
||||||
|
|
||||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<EditForm>({
|
const { register, handleSubmit, reset, formState: { errors } } = useForm<EditForm>({
|
||||||
@@ -476,6 +514,65 @@ export default function UserDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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 ────────────────────────────────────────── */}
|
{/* ── Edit Modal ────────────────────────────────────────── */}
|
||||||
<Modal
|
<Modal
|
||||||
open={editOpen}
|
open={editOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user