+
{initials}
);
}
-function SpecialtyBadge({ name, idx }: { name: string; idx: number }) {
- return (
-
- {name}
-
- );
-}
-
-function DegreeBadge({ degree }: { degree: string | null }) {
- if (!degree) return null;
- const meta = DEGREE_META[degree] ?? { label: degree, cls: 'bg-slate-100 dark:bg-slate-400/10 text-slate-600 dark:text-slate-400' };
- return
{meta.label};
-}
-
-function ActiveBadge({ active }: { active: boolean }) {
- return active ? (
-
- فعال
-
- ) : (
-
- غیرفعال
-
- );
-}
-
function StarRating({ rate }: { rate: number }) {
return (
-
- {Array.from({ length: 5 }).map((_, i) => (
+
+ {Array.from({ length: 5 }).map((_, i) =>
i < Math.round(rate)
- ?
- :
- ))}
-
- );
-}
-
-function highlight(text: string | null | undefined, q: string): React.ReactNode {
- if (!text || !q.trim()) return text ?? '—';
- const idx = text.toLowerCase().indexOf(q.toLowerCase());
- if (idx === -1) return text;
- return (
- <>
- {text.slice(0, idx)}
-
- {text.slice(idx, idx + q.length)}
-
- {text.slice(idx + q.length)}
- >
- );
-}
-
-function KpiCard({ label, value, icon: Icon, iconBg, iconColor, sub }: {
- label: string; value: number | undefined;
- icon: React.ElementType; iconBg: string; iconColor: string; sub?: string;
-}) {
- return (
-
-
-
-
-
-
{label}
- {value === undefined
- ?
- :
{formatNumber(value)}
- }
- {sub &&
{sub}
}
-
-
- );
-}
-
-// ── Specialty Dropdown Filter ──────────────────────────────────────────────
-
-function SpecialtyFilter({ value, onChange, specialties }: {
- value: string; onChange: (v: string) => void;
- specialties: SpecialtyOption[];
-}) {
- const [open, setOpen] = useState(false);
- const [q, setQ] = useState('');
-
- const filtered = useMemo(
- () => specialties.filter(s => s.name.includes(q)),
- [specialties, q]
- );
-
- const selected = specialties.find(s => String(s.id) === value);
-
- useEffect(() => {
- if (!open) return;
- const close = (e: MouseEvent) => {
- if ((e.target as HTMLElement).closest('[data-specfilter]') === null) setOpen(false);
- };
- document.addEventListener('mousedown', close);
- return () => document.removeEventListener('mousedown', close);
- }, [open]);
-
- return (
-
-
-
- {open && (
-
-
- setQ(e.target.value)}
- placeholder="جستجوی تخصص..."
- className="cp-input text-sm h-8"
- data-specfilter
- />
-
-
-
- {filtered.map(s => (
-
- ))}
- {filtered.length === 0 &&
نتیجهای یافت نشد
}
-
-
+ ?
+ :
)}
-
- );
-}
-
-// ── Card View ─────────────────────────────────────────────────────────────
-
-function DoctorCard({ doctor, selected, onSelect, onView, onEdit, onToggle, onDelete, search, togglePending }: {
- doctor: AdminDoctor; selected: boolean;
- onSelect: () => void; onView: () => void; onEdit: () => void;
- onToggle: () => void; onDelete: () => void;
- search: string; togglePending: boolean;
-}) {
- const [menuOpen, setMenuOpen] = useState(false);
-
- useEffect(() => {
- if (!menuOpen) return;
- const close = (e: MouseEvent) => {
- if ((e.target as HTMLElement).closest('[data-cardmenu]') === null) setMenuOpen(false);
- };
- document.addEventListener('mousedown', close);
- return () => document.removeEventListener('mousedown', close);
- }, [menuOpen]);
-
- return (
-
- {/* Top row */}
-
-
-
-
-
- دکتر {highlight(doctor.name, search)}
-
-
- {doctor.degree &&
}
-
-
-
- {/* menu */}
-
-
- {menuOpen && (
-
-
-
-
-
-
-
- )}
-
-
-
- {/* Specialties */}
- {doctor.specialties.length > 0 && (
-
- {doctor.specialties.slice(0, 3).map((s, i) => )}
- {doctor.specialties.length > 3 && (
- +{doctor.specialties.length - 3}
- )}
-
- )}
-
- {/* Info row */}
-
- {doctor.mobile ?? '—'}
-
- {formatDate(doctor.created_at)}
-
-
+
);
}
@@ -348,38 +105,24 @@ export default function DoctorsPage() {
const navigate = useNavigate();
const qc = useQueryClient();
- const [page, setPage] = useState(1);
- const [limit, setLimit] = useState(25);
- const [searchInput, setSearch] = useState('');
- const [search, setDebouncedSearch] = useState('');
- const [status, setStatus] = useState('');
- const [gender, setGender] = useState('');
- const [specialtyId, setSpecialty] = useState('');
- const [sort, setSort] = useState('newest');
- const [view, setView] = useState<'table' | 'card'>('table');
- const [selected, setSelected] = useState
([]);
- const [activeMenu, setActiveMenu] = useState(null);
+ const [page, setPage] = useState(1);
+ const [limit] = useState(25);
+ const [searchInput, setSearchInput] = useState('');
+ const [search, setSearch] = useState('');
+ const [status, setStatus] = useState('');
+ const [specialtyId, setSpecialty] = useState('');
+ const [view, setView] = useState<'table' | 'grid'>('table');
+ const [selected, setSelected] = useState([]);
const [deleteTarget, setDeleteTarget] = useState(null);
const [confirmBulkDel, setConfirmBulkDel] = useState(false);
const [bulkDeleting, setBulkDeleting] = useState(false);
- // Debounce
useEffect(() => {
- const t = setTimeout(() => { setDebouncedSearch(searchInput); setPage(1); }, 350);
+ const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350);
return () => clearTimeout(t);
}, [searchInput]);
- useEffect(() => { setPage(1); setSelected([]); }, [status, gender, specialtyId, sort, limit]);
-
- // Close table row menu on outside click
- useEffect(() => {
- if (!activeMenu) return;
- const close = (e: MouseEvent) => {
- if ((e.target as HTMLElement).closest('[data-rowmenu]') === null) setActiveMenu(null);
- };
- document.addEventListener('mousedown', close);
- return () => document.removeEventListener('mousedown', close);
- }, [activeMenu]);
+ useEffect(() => { setPage(1); setSelected([]); }, [status, specialtyId]);
// ── Queries ──
@@ -396,12 +139,11 @@ export default function DoctorsPage() {
});
const doctorsQ = useQuery({
- queryKey: ['admin-doctors', page, limit, search, status, gender, specialtyId, sort],
+ queryKey: ['admin-doctors', page, limit, search, status, specialtyId],
queryFn: () => {
- const p = new URLSearchParams({ page: String(page), limit: String(limit), sort });
- if (search) p.set('search', search);
- if (status) p.set('status', status);
- if (gender) p.set('gender', gender);
+ const p = new URLSearchParams({ page: String(page), limit: String(limit) });
+ if (search) p.set('search', search);
+ if (status) p.set('status', status);
if (specialtyId) p.set('specialty_id', specialtyId);
return api.get>(`/api/v1/admin/doctors?${p}`);
},
@@ -442,10 +184,10 @@ export default function DoctorsPage() {
// ── Bulk ──
- const allSelected = items.length > 0 && items.every(d => selected.includes(d.uuid));
- const toggleAll = () => setSelected(allSelected ? [] : items.map(d => d.uuid));
+ const allSelected = items.length > 0 && items.every((d) => selected.includes(d.uuid));
+ const toggleAll = () => setSelected(allSelected ? [] : items.map((d) => d.uuid));
const toggleOne = (uuid: string) =>
- setSelected(prev => prev.includes(uuid) ? prev.filter(id => id !== uuid) : [...prev, uuid]);
+ setSelected((prev) => prev.includes(uuid) ? prev.filter((id) => id !== uuid) : [...prev, uuid]);
const handleBulkDelete = async () => {
setBulkDeleting(true);
@@ -461,346 +203,213 @@ export default function DoctorsPage() {
qc.invalidateQueries({ queryKey: ['doctors-stats'] });
};
- const hasFilters = !!(search || status || gender || specialtyId);
-
- const clearFilters = () => {
- setSearch(''); setDebouncedSearch('');
- setStatus(''); setGender(''); setSpecialty('');
- };
-
- // ── KPI ──
-
const kpiCards = [
- { label: 'کل پزشکان', value: stats?.total, icon: UserGroupIcon, iconBg: 'bg-slate-100 dark:bg-slate-400/10', iconColor: 'text-slate-600 dark:text-slate-400' },
- { label: 'فعال', value: stats?.active, icon: CheckCircleIcon, iconBg: 'bg-green-100 dark:bg-green-400/10', iconColor: 'text-green-600 dark:text-green-400' },
- { label: 'غیرفعال', value: stats?.inactive, icon: XCircleIcon, iconBg: 'bg-slate-100 dark:bg-slate-400/10', iconColor: 'text-slate-500 dark:text-slate-400' },
- { label: 'مرد', value: stats?.male, icon: UserGroupIcon, iconBg: 'bg-blue-100 dark:bg-blue-400/10', iconColor: 'text-blue-600 dark:text-blue-400' },
- { label: 'زن', value: stats?.female, icon: UserGroupIcon, iconBg: 'bg-pink-100 dark:bg-pink-400/10', iconColor: 'text-pink-600 dark:text-pink-400' },
+ { label: 'کل پزشکان', value: stats?.total, bg: 'var(--surface-3)', color: 'var(--text-2)' },
+ { label: 'فعال', value: stats?.active, bg: 'var(--success-bg)', color: 'var(--success)' },
+ { label: 'غیرفعال', value: stats?.inactive, bg: 'var(--surface-3)', color: 'var(--text-3)' },
+ { label: 'مرد', value: stats?.male, bg: 'var(--info-bg)', color: 'var(--info)' },
+ { label: 'زن', value: stats?.female, bg: 'var(--violet-bg)', color: 'var(--violet)' },
];
return (
-
-
- {/* ── Header ─────────────────────────────────────────────── */}
-
+
+ {/* Header */}
+
-
پزشکان
-
مدیریت پزشکان، تخصصها، کلینیکها و وضعیت همکاری
+
پزشکان
+
مدیریت پزشکان، تخصصها و وضعیت همکاری
+
+
+
+
-
- {/* ── KPI Cards ──────────────────────────────────────────── */}
-
- {kpiCards.map(c =>
)}
+ {/* KPI cards */}
+
+ {kpiCards.map((c) => (
+
+
+
{c.label}
+
+ {c.value === undefined
+ ?
+ : formatNumber(c.value)}
+
+
+ ))}
- {/* ── Main Card ──────────────────────────────────────────── */}
-
-
+ {/* Main card */}
+
{/* Toolbar */}
-
-
- {/* Row 1 */}
-
- {/* Search */}
-
-
+
+
+
+
setSearch(e.target.value)}
- placeholder="نام پزشک، موبایل یا ایمیل..."
- className="cp-input pr-9"
+ onChange={(e) => setSearchInput(e.target.value)}
+ placeholder="نام پزشک یا موبایل..."
/>
- {searchInput && (
-
- )}
-
- {/* Specialty dropdown */}
-
-
- {/* Status */}
-
-
- {/* Gender */}
-
-
- {/* Sort */}
-
-
- {/* Limit */}
-
-
- {/* View toggle */}
-
-
-
- {/* Active filter chips */}
- {hasFilters && (
-
- فیلترهای فعال:
- {search && (
-
- جستجو: {search}
- { setSearch(''); setDebouncedSearch(''); }} className="hover:text-primary-900 dark:hover:text-primary-100 ml-0.5">×
-
- )}
- {status && (
-
- وضعیت: {status === '1' ? 'فعال' : 'غیرفعال'}
- setStatus('')} className="hover:text-slate-900 dark:hover:text-slate-200 ml-0.5">×
-
- )}
- {gender && (
-
- جنسیت: {gender === 'man' ? 'مرد' : 'زن'}
- setGender('')} className="hover:text-slate-900 dark:hover:text-slate-200 ml-0.5">×
-
- )}
- {specialtyId && (
-
- تخصص: {specialties.find(s => String(s.id) === specialtyId)?.name}
- setSpecialty('')} className="hover:text-slate-900 dark:hover:text-slate-200 ml-0.5">×
-
- )}
- پاک کردن همه
-
- )}
- {/* Bulk actions */}
+ {/* Bulk bar */}
{selected.length > 0 && (
-
-
+
+
{formatNumber(selected.length)} پزشک انتخاب شده
-
-
setConfirmBulkDel(true)}
- className="text-xs px-3 py-1.5 rounded-lg bg-red-100 dark:bg-red-400/10 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-400/20 transition-colors font-medium"
- >
- حذف انتخابشدهها
-
-
setSelected([])} className="text-xs px-3 py-1.5 rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors">
- لغو انتخاب
-
+
+ setConfirmBulkDel(true)}>حذف انتخابشدهها
+ setSelected([])}>لغو
)}
- {/* ── TABLE VIEW ─────────────────────────────────────── */}
+ {/* Table view */}
{view === 'table' && (
-
-
+
+
-
- |
+ |
+ |
+ style={{ accentColor: 'var(--primary)', cursor: 'pointer' }} />
|
- پزشک |
- تخصصها |
- درجه |
- موبایل |
- وضعیت |
- امتیاز |
- ثبت |
- |
+ پزشک |
+ تخصص |
+ درجه |
+ امتیاز |
+ موبایل |
+ وضعیت |
+ |
-
-
- {/* Skeleton */}
- {doctorsQ.isLoading && Array.from({ length: 8 }).map((_, i) => (
+
+ {doctorsQ.isLoading && Array.from({ length: 6 }).map((_, i) => (
- |
-
-
- |
- |
- |
- |
- |
- |
- |
- |
+ {Array.from({ length: 8 }).map((_, j) => (
+
+
+ |
+ ))}
))}
- {/* Empty state */}
{!doctorsQ.isLoading && items.length === 0 && (
-
-
-
-
-
-
- {hasFilters ? 'نتیجهای برای فیلترهای انتخابی یافت نشد' : 'هیچ پزشکی ثبت نشده'}
-
- {hasFilters
- ? پاک کردن فیلترها
- : navigate('/admin/doctors/new')} className="cp-btn-primary text-xs">افزودن اولین پزشک
- }
-
+ |
+ هیچ پزشکی یافت نشد
|
)}
- {/* Data rows */}
- {!doctorsQ.isLoading && items.map(doctor => (
-
- {/* Checkbox */}
- | e.stopPropagation()}>
- toggleOne(doctor.uuid)}
- className="rounded border-slate-300 dark:border-gray-600 text-primary-600 cursor-pointer" />
+ {!doctorsQ.isLoading && items.map((doc) => (
+ |
navigate(`/admin/doctors/${doc.uuid}`)}>
+ | e.stopPropagation()}>
+ toggleOne(doc.uuid)}
+ style={{ accentColor: 'var(--primary)', cursor: 'pointer' }} />
|
-
- {/* Avatar + Name */}
-
- navigate(`/admin/doctors/${doctor.uuid}`)}>
-
-
-
- دکتر {highlight(doctor.name, search)}
-
-
- {doctor.gender === 'man' ? '♂ مرد' : doctor.gender === 'woman' ? '♀ زن' : '—'}
-
+
+
+
+
+ دکتر {doc.name}
+ {doc.gender ?? '—'}
|
-
- {/* Specialties */}
-
-
- {doctor.specialties.slice(0, 2).map((s, i) => )}
- {doctor.specialties.length > 2 && (
- +{doctor.specialties.length - 2}
- )}
- {doctor.specialties.length === 0 && —}
+
+ {doc.specialties.slice(0, 2).map((s) => (
+ {s.name}
+ ))}
+ {doc.specialties.length > 2 && (
+ +{doc.specialties.length - 2}
+ )}
+ |
+
+ {doc.degree && (
+
+
+ {DEGREE_LABEL[doc.degree] ?? doc.degree}
+
+ )}
+ |
+
+
+
+ {doc.rate.toFixed(1)}
|
-
- {/* Degree */}
-
-
+ |
+ {doc.mobile ?? '—'}
|
-
- {/* Mobile */}
-
-
- {highlight(doctor.mobile, search) || '—'}
+ |
+
+
+ {doc.is_active ? 'فعال' : 'غیرفعال'}
|
-
- {/* Status */}
- |
-
- {/* Rating */}
- |
-
- {/* Date */}
-
- {formatDate(doctor.created_at)}
- |
-
- {/* Actions */}
- e.stopPropagation()}>
-
- setActiveMenu(activeMenu === doctor.uuid ? null : doctor.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"
- >
-
+ e.stopPropagation()}>
+
+ navigate(`/admin/doctors/${doc.uuid}`)}>
+
+
+ toggleMut.mutate(doc.uuid)} disabled={toggleMut.isPending}>
+ {doc.is_active
+ ?
+ : }
+
+ setDeleteTarget(doc)}>
+
- {activeMenu === doctor.uuid && (
-
- { navigate(`/admin/doctors/${doctor.uuid}`); setActiveMenu(null); }}
- 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">
- مشاهده پروفایل
-
- { navigate(`/admin/doctors/${doctor.uuid}?edit=1`); setActiveMenu(null); }}
- 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">
- ویرایش اطلاعات
-
- { toggleMut.mutate(doctor.uuid); setActiveMenu(null); }}
- disabled={toggleMut.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 disabled:opacity-50"
- >
- {doctor.is_active
- ?
- :
- }
- {doctor.is_active ? 'غیرفعال کردن' : 'فعالسازی'}
-
- { navigate(`/admin/appointments?doctor=${doctor.uuid}`); setActiveMenu(null); }}
- 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">
- مشاهده نوبتها
-
-
- { setDeleteTarget(doctor); setActiveMenu(null); }}
- 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">
- حذف پزشک
-
-
- )}
|
| | |
@@ -810,67 +419,71 @@ export default function DoctorsPage() {
)}
- {/* ── CARD VIEW ──────────────────────────────────────── */}
- {view === 'card' && (
-
- {doctorsQ.isLoading ? (
-
- {Array.from({ length: 6 }).map((_, i) => (
-
-
-
-
+ {/* Grid view */}
+ {view === 'grid' && (
+
+ {doctorsQ.isLoading && Array.from({ length: 8 }).map((_, i) => (
+
+
- ) : items.length === 0 ? (
-
-
-
-
- {hasFilters ? 'نتیجهای یافت نشد' : 'هیچ پزشکی ثبت نشده'}
-
- {hasFilters &&
پاک کردن فیلترها}
- ) : (
-
- {items.map(doctor => (
-
toggleOne(doctor.uuid)}
- onView={() => navigate(`/admin/doctors/${doctor.uuid}`)}
- onEdit={() => navigate(`/admin/doctors/${doctor.uuid}?edit=1`)}
- onToggle={() => toggleMut.mutate(doctor.uuid)}
- onDelete={() => setDeleteTarget(doctor)}
- search={search}
- togglePending={toggleMut.isPending}
- />
- ))}
+ ))}
+
+ {!doctorsQ.isLoading && items.map((doc) => (
+ navigate(`/admin/doctors/${doc.uuid}`)}
+ >
+
+
+
+
+ دکتر {doc.name}
+
+
+ {doc.specialties[0]?.name ?? '—'}
+
+
+
+
+ {doc.is_active ? 'فعال' : 'غیرفعال'}
+
+
+
+
+
+ {doc.rate.toFixed(1)}
+
+ {formatDate(doc.created_at)}
+
+ ))}
+
+ {!doctorsQ.isLoading && items.length === 0 && (
+ هیچ پزشکی یافت نشد
)}
)}
- {/* Pagination */}
-
+
- {/* Delete confirm */}
+ {/* Dialogs */}
setDeleteTarget(null)}
/>
- {/* Bulk delete confirm */}
;
export default function LoginPage() {
const login = useAuthStore((s) => s.login);
- const darkMode = useUiStore((s) => s.darkMode);
- const toggleDarkMode = useUiStore((s) => s.toggleDarkMode);
const [showPass, setShowPass] = useState(false);
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
resolver: zodResolver(schema),
});
- useEffect(() => {
- document.documentElement.classList.toggle('dark', darkMode);
- }, [darkMode]);
-
const onSubmit = async (data: FormData) => {
try {
const res = await fetch('/api/v1/user/login', {
@@ -51,140 +44,81 @@ export default function LoginPage() {
};
return (
-
-
- {/* ── Left panel: branding ───────────────── */}
-
- {/* Gradient orbs */}
-
-
-
-
- {/* Logo */}
-
-
-
-
-
ClinicPro
+
+
+ {/* Logo */}
+
+
+ ♥
-
- {/* Center content */}
-
-
-
-
-
- سیستم مدیریت
کلینیک هوشمند
-
-
- مدیریت نوبتدهی، پزشکان، کلینیکها و پرداختها در یک پنل یکپارچه
-
-
- {/* Feature pills */}
-
- {['نوبتدهی آنلاین', 'مدیریت پزشکان', 'گزارشگیری', 'پرداخت امن'].map((f) => (
-
- {f}
-
- ))}
-
-
-
- {/* Bottom note */}
-
نسخه ۱.۰.۰ — تمامی حقوق محفوظ است
-
-
-
- {/* ── Right panel: form ──────────────────── */}
-
- {/* Dark mode toggle */}
-
-
- {darkMode
- ?
- : }
-
+
ورود به پنل ادمین
+
اطلاعات حساب مدیریتی خود را وارد کنید
-