diff --git a/assets/admin/components/layout/Sidebar.tsx b/assets/admin/components/layout/Sidebar.tsx index e3cd05eb..3c2021e6 100644 --- a/assets/admin/components/layout/Sidebar.tsx +++ b/assets/admin/components/layout/Sidebar.tsx @@ -1,125 +1,169 @@ -import React from 'react'; -import { NavLink, useNavigate } from 'react-router-dom'; import { - ChartBarIcon, UserGroupIcon, HeartIcon, BuildingOffice2Icon, - CalendarDaysIcon, CreditCardIcon, BanknotesIcon, - ChatBubbleLeftEllipsisIcon, StarIcon, DevicePhoneMobileIcon, - TagIcon, DocumentTextIcon, UsersIcon, KeyIcon, - ArrowLeftOnRectangleIcon, -} from '@heroicons/react/24/outline'; -import { useUiStore } from '../../stores/uiStore'; -import { useAuthStore } from '../../stores/authStore'; + ArrowLeftOnRectangleIcon, + BanknotesIcon, + BuildingOffice2Icon, + CalendarDaysIcon, + ChartBarIcon, + ChatBubbleLeftEllipsisIcon, + CreditCardIcon, + DevicePhoneMobileIcon, + DocumentTextIcon, + HeartIcon, + KeyIcon, + StarIcon, + TagIcon, + UserGroupIcon, + UsersIcon, +} from "@heroicons/react/24/outline"; +import { NavLink, useNavigate } from "react-router-dom"; +import { useAuthStore } from "../../stores/authStore"; +import { useUiStore } from "../../stores/uiStore"; const sections = [ - { - label: 'عمومی', - items: [ - { to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' }, - { to: '/admin/users', icon: UserGroupIcon, label: 'کاربران' }, - { to: '/admin/doctors', icon: HeartIcon, label: 'پزشکان' }, - { to: '/admin/clinics', icon: BuildingOffice2Icon, label: 'کلینیک‌ها' }, - ], - }, - { - label: 'مدیریت', - items: [ - { to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبت‌ها' }, - { to: '/admin/payments', icon: CreditCardIcon, label: 'پرداخت‌ها' }, - { to: '/admin/settlements', icon: BanknotesIcon, label: 'تسویه‌حساب' }, - ], - }, - { - label: 'محتوا', - items: [ - { to: '/admin/comments', icon: ChatBubbleLeftEllipsisIcon, label: 'نظرات' }, - { to: '/admin/ratings', icon: StarIcon, label: 'امتیازها' }, - { to: '/admin/blogs', icon: DocumentTextIcon, label: 'بلاگ' }, - { to: '/admin/sms', icon: DevicePhoneMobileIcon, label: 'پیامک' }, - ], - }, - { - label: 'سیستم', - items: [ - { to: '/admin/categories', icon: TagIcon, label: 'دسته‌بندی‌ها' }, - { to: '/admin/representations', icon: UsersIcon, label: 'نمایندگان' }, - { to: '/admin/secretaries', icon: KeyIcon, label: 'منشی‌ها' }, - ], - }, + { + label: "عمومی", + items: [ + { to: "/admin/dashboard", icon: ChartBarIcon, label: "داشبورد" }, + { to: "/admin/users", icon: UserGroupIcon, label: "کاربران" }, + { to: "/admin/doctors", icon: HeartIcon, label: "پزشکان" }, + { + to: "/admin/clinics", + icon: BuildingOffice2Icon, + label: "کلینیک‌ها", + }, + ], + }, + { + label: "مدیریت", + items: [ + { + to: "/admin/appointments", + icon: CalendarDaysIcon, + label: "نوبت‌ها", + }, + { to: "/admin/payments", icon: CreditCardIcon, label: "پرداخت‌ها" }, + { + to: "/admin/settlements", + icon: BanknotesIcon, + label: "تسویه‌حساب", + }, + ], + }, + { + label: "محتوا", + items: [ + { + to: "/admin/comments", + icon: ChatBubbleLeftEllipsisIcon, + label: "نظرات", + }, + { to: "/admin/ratings", icon: StarIcon, label: "امتیازها" }, + { to: "/admin/blogs", icon: DocumentTextIcon, label: "بلاگ" }, + { to: "/admin/sms", icon: DevicePhoneMobileIcon, label: "پیامک" }, + ], + }, + { + label: "سیستم", + items: [ + { to: "/admin/categories", icon: TagIcon, label: "دسته‌بندی‌ها" }, + { + to: "/admin/representations", + icon: UsersIcon, + label: "نمایندگان", + }, + { to: "/admin/secretaries", icon: KeyIcon, label: "منشی‌ها" }, + ], + }, ]; interface Props { - mobileOpen?: boolean; - onMobileClose?: () => void; + mobileOpen?: boolean; + onMobileClose?: () => void; } -export default function Sidebar({ mobileOpen: _mobileOpen, onMobileClose: _onMobileClose }: Props) { - const sidebarOpen = useUiStore((s) => s.sidebarOpen); - const toggleSidebar = useUiStore((s) => s.toggleSidebar); - const logout = useAuthStore((s) => s.logout); - const navigate = useNavigate(); +export default function Sidebar({ + mobileOpen: _mobileOpen, + onMobileClose: _onMobileClose, +}: Props) { + const sidebarOpen = useUiStore((s) => s.sidebarOpen); + const logout = useAuthStore((s) => s.logout); + const navigate = useNavigate(); - return ( - + ); } diff --git a/assets/admin/components/layout/Topbar.tsx b/assets/admin/components/layout/Topbar.tsx index 381b1a61..7e956b52 100644 --- a/assets/admin/components/layout/Topbar.tsx +++ b/assets/admin/components/layout/Topbar.tsx @@ -1,109 +1,72 @@ import React from 'react'; -import { useLocation } from 'react-router-dom'; import { BellIcon, SunIcon, MoonIcon, Bars3Icon, Cog6ToothIcon, MagnifyingGlassIcon, XMarkIcon, CheckIcon, } from '@heroicons/react/24/outline'; -import { ChevronLeftIcon } from '@heroicons/react/24/outline'; import { useUiStore, HUES, type BrandHue } from '../../stores/uiStore'; -const CRUMBS: Record = { - '/admin/dashboard': ['داشبورد'], - '/admin/users': ['کاربران'], - '/admin/doctors': ['پزشکان'], - '/admin/clinics': ['کلینیک‌ها'], - '/admin/appointments': ['مدیریت', 'نوبت‌ها'], - '/admin/payments': ['مدیریت', 'پرداخت‌ها'], - '/admin/settlements': ['مدیریت', 'تسویه‌حساب'], - '/admin/comments': ['محتوا', 'نظرات'], - '/admin/ratings': ['محتوا', 'امتیازها'], - '/admin/blogs': ['محتوا', 'بلاگ'], - '/admin/sms': ['محتوا', 'پیامک'], - '/admin/categories': ['سیستم', 'دسته‌بندی‌ها'], - '/admin/representations': ['سیستم', 'نمایندگان'], - '/admin/secretaries': ['سیستم', 'منشی‌ها'], -}; - -function getCrumbs(pathname: string): string[] { - if (CRUMBS[pathname]) return CRUMBS[pathname]; - const entry = Object.entries(CRUMBS).find(([k]) => pathname.startsWith(k + '/')); - return entry ? entry[1] : ['داشبورد']; -} - -interface Props { - onMobileMenuOpen?: () => void; -} - -export default function Topbar({ onMobileMenuOpen }: Props) { - const location = useLocation(); - const toggleSidebar = useUiStore((s) => s.toggleSidebar); - const darkMode = useUiStore((s) => s.darkMode); - const toggleDarkMode = useUiStore((s) => s.toggleDarkMode); - const density = useUiStore((s) => s.density); - const setDensity = useUiStore((s) => s.setDensity); - const brandHue = useUiStore((s) => s.brandHue); - const setBrandHue = useUiStore((s) => s.setBrandHue); - const sidebarOpen = useUiStore((s) => s.sidebarOpen); - const settingsPanelOpen = useUiStore((s) => s.settingsPanelOpen); +export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () => void }) { + const toggleSidebar = useUiStore((s) => s.toggleSidebar); + const darkMode = useUiStore((s) => s.darkMode); + const toggleDarkMode = useUiStore((s) => s.toggleDarkMode); + const density = useUiStore((s) => s.density); + const setDensity = useUiStore((s) => s.setDensity); + const brandHue = useUiStore((s) => s.brandHue); + const setBrandHue = useUiStore((s) => s.setBrandHue); + const sidebarOpen = useUiStore((s) => s.sidebarOpen); + const settingsPanelOpen = useUiStore((s) => s.settingsPanelOpen); const toggleSettingsPanel = useUiStore((s) => s.toggleSettingsPanel); - const crumbs = getCrumbs(location.pathname); - return ( <>
- {/* Sidebar toggle — desktop only */} - - - {/* Mobile menu open — mobile only */} - {/* Search bar */}
- + ⌘K
- {/* Breadcrumbs */} - - -
- - {/* Settings */} - - {/* Dark mode */} {/* Bell */} - + {/* Settings */} + + {/* Avatar */} -
A
+
+ A +
{/* Settings Panel */} @@ -134,7 +97,7 @@ export default function Topbar({ onMobileMenuOpen }: Props) { style={{ flex: 1 }} onClick={() => { if (darkMode) toggleDarkMode(); }} > - + روشن @@ -151,21 +114,20 @@ export default function Topbar({ onMobileMenuOpen }: Props) { {/* Brand color */}
-
+
{HUES.map((hue) => ( ))}
@@ -206,11 +168,10 @@ export default function Topbar({ onMobileMenuOpen }: Props) {
- - تنظیمات شما به‌صورت خودکار ذخیره می‌شود و در بازدید بعدی حفظ می‌ماند. + تنظیمات شما به‌صورت خودکار ذخیره می‌شود.
diff --git a/assets/admin/pages/DoctorDetailPage.tsx b/assets/admin/pages/DoctorDetailPage.tsx index 38c810ce..dc201cff 100644 --- a/assets/admin/pages/DoctorDetailPage.tsx +++ b/assets/admin/pages/DoctorDetailPage.tsx @@ -1017,110 +1017,126 @@ function SessionEditor({ session, onChange, onRemove, addresses }: { }) { const upd = (k: K, v: SessionConfig[K]) => onChange({ ...session, [k]: v }); - const hasAdvanced = session.has_rest || session.patient_limit !== null; - const [showAdvanced, setShowAdvanced] = useState(hasAdvanced); - const slotCount = session.active ? calcSlotCount(session) : 0; + const slotCount = calcSlotCount(session); return ( -
- {/* ─ row اصلی */} -
- -
- از - upd('start_time', e.target.value)} - className="cp-input h-8 w-28 text-sm font-mono text-center px-2" /> - تا - upd('end_time', e.target.value)} - className="cp-input h-8 w-28 text-sm font-mono text-center px-2" /> +
+ + {/* Time inputs */} +
+
+
از ساعت
+
+ upd('start_time', e.target.value)} /> +
- - {session.active && slotCount > 0 && ( - - {slotCount} نوبت - - )} -
- {session.active && ( -
- {/* ─ مکان مطب — اجباری */} - {addresses.length > 0 && ( -
- -
- - {!session.location_id && ( -

انتخاب مکان مطب الزامی است

- )} -
-
- )} + {/* Duration + Location + Limit */} +
+
+
بازه هر نوبت
+
+ +
+
+
+
مکان نوبت
+
+ +
+
+
+
حداکثر نوبت
+
+ upd('patient_limit', e.target.value ? Number(e.target.value) : null)} /> +
+
+
- {/* ─ تنظیمات پیشرفته collapse */} - - {showAdvanced && ( -
-
- - upd('patient_limit', e.target.value ? Number(e.target.value) : null)} - className="cp-input h-8 text-sm w-full" /> -
-
- - {session.has_rest && ( -
-
- هر - upd('rest_interval', Number(e.target.value))} - className="cp-input h-7 w-16 text-sm text-center px-1" /> - دقیقه کار -
-
- استراحت - upd('time_to_rest', Number(e.target.value))} - className="cp-input h-7 w-16 text-sm text-center px-1" /> - دقیقه -
-
- )} -
+ {/* Rest toggle */} +
+
+ استراحت دوره‌ای بین نوبت‌ها +
توقف خودکار پس از مدت کاری مشخص
+
+ +
+ + {/* Rest params */} + {session.has_rest && ( +
+
+
هر (دقیقه کار)
+
+ upd('rest_interval', Number(e.target.value))} />
- )} +
+
+
استراحت (دقیقه)
+
+ upd('time_to_rest', Number(e.target.value))} /> +
+
)} + + {/* Slot count footer */} +
+ تعداد نوبت محاسبه‌شده در این بازه + {slotCount > 0 ? ( + + + {slotCount} نوبت + + ) : ( + + )} +
); } @@ -1228,65 +1244,59 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr )} {SCHEDULE_DAYS.map(day => { - const sessions = scheduleMap[day.key]?.sessions ?? []; - const isOverlap = overlapDays[day.key]; - const isExpanded = expandedDay === day.key; + const sessions = scheduleMap[day.key]?.sessions ?? []; + const isOverlap = overlapDays[day.key]; + const isExpanded = expandedDay === day.key; const activeSessions = sessions.filter(s => s.active); - const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0); - const hasAny = sessions.length > 0; + const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0); return ( -
- {/* ─ header هر روز */} +
+ {/* Day header */}
setExpandedDay(isExpanded ? null : day.key)} > - - {day.label} - - - {/* chips بازه‌های active */} -
- {sessions.length === 0 && ( - تعطیل - )} + {day.label} +
{activeSessions.map((s, i) => ( - - {s.start_time}–{s.end_time} + + {s.start_time} — {s.end_time} ))} - {daySlots > 0 && ( - - ({daySlots} نوبت) - - )} - {isOverlap && ( - - تداخل - - )} -
- -
- - + {sessions.length === 0 && تعطیل}
+ {daySlots > 0 && ( + + {daySlots} نوبت + + )} + {isOverlap && ( + + تداخل + + )} + +
- {/* ─ محتوای expanded */} + {/* Expanded sessions */} {isExpanded && ( -
+
{sessions.length === 0 ? ( -
-

هیچ بازه‌ای برای این روز تنظیم نشده

-
) : sessions.map((session, idx) => ( @@ -1309,11 +1319,8 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr )}
diff --git a/assets/admin/pages/DoctorsPage.tsx b/assets/admin/pages/DoctorsPage.tsx index b41633e3..74a308eb 100644 --- a/assets/admin/pages/DoctorsPage.tsx +++ b/assets/admin/pages/DoctorsPage.tsx @@ -5,8 +5,6 @@ import { MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon, CheckCircleIcon, XCircleIcon, TableCellsIcon, Squares2X2Icon, } from '@heroicons/react/24/outline'; -import { StarIcon as StarSolid } from '@heroicons/react/24/solid'; -import { StarIcon } from '@heroicons/react/24/outline'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; @@ -87,18 +85,6 @@ function DoctorAvatar({ name, id, image, size = 'sm' }: { ); } -function StarRating({ rate }: { rate: number }) { - return ( - - {Array.from({ length: 5 }).map((_, i) => - i < Math.round(rate) - ? - : - )} - - ); -} - // ── Main Page ───────────────────────────────────────────────────────────── export default function DoctorsPage() { @@ -112,17 +98,14 @@ export default function DoctorsPage() { 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); useEffect(() => { const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); return () => clearTimeout(t); }, [searchInput]); - useEffect(() => { setPage(1); setSelected([]); }, [status, specialtyId]); + useEffect(() => { setPage(1); }, [status, specialtyId]); // ── Queries ── @@ -182,26 +165,6 @@ export default function DoctorsPage() { onError: (e: Error) => toast.error(e.message), }); - // ── Bulk ── - - 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]); - - const handleBulkDelete = async () => { - setBulkDeleting(true); - let count = 0; - for (const uuid of selected) { - try { await api.delete(`/api/v1/doctor/${uuid}`); count++; } catch {} - } - setBulkDeleting(false); - setConfirmBulkDel(false); - setSelected([]); - toast.success(`${formatNumber(count)} پزشک حذف شد`); - qc.invalidateQueries({ queryKey: ['admin-doctors'] }); - qc.invalidateQueries({ queryKey: ['doctors-stats'] }); - }; const kpiCards = [ { label: 'کل پزشکان', value: stats?.total, bg: 'var(--surface-3)', color: 'var(--text-2)' }, @@ -293,33 +256,12 @@ export default function DoctorsPage() {
- {/* Bulk bar */} - {selected.length > 0 && ( -
- - {formatNumber(selected.length)} پزشک انتخاب شده - -
- - -
-
- )} - {/* Table view */} {view === 'table' && (
- @@ -332,7 +274,7 @@ export default function DoctorsPage() { {doctorsQ.isLoading && Array.from({ length: 6 }).map((_, i) => ( - {Array.from({ length: 8 }).map((_, j) => ( + {Array.from({ length: 7 }).map((_, j) => ( @@ -342,7 +284,7 @@ export default function DoctorsPage() { {!doctorsQ.isLoading && items.length === 0 && ( - @@ -350,11 +292,6 @@ export default function DoctorsPage() { {!doctorsQ.isLoading && items.map((doc) => ( navigate(`/admin/doctors/${doc.uuid}`)}> - - + @@ -461,10 +393,7 @@ export default function DoctorsPage() {
- - - {doc.rate.toFixed(1)} - + {doc.rate.toFixed(1)} {formatDate(doc.created_at)}
@@ -491,16 +420,6 @@ export default function DoctorsPage() { onCancel={() => setDeleteTarget(null)} /> - setConfirmBulkDel(false)} - /> ); } diff --git a/assets/admin/pages/UsersPage.tsx b/assets/admin/pages/UsersPage.tsx index e503db0c..21117f75 100644 --- a/assets/admin/pages/UsersPage.tsx +++ b/assets/admin/pages/UsersPage.tsx @@ -165,18 +165,15 @@ export default function UsersPage() { const [search, setSearch] = useState(''); const [role, setRole] = useState(''); const [status, setStatus] = useState(''); - const [selected, setSelected] = useState([]); - const [deleteTarget, setDeleteTarget] = useState(null); - const [roleTarget, setRoleTarget] = useState(null); - const [confirmBulkDel, setConfirmBulkDel] = useState(false); - const [bulkDeleting, setBulkDeleting] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [roleTarget, setRoleTarget] = useState(null); useEffect(() => { const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); return () => clearTimeout(t); }, [searchInput]); - useEffect(() => { setPage(1); setSelected([]); }, [role, status]); + useEffect(() => { setPage(1); }, [role, status]); // ── Queries ── @@ -238,27 +235,6 @@ export default function UsersPage() { onError: (e: Error) => toast.error(e.message), }); - // ── Bulk ── - - const allSelected = items.length > 0 && items.every((u) => selected.includes(u.uuid)); - const toggleAll = () => setSelected(allSelected ? [] : items.map((u) => u.uuid)); - const toggleOne = (uuid: string) => - setSelected((prev) => prev.includes(uuid) ? prev.filter((id) => id !== uuid) : [...prev, uuid]); - - const handleBulkDelete = async () => { - setBulkDeleting(true); - let count = 0; - for (const uuid of selected) { - try { await api.delete(`/api/v1/admin/users/${uuid}`); count++; } catch {} - } - setBulkDeleting(false); - setConfirmBulkDel(false); - setSelected([]); - toast.success(`${formatNumber(count)} کاربر حذف شد`); - qc.invalidateQueries({ queryKey: ['users'] }); - qc.invalidateQueries({ queryKey: ['users-stats'] }); - }; - // ── KPI data ── const kpiCards = [ @@ -335,32 +311,11 @@ export default function UsersPage() { - {/* Bulk bar */} - {selected.length > 0 && ( -
- - {formatNumber(selected.length)} کاربر انتخاب شده - -
- - -
-
- )} - {/* Table */}
- - پزشک تخصص درجه
+
هیچ پزشکی یافت نشد
e.stopPropagation()}> - toggleOne(doc.uuid)} - style={{ accentColor: 'var(--primary)', cursor: 'pointer' }} /> -
@@ -380,12 +317,7 @@ export default function DoctorsPage() { )}
-
- - {doc.rate.toFixed(1)} -
-
{doc.rate.toFixed(1)} {doc.mobile ?? '—'}
- @@ -372,7 +327,7 @@ export default function UsersPage() { {usersQ.isLoading && Array.from({ length: 7 }).map((_, i) => ( - {Array.from({ length: 7 }).map((_, j) => ( + {Array.from({ length: 6 }).map((_, j) => ( @@ -382,7 +337,7 @@ export default function UsersPage() { {!usersQ.isLoading && items.length === 0 && ( - @@ -390,11 +345,6 @@ export default function UsersPage() { {!usersQ.isLoading && items.map((user) => ( -
- - کاربر شماره موبایل نقش
+
هیچ کاربری یافت نشد
- toggleOne(user.uuid)} - style={{ accentColor: 'var(--primary)', cursor: 'pointer' }} /> -
@@ -454,17 +404,6 @@ export default function UsersPage() { onCancel={() => setDeleteTarget(null)} /> - setConfirmBulkDel(false)} - /> - {roleTarget && (