diff --git a/src/app/router.tsx b/src/app/router.tsx index 98ae55d..d8bfd92 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -3,13 +3,9 @@ import { Routes, Route, Navigate, Outlet } from 'react-router-dom' import { RouteProgressStart, PageReady } from './nprogress' import RequireAuth from '@/features/auth/RequireAuth' -/* لود تنبل (Lazy Loading) صفحات */ +/* لود صفحات اصلی */ const LoginPage = lazy(() => import('@/features/auth/LoginPage')) const DashboardHome = lazy(() => import('@/features/dashboard/DashboardHome')) -const UsersPage = lazy(() => import('@/features/users/UsersPage')) -const ItemsPage = lazy(() => import('@/features/items/ItemsPage')) -const SettingsPage = lazy(() => import('@/features/settings/SettingsPage')) -const ComponentsShowcase = lazy(() => import('@/features/showcase/ComponentsShowcase')) export default function AppRouter() { return ( @@ -23,12 +19,8 @@ export default function AppRouter() { {/* تمام مسیرهای محافظت‌شده پشت گارد امنیتی */} }> } /> - } /> - } /> - } /> - } /> - {/* مسیرهای نامعتبر → هدایت به صفحه اصلی */} + {/* هدایت مسیرهای ناشناخته به صفحه اصلی */} } /> diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index 89441b5..d9c5dad 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -70,7 +70,7 @@ function SidebarUserFooter() { const firstLetter = user?.name?.[0] ?? user?.email?.[0]?.toUpperCase() ?? 'م' return ( - +
@@ -78,10 +78,10 @@ function SidebarUserFooter() {
- {user?.name?.trim() || 'مدیر سیستم'} + {user?.name?.trim() || 'مدیر کل سیستم'}
- {user?.email ?? 'admin@example.com'} + {user?.email ?? 'admin@gmail.com'}
diff --git a/src/components/layout/PanelSlot.tsx b/src/components/layout/PanelSlot.tsx index 468a50d..b151d48 100644 --- a/src/components/layout/PanelSlot.tsx +++ b/src/components/layout/PanelSlot.tsx @@ -10,22 +10,16 @@ import { SheetDescription, } from '@/components/ui/sheet' import { Sidebar, SidebarContext, useSidebar } from '@/components/ui/sidebar' -import UserDetailPanel from '@/features/panels/UserDetailPanel' -import SampleDetailPanel from '@/features/panels/SampleDetailPanel' -import FilterDrawer from '@/features/filters/FilterDrawer' /* - * شل سمت چپ (RTL): پنل کشویی جزئیات رکوردها / دراور فیلترها (قرینهٔ AppSidebar). - * - * دسکتاپ ≥1280px → Sidebar تمام‌ارتفاع offcanvas از چپ اسلاید می‌کند بدون اینکه محتوای وسط فشرده شود. + * شل سمت چپ (RTL): اسلات پنل کشویی جزئیات رکوردها (قرینهٔ AppSidebar). + * دسکتاپ ≥1280px → Sidebar تمام‌ارتفاع offcanvas از چپ اسلاید می‌کند. * زیر 1280px → Sheet استاندارد به صورت Modal Drawer از چپ باز می‌شود. */ export default function PanelSlot() { const isOverlay = useIsPanelOverlay() const sidebar = useSidebar() - const { activePanel, drawerParam, closePanel, closeDrawer, isPanelOpen } = usePanelRouting() - - const onClose = drawerParam ? closeDrawer : closePanel + const { activePanel, closePanel, isPanelOpen } = usePanelRouting() const [activeState, setActiveState] = useState<{ type: string; id?: string } | null>(activePanel) useEffect(() => { @@ -35,28 +29,19 @@ export default function PanelSlot() { } }, [activePanel]) - /* رندر پنل مورد نظر بر اساس نوع موجود در URL */ + /* رندر پنل مورد نظر */ const renderPanel = (type: string, id?: string) => { - switch (type) { - case 'user': - return - case 'item': - return - case 'filters': - return - default: - return ( -
-
- پنل جزئیات - -
-
- پنل ناشناخته ({type}) -
-
- ) - } + return ( +
+
+ پنل جزئیات ({type}) + +
+
+ شناسه رکورد: {id ?? 'نامشخص'} +
+
+ ) } const [mounted, setMounted] = useState(isPanelOpen) @@ -75,21 +60,21 @@ export default function PanelSlot() { const handleEsc = (e: KeyboardEvent) => { if (e.key !== 'Escape') return if (useGlobalSearchStore.getState().open) return - onClose() + closePanel() } window.addEventListener('keydown', handleEsc, true) return () => window.removeEventListener('keydown', handleEsc, true) - }, [isPanelOpen, onClose]) + }, [isPanelOpen, closePanel]) const shouldRender = isPanelOpen || mounted - const current = isPanelOpen ? (activePanel || (drawerParam ? { type: 'filters' } : null)) : activeState + const current = isPanelOpen ? activePanel : activeState const content = shouldRender && current ? renderPanel(current.type, current.id) : null /* ── در ابعاد کوچک (<1280px): نمایش به صورت Sheet کشویی ── */ if (isOverlay) { if (!shouldRender) return null return ( - !o && onClose()}> + !o && closePanel()}> { - setEmail('admin@example.com') + setEmail('admin@gmail.com') setPassword('admin') } @@ -79,7 +79,7 @@ export default function LoginPage() {
حساب آزمایشی آماده: - admin@example.com / admin + admin@gmail.com / admin
- -
- - {/* کارت‌های آماری بالا */} -
- {stats.map((stat) => ( + {/* نشانگر وضعیت خودکار رنگی (سبز / قرمز) */} +
-
- {stat.title} -
- -
-
-
{stat.value}
-
- {stat.change} - {stat.desc} -
+ + {health.status === 'connected' && ( + + )} + {health.status === 'error' && ( + + )} + + + + {health.status === 'connected' + ? 'ارتباط آنلاین با سرور' + : health.status === 'checking' + ? 'در حال بررسی...' + : 'قطع ارتباط'} + + {health.latencyMs !== undefined && health.status === 'connected' && ( + + ({health.latencyMs}ms) + + )}
- ))} +
- {/* بخش پایینی: جدول فعالیت‌های اخیر + کارت‌های دسترسی سریع */} -
- {/* جدول فعالیت‌های اخیر */} -
-
-

آخرین رویدادهای سیستم

- + {/* کارت‌های وضعیت سیستم */} +
+ {/* کارت وضعیت بک‌اند جنگو */} +
+
+ وضعیت سرور جنگو +
- -
- {recentActivities.map((act) => ( -
-
-
- {act.user.charAt(0)} -
-
-
{act.user}
-
{act.action}
-
-
-
- {act.date} - - موفق - -
-
- ))} +
+ + {health.status === 'connected' + ? 'متصل' + : health.status === 'checking' + ? 'در حال بررسی...' + : 'قطع ارتباط'} + + {health.latencyMs !== undefined && ( + + ({health.latencyMs} ms) + + )}
+

{health.message}

- {/* دسترسی‌های سریع */} -
-

- دسترسی‌های سریع -

-
- - - - - + {/* کارت مشخصات اتصال بک‌اند */} +
+
+ پیکربندی API و سرور +
+
+
+ Target Server: + http://127.0.0.1:8000 +
+
+ API Prefix: + /api +
+
+ Auth Header: + Token (DRF) +
+
+
-
-
- - راهنمای قالب + {/* کارت حساب کاربری */} +
+
+ اطلاعات حساب جاری + +
+
+
+ نام کاربر: + {user?.name || 'مدیر'} +
+
+ ایمیل: + + {user?.email || '-'} + +
+
+ نقش: + {user?.role || 'admin'}
-

- این قالب با قابلیت باز شدن پنل جزئیات (Drawer) از چپ و مدیریت انواع مودال‌ها طراحی شده است. -

+ + {/* بوم خالی آماده توسعه */} +
+
+ +
+

داشبورد تمیز و آماده توسعه

+

+ تمام اطلاعات و یوآی‌های ساختگی حذف شدند. می‌توانید ماژول‌های جدید خود را با کامپوننت‌های آماده UI پیاده‌سازی و مستقیماً به دیتابیس و بک‌اند عقیله متصل نمایید. +

+
) } diff --git a/src/features/filters/FilterDrawer.tsx b/src/features/filters/FilterDrawer.tsx deleted file mode 100644 index e0a123f..0000000 --- a/src/features/filters/FilterDrawer.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from 'react' -import { Ic } from '@/icons' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { - Accordion, AccordionItem, AccordionTrigger, AccordionContent, -} from '@/components/ui/accordion' -import PanelHeader from '@/components/shared/PanelHeader' -import { useFiltersStore } from '@/stores/filters' - -export default function FilterDrawer({ onClose }: { onClose: () => void }) { - const currentStatus = useFiltersStore((s) => s.status) - const setStatus = useFiltersStore((s) => s.setStatus) - const resetFilters = useFiltersStore((s) => s.reset) - - const [draftStatus, setDraftStatus] = useState(currentStatus) - - const toggleStatus = (val: string) => { - setDraftStatus((prev) => - prev.includes(val) ? prev.filter((s) => s !== val) : [...prev, val], - ) - } - - const onApply = () => { - setStatus(draftStatus) - onClose() - } - - const clearAll = () => { - setDraftStatus([]) - resetFilters() - } - - return ( -
- فیلترهای پیشرفته} - action={ - - } - onClose={onClose} - /> - -
- - - وضعیت - - {[ - { label: 'فعال / منتشر شده', value: 'active' }, - { label: 'پیش‌نویس / در انتظار', value: 'pending' }, - { label: 'غیرفعال / بایگانی', value: 'inactive' }, - ].map((item) => ( - - ))} - - - -
- -
- - -
-
- ) -} diff --git a/src/features/global-search/GlobalSearchDialog.tsx b/src/features/global-search/GlobalSearchDialog.tsx index 3bd24ff..7e6c74a 100644 --- a/src/features/global-search/GlobalSearchDialog.tsx +++ b/src/features/global-search/GlobalSearchDialog.tsx @@ -12,13 +12,11 @@ import { } from '@/components/ui/command' import { useGlobalSearchStore } from '@/stores/global-search' import { navigationConfig } from '@/config/navigation' -import { usePanelRouting } from '@/hooks/use-panel-routing' import { useLogout } from '@/features/auth/use-auth' export default function GlobalSearchDialog() { const { open, setOpen } = useGlobalSearchStore() const navigate = useNavigate() - const { openModal } = usePanelRouting() const logout = useLogout() // میانبر صفحه‌کلید Cmd+K یا Ctrl+K @@ -73,22 +71,6 @@ export default function GlobalSearchDialog() { {/* اکشن‌های سریع */} - runAction(() => openModal('user', 'new'))} - className="flex cursor-pointer items-center gap-2.5 rounded-lg px-3 py-2 text-xs" - > - - افزودن کاربر جدید - - - runAction(() => openModal('item', 'new'))} - className="flex cursor-pointer items-center gap-2.5 rounded-lg px-3 py-2 text-xs" - > - - افزودن آیتم جدید - - runAction(() => logout())} className="flex cursor-pointer items-center gap-2.5 rounded-lg px-3 py-2 text-xs text-rose-400" diff --git a/src/features/items/ItemsPage.tsx b/src/features/items/ItemsPage.tsx deleted file mode 100644 index 02e73ae..0000000 --- a/src/features/items/ItemsPage.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useState } from 'react' -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { getSampleItems, deleteSampleItem } from '@/services/sample-api' -import { Ic } from '@/icons' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Badge } from '@/components/ui/badge' -import { - Table, - TableHeader, - TableBody, - TableHead, - TableRow, - TableCell, -} from '@/components/ui/table' -import TablePagination from '@/components/shared/TablePagination' -import TableRowsSkeleton from '@/components/shared/TableRowsSkeleton' -import DeleteConfirmDialog from '@/components/shared/DeleteConfirmDialog' -import SampleItemModal from './SampleItemModal' -import { usePanelRouting } from '@/hooks/use-panel-routing' -import type { SampleItem } from '@/types' - -export default function ItemsPage() { - const queryClient = useQueryClient() - const { openPanel, openModal, closeModal, activeModal } = usePanelRouting() - - const [page, setPage] = useState(1) - const [search, setSearch] = useState('') - const [deleteTarget, setDeleteTarget] = useState(null) - - const { data, isLoading } = useQuery({ - queryKey: ['items', { page, search }], - queryFn: () => getSampleItems({ page, pageSize: 10, search }), - }) - - const deleteMutation = useMutation({ - mutationFn: (id: string) => deleteSampleItem(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['items'] }) - setDeleteTarget(null) - }, - }) - - const isItemModalOpen = activeModal?.type === 'item' - - const statusBadge = (status: string) => { - switch (status) { - case 'published': - return منتشر شده - case 'draft': - return پیش‌نویس - default: - return بایگانی - } - } - - return ( -
- {/* هدر صفحه */} -
-
-

مدیریت آیتم‌ها (الگوی CRUD)

-

- نمونه کامل صفحه مدیریت داده با جدول، فیلتر، مودال و نمایش کشویی جزئیات -

-
- -
- - {/* جستجو و ابزار */} -
-
- - { - setSearch(e.target.value) - setPage(1) - }} - placeholder="جستجوی عنوان یا دسته..." - className="ps-9 text-xs" - /> -
-
- مجموع: {data?.total || 0} مورد -
-
- - {/* جدول داده‌ها */} -
- - - - # - عنوان آیتم - دسته‌بندی - ارزش / مبلغ - وضعیت - تاریخ ثبت - عملیات - - - - {isLoading ? ( - - ) : !data?.items || data.items.length === 0 ? ( - - - موردی یافت نشد. - - - ) : ( - data.items.map((item, idx) => ( - openPanel('item', item.id)} - > - - {(page - 1) * 10 + idx + 1} - - -
{item.title}
- {item.description && ( -
- {item.description} -
- )} -
- - - {item.category} - - - - {item.price ? `${item.price.toLocaleString('fa-IR')} تومان` : 'رایگان'} - - {statusBadge(item.status)} - {item.createdAt} - e.stopPropagation()}> -
- - - -
-
-
- )) - )} -
-
- - {data && data.totalPages > 1 && ( -
- -
- )} -
- - {/* مودال ایجاد / ویرایش */} - { - if (!isOpen) closeModal() - }} - onSuccess={() => { - queryClient.invalidateQueries({ queryKey: ['items'] }) - }} - /> - - {/* دیالوگ تأیید حذف */} - !o && setDeleteTarget(null)} - title={`آیا از حذف "${deleteTarget?.title}" اطمینان دارید؟`} - isLoading={deleteMutation.isPending} - onConfirm={() => { - if (deleteTarget) { - deleteMutation.mutate(deleteTarget.id) - } - }} - /> -
- ) -} diff --git a/src/features/items/SampleItemModal.tsx b/src/features/items/SampleItemModal.tsx deleted file mode 100644 index cfe9383..0000000 --- a/src/features/items/SampleItemModal.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { useState, useEffect } from 'react' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' -import { Textarea } from '@/components/ui/textarea' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { createSampleItem, updateSampleItem, getSampleItem } from '@/services/sample-api' -import type { SampleItem, ItemStatus } from '@/types' - -interface SampleItemModalProps { - open: boolean - itemId?: string | null - onOpenChange: (open: boolean) => void - onSuccess?: (item: SampleItem) => void -} - -export default function SampleItemModal({ - open, - itemId, - onOpenChange, - onSuccess, -}: SampleItemModalProps) { - const isEditing = Boolean(itemId && itemId !== 'new') - - const [title, setTitle] = useState('') - const [category, setCategory] = useState('نرم‌افزار') - const [status, setStatus] = useState('published') - const [price, setPrice] = useState('') - const [description, setDescription] = useState('') - const [tags, setTags] = useState('') - const [loading, setLoading] = useState(false) - const [submitting, setSubmitting] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - if (open) { - setError(null) - if (isEditing && itemId) { - setLoading(true) - getSampleItem(itemId) - .then((it) => { - setTitle(it.title) - setCategory(it.category) - setStatus(it.status) - setPrice(it.price ? String(it.price) : '') - setDescription(it.description || '') - setTags(it.tags?.join('، ') || '') - }) - .catch(() => setError('دریافت اطلاعات با خطا مواجه شد.')) - .finally(() => setLoading(false)) - } else { - setTitle('') - setCategory('نرم‌افزار') - setStatus('published') - setPrice('') - setDescription('') - setTags('') - } - } - }, [open, isEditing, itemId]) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!title.trim()) { - setError('عنوان الزامی است.') - return - } - - try { - setSubmitting(true) - setError(null) - - const parsedTags = tags - .split(/[،,]/) - .map((t) => t.trim()) - .filter(Boolean) - - let result: SampleItem - if (isEditing && itemId) { - result = await updateSampleItem(itemId, { - title, - category, - status, - price: price ? Number(price) : undefined, - description, - tags: parsedTags, - }) - } else { - result = await createSampleItem({ - title, - category, - status, - price: price ? Number(price) : undefined, - description, - tags: parsedTags, - }) - } - - onSuccess?.(result) - onOpenChange(false) - } catch (err: any) { - setError(err?.message || 'عملیات با خطا مواجه شد.') - } finally { - setSubmitting(false) - } - } - - return ( - - - - - {isEditing ? 'ویرایش اطلاعات آیتم' : 'افزودن آیتم جدید'} - - - فرم زیر را تکمیل کرده و دکمه ثبت را کلیک کنید. - - - - {loading ? ( -
در حال بارگذاری...
- ) : ( -
- {error && ( -
- {error} -
- )} - -
- - setTitle(e.target.value)} - required - /> -
- -
-
- - -
- -
- - -
-
- -
-
- - setPrice(e.target.value)} - /> -
- -
- - setTags(e.target.value)} - /> -
-
- -
- -