Files
clinicpro/assets/admin/pages/UserDetailPage.tsx
T

601 lines
27 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
ArrowRightIcon, PencilIcon, TrashIcon, CheckCircleIcon,
XCircleIcon, ShieldCheckIcon, UserIcon, PhoneIcon,
EnvelopeIcon, CalendarIcon, KeyIcon, EllipsisVerticalIcon,
ClipboardDocumentIcon,
} from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { formatDate, formatDateTime } from '../lib/utils';
import Modal from '../components/ui/Modal';
import ConfirmDialog from '../components/ui/ConfirmDialog';
// ── Types ─────────────────────────────────────────────────────────────────
interface AdminUserDetail {
uuid: string;
id: number;
mobile_number: string;
name: string | null;
email: string | null;
roles: string[];
is_active: boolean;
status: number;
created_at: string;
updated_at: string;
}
// ── Helpers ───────────────────────────────────────────────────────────────
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
admin: { label: 'ادمین', cls: 'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300', dot: '#8b5cf6' },
doctor: { label: 'پزشک', cls: 'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300', dot: '#3b82f6' },
secretary: { label: 'منشی', cls: 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', dot: '#f97316' },
clinic: { label: 'کلینیک', cls: 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', dot: '#10b981' },
patient: { label: 'بیمار', cls: 'bg-slate-100 dark:bg-slate-400/10 text-slate-600 dark:text-slate-400', dot: '#94a3b8' },
};
const ROLE_MAP: Record<string, string[]> = {
admin: ['ROLE_USER', 'ROLE_ADMIN'],
doctor: ['ROLE_USER', 'ROLE_DOCTOR'],
secretary: ['ROLE_USER', 'ROLE_SECRETARY'],
clinic: ['ROLE_USER', 'ROLE_CLINIC'],
patient: ['ROLE_USER'],
};
function getPrimaryRole(roles: string[]): string {
if (roles.includes('ROLE_ADMIN')) return 'admin';
if (roles.includes('ROLE_DOCTOR')) return 'doctor';
if (roles.includes('ROLE_SECRETARY')) return 'secretary';
if (roles.includes('ROLE_CLINIC')) return 'clinic';
return 'patient';
}
const AVATAR_COLORS = [
'from-violet-500 to-purple-600',
'from-blue-500 to-cyan-600',
'from-emerald-500 to-teal-600',
'from-pink-500 to-rose-600',
'from-amber-500 to-orange-600',
];
function BigAvatar({ name, id }: { name: string | null; id: number }) {
const initials = name
? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2)
: '؟';
return (
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[id % AVATAR_COLORS.length]} flex items-center justify-center text-white text-2xl font-bold shadow-lg shrink-0`}>
{initials}
</div>
);
}
function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }: {
icon: React.ElementType; label: string; value: React.ReactNode;
mono?: boolean; copyable?: boolean;
}) {
const handleCopy = () => {
if (typeof value === 'string') {
navigator.clipboard.writeText(value);
toast.success('کپی شد');
}
};
return (
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors group">
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
<Icon className="w-4.5 h-4.5 text-slate-500 dark:text-slate-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-0.5">{label}</p>
<p className={`text-sm font-medium text-slate-800 dark:text-slate-200 truncate ${mono ? 'font-mono' : ''}`}
dir={mono ? 'ltr' : undefined}>
{value ?? <span className="text-slate-400 dark:text-slate-500 font-normal">ثبت نشده</span>}
</p>
</div>
{copyable && typeof value === 'string' && (
<button
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-white dark:hover:bg-gray-700 transition-all shrink-0"
>
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
</button>
)}
</div>
);
}
// ── Edit schema ───────────────────────────────────────────────────────────
const editSchema = z.object({
name: z.string().max(100).optional().or(z.literal('')),
email: z.string().email('ایمیل نامعتبر').optional().or(z.literal('')),
password: z.string().min(6, 'رمز عبور حداقل ۶ کاراکتر').optional().or(z.literal('')),
});
type EditForm = z.infer<typeof editSchema>;
// ── Role Change Modal ─────────────────────────────────────────────────────
function ChangeRoleModal({ current, loading, onSave, onClose }: {
current: string; loading: boolean;
onSave: (role: string) => void; onClose: () => void;
}) {
const [selected, setSelected] = useState(current);
return (
<Modal open title="تغییر نقش کاربر" onClose={onClose}
footer={
<>
<button onClick={onClose} className="cp-btn-secondary px-5">لغو</button>
<button onClick={() => onSave(selected)} disabled={loading} className="btn primary sm">
{loading ? 'در حال ذخیره...' : 'ذخیره تغییرات'}
</button>
</>
}
>
<div className="space-y-2">
{Object.entries(ROLE_META).map(([key, meta]) => (
<label
key={key}
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${
selected === key
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
: 'border-transparent hover:bg-slate-50 dark:hover:bg-gray-800 border border-slate-200 dark:border-gray-700'
}`}
>
<input type="radio" name="role" value={key} checked={selected === key}
onChange={() => setSelected(key)} className="sr-only" />
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: meta.dot }} />
<span className="text-sm font-medium text-slate-800 dark:text-slate-200 flex-1">{meta.label}</span>
{selected === key && <CheckCircleIcon className="w-4 h-4 text-primary-600 dark:text-primary-400" />}
</label>
))}
</div>
</Modal>
);
}
// ── Main Page ─────────────────────────────────────────────────────────────
export default function UserDetailPage() {
const { uuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const qc = useQueryClient();
const [editOpen, setEditOpen] = useState(searchParams.get('edit') === '1');
const [roleOpen, setRoleOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
if (!menuOpen) return;
const close = (e: MouseEvent) => {
if ((e.target as HTMLElement).closest('[data-upmenu]') === null) setMenuOpen(false);
};
document.addEventListener('mousedown', close);
return () => document.removeEventListener('mousedown', close);
}, [menuOpen]);
// ── Query ──
const { data, isLoading, isError } = useQuery({
queryKey: ['admin-user', uuid],
queryFn: () => api.get<ApiResponse<AdminUserDetail>>(`/api/v1/admin/users/${uuid}`),
enabled: !!uuid,
});
const user: AdminUserDetail | undefined = (data?.data as any)?.data ?? data?.data;
// ── Edit form ──
const { register, handleSubmit, reset, formState: { errors } } = useForm<EditForm>({
resolver: zodResolver(editSchema),
});
useEffect(() => {
if (user) reset({ name: user.name ?? '', email: user.email ?? '', password: '' });
}, [user, reset]);
// ── Mutations ──
const updateMut = useMutation({
mutationFn: (body: EditForm) => api.put<ApiResponse<AdminUserDetail>>(`/api/v1/admin/users/${uuid}`, body),
onSuccess: () => {
toast.success('اطلاعات بروزرسانی شد');
setEditOpen(false);
qc.invalidateQueries({ queryKey: ['admin-user', uuid] });
qc.invalidateQueries({ queryKey: ['users'] });
},
onError: (e: Error) => toast.error(e.message),
});
const toggleStatusMut = useMutation({
mutationFn: () => api.post<ApiResponse<{ is_active: boolean }>>(`/api/v1/admin/users/${uuid}/status`, {}),
onSuccess: () => {
toast.success('وضعیت کاربر تغییر کرد');
qc.invalidateQueries({ queryKey: ['admin-user', uuid] });
qc.invalidateQueries({ queryKey: ['users'] });
},
onError: (e: Error) => toast.error(e.message),
});
const updateRoleMut = useMutation({
mutationFn: (role: string) => api.put<ApiResponse<null>>(`/api/v1/admin/users/${uuid}/role`, { role }),
onSuccess: () => {
toast.success('نقش کاربر بروزرسانی شد');
setRoleOpen(false);
qc.invalidateQueries({ queryKey: ['admin-user', uuid] });
qc.invalidateQueries({ queryKey: ['users'] });
},
onError: (e: Error) => toast.error(e.message),
});
const deleteMut = useMutation({
mutationFn: () => api.delete<ApiResponse<null>>(`/api/v1/admin/users/${uuid}`),
onSuccess: () => {
toast.success('کاربر حذف شد');
navigate('/admin/users');
},
onError: (e: Error) => toast.error(e.message),
});
// ── Loading skeleton ──
if (isLoading) {
return (
<div className="animate-slide-up space-y-5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg skeleton" />
<div className="h-5 w-32 rounded skeleton" />
</div>
<div className="cp-card p-6">
<div className="flex items-start gap-5 mb-8">
<div className="w-20 h-20 rounded-2xl skeleton" />
<div className="flex-1 space-y-2 pt-1">
<div className="h-6 w-48 rounded skeleton" />
<div className="h-4 w-24 rounded skeleton" />
<div className="h-5 w-16 rounded-full skeleton mt-3" />
</div>
</div>
<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>
</div>
</div>
);
}
if (isError || !user) {
return (
<div className="animate-slide-up">
<div className="cp-card p-16 flex flex-col items-center gap-4 text-center">
<div className="w-16 h-16 rounded-2xl bg-red-100 dark:bg-red-400/10 flex items-center justify-center">
<XCircleIcon className="w-8 h-8 text-red-500" />
</div>
<p className="font-semibold text-slate-800 dark:text-slate-200">کاربر یافت نشد</p>
<p className="text-sm text-slate-500 dark:text-slate-400">این کاربر حذف شده یا UUID اشتباه است.</p>
<button onClick={() => navigate('/admin/users')} className="cp-btn-secondary mt-2">
بازگشت به لیست کاربران
</button>
</div>
</div>
);
}
const primaryRole = getPrimaryRole(user.roles);
const roleMeta = ROLE_META[primaryRole];
return (
<div className="animate-slide-up space-y-5">
{/* ── Breadcrumb / Back ─────────────────────────────────── */}
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<button
onClick={() => navigate('/admin/users')}
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors"
>
<ArrowRightIcon className="w-4 h-4" />
کاربران
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل کاربر</span>
</div>
{/* ── Profile Card ──────────────────────────────────────── */}
<div className="cp-card overflow-hidden">
{/* Cover strip */}
<div className="h-24 bg-gradient-to-l from-primary-600/20 via-primary-500/10 to-transparent dark:from-primary-500/20" />
{/* Profile header */}
<div className="px-6 pb-6 -mt-10 flex flex-col sm:flex-row sm:items-end gap-4">
<BigAvatar name={user.name} id={user.id} />
<div className="flex-1 min-w-0 sm:mb-1">
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50 leading-tight">
{user.name || <span className="text-slate-400 font-normal">نام ثبت نشده</span>}
</h1>
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full ${roleMeta.cls}`}>
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: roleMeta.dot }} />
{roleMeta.label}
</span>
{user.is_active ? (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-green-100 dark:bg-green-400/10 text-green-700 dark:text-green-300">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />فعال
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-400/10 text-slate-500 dark:text-slate-400">
<span className="w-1.5 h-1.5 rounded-full bg-slate-400" />غیرفعال
</span>
)}
<span className="text-xs text-slate-400 dark:text-slate-500">#{user.id}</span>
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-2 sm:mb-1">
<button
onClick={() => setEditOpen(true)}
className="cp-btn-primary text-sm"
>
<PencilIcon className="w-4 h-4" />
ویرایش اطلاعات
</button>
{/* More menu */}
<div className="relative" data-upmenu>
<button
data-upmenu
onClick={() => setMenuOpen(!menuOpen)}
className="cp-btn-secondary h-10 w-10 flex items-center justify-center px-0"
>
<EllipsisVerticalIcon className="w-4 h-4" />
</button>
{menuOpen && (
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-upmenu>
<button
onClick={() => { toggleStatusMut.mutate(); setMenuOpen(false); }}
disabled={toggleStatusMut.isPending}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors disabled:opacity-50"
>
{user.is_active
? <XCircleIcon className="w-4 h-4 text-orange-400 shrink-0" />
: <CheckCircleIcon className="w-4 h-4 text-green-500 shrink-0" />
}
{user.is_active ? 'غیرفعال کردن حساب' : 'فعال‌سازی حساب'}
</button>
<button
onClick={() => { setRoleOpen(true); setMenuOpen(false); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors"
>
<ShieldCheckIcon className="w-4 h-4 text-violet-400 shrink-0" />
تغییر نقش کاربر
</button>
<div className="border-t border-slate-100 dark:border-gray-700 my-1" />
<button
onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors"
>
<TrashIcon className="w-4 h-4 shrink-0" />
حذف این کاربر
</button>
</div>
)}
</div>
</div>
</div>
</div>
{/* ── Info Grid ─────────────────────────────────────────── */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
{/* Left: Contact info */}
<div className="lg:col-span-2 cp-card p-6 space-y-4">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">اطلاعات تماس و حساب</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<InfoCard icon={UserIcon} label="نام کامل" value={user.name} />
<InfoCard icon={PhoneIcon} label="شماره موبایل" value={user.mobile_number} mono copyable />
<InfoCard icon={EnvelopeIcon} label="آدرس ایمیل" value={user.email} copyable />
<InfoCard icon={ShieldCheckIcon} label="نقش در سیستم" value={roleMeta.label} />
<InfoCard icon={CalendarIcon} label="تاریخ عضویت" value={formatDate(user.created_at)} />
<InfoCard icon={CalendarIcon} label="آخرین بروزرسانی" value={formatDateTime(user.updated_at)} />
</div>
</div>
{/* Right: Roles + UUID */}
<div className="space-y-5">
{/* Roles card */}
<div className="cp-card p-5 space-y-3">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">نقش‌های سیستمی</h2>
<div className="flex flex-wrap gap-2">
{user.roles.map((r) => (
<span key={r} className="text-xs font-mono px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-gray-700">
{r}
</span>
))}
</div>
<button
onClick={() => setRoleOpen(true)}
className="w-full mt-1 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right"
>
تغییر نقش
</button>
</div>
{/* UUID card */}
<div className="cp-card p-5 space-y-2">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">شناسه یکتا (UUID)</h2>
<div className="flex items-center gap-2">
<p className="text-[11px] font-mono text-slate-500 dark:text-slate-400 break-all flex-1" dir="ltr">
{user.uuid}
</p>
<button
onClick={() => { navigator.clipboard.writeText(user.uuid); toast.success('UUID کپی شد'); }}
className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors shrink-0"
>
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Status card */}
<div className="cp-card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">وضعیت حساب</h2>
{user.is_active
? <span className="text-xs font-medium text-green-600 dark:text-green-400">فعال</span>
: <span className="text-xs font-medium text-slate-500">غیرفعال</span>
}
</div>
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-green-200 dark:bg-green-400/20' : 'bg-slate-200 dark:bg-gray-700'}`}>
<div className={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-green-500' : 'w-0'}`} />
</div>
<button
onClick={() => toggleStatusMut.mutate()}
disabled={toggleStatusMut.isPending}
className={`w-full mt-4 text-sm font-medium py-2 rounded-xl transition-colors disabled:opacity-50 ${
user.is_active
? 'bg-orange-50 dark:bg-orange-400/10 text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-400/20'
: 'bg-green-50 dark:bg-green-400/10 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-400/20'
}`}
>
{toggleStatusMut.isPending
? 'در حال تغییر...'
: user.is_active ? 'غیرفعال کردن حساب' : 'فعال‌سازی حساب'
}
</button>
</div>
</div>
</div>
{/* ── Edit Modal ────────────────────────────────────────── */}
<Modal
open={editOpen}
title="ویرایش اطلاعات کاربر"
size="md"
onClose={() => setEditOpen(false)}
footer={
<>
<button onClick={() => setEditOpen(false)} className="cp-btn-secondary px-5">لغو</button>
<button
form="edit-user-form"
type="submit"
disabled={updateMut.isPending}
className="btn primary sm"
>
{updateMut.isPending ? 'در حال ذخیره...' : 'ذخیره تغییرات'}
</button>
</>
}
>
<form id="edit-user-form" onSubmit={handleSubmit((v) => updateMut.mutate(v))} className="space-y-4">
{/* Avatar preview */}
<div className="flex items-center gap-4 pb-4 border-b border-slate-100 dark:border-gray-700">
<BigAvatar name={user.name} id={user.id} />
<div>
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200">
{user.name || 'نام ثبت نشده'}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 font-mono" dir="ltr">{user.mobile_number}</p>
</div>
</div>
{/* Name */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
نام و نام خانوادگی
</label>
<input
type="text"
placeholder="مثال: علی احمدی"
className={`cp-input ${errors.name ? 'border-red-400 dark:border-red-500' : ''}`}
{...register('name')}
/>
{errors.name && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.name.message}</p>}
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
آدرس ایمیل
</label>
<input
type="email"
placeholder="example@email.com"
dir="ltr"
className={`cp-input text-left ${errors.email ? 'border-red-400 dark:border-red-500' : ''}`}
{...register('email')}
/>
{errors.email && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.email.message}</p>}
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
رمز عبور جدید
<span className="text-xs font-normal text-slate-400 dark:text-slate-500 mr-1">(خالی = بدون تغییر)</span>
</label>
<div className="relative">
<KeyIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
<input
type="password"
placeholder="حداقل ۶ کاراکتر"
dir="ltr"
className={`cp-input pr-9 text-left ${errors.password ? 'border-red-400 dark:border-red-500' : ''}`}
{...register('password')}
/>
</div>
{errors.password && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.password.message}</p>}
</div>
{/* Mobile (read-only) */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
شماره موبایل
<span className="text-xs font-normal text-slate-400 dark:text-slate-500 mr-1">(قابل تغییر نیست)</span>
</label>
<input
type="text"
value={user.mobile_number}
readOnly
dir="ltr"
className="cp-input text-left bg-slate-50 dark:bg-gray-800/60 text-slate-400 cursor-not-allowed"
/>
</div>
</form>
</Modal>
{/* ── Role change modal ─────────────────────────────────── */}
{roleOpen && (
<ChangeRoleModal
current={primaryRole}
loading={updateRoleMut.isPending}
onSave={(r) => updateRoleMut.mutate(r)}
onClose={() => setRoleOpen(false)}
/>
)}
{/* ── Delete confirm ────────────────────────────────────── */}
<ConfirmDialog
open={deleteOpen}
title="حذف کاربر"
message={`آیا از حذف کاربر "${user.name ?? user.mobile_number}" اطمینان دارید؟ این عمل قابل بازگشت نیست و تمام داده‌های مرتبط پاک می‌شود.`}
confirmLabel="بله، حذف کن"
danger
loading={deleteMut.isPending}
onConfirm={() => deleteMut.mutate()}
onCancel={() => setDeleteOpen(false)}
/>
</div>
);
}