diff --git a/index.html b/index.html index b3ad731..2f759a3 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,24 @@ - + + + + + - داشبورد مدیریت - + پنل مدیریت عقیله + + diff --git a/plugins/add-data-locator.cjs b/plugins/add-data-locator.cjs new file mode 100644 index 0000000..1a8ca9d --- /dev/null +++ b/plugins/add-data-locator.cjs @@ -0,0 +1,40 @@ +module.exports = function addDataLocator({ types: t }) { + return { + name: "add-data-locator", + visitor: { + JSXOpeningElement(path, state) { + if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") { + return; + } + + const filePath = state.file.opts.filename; + + if (!filePath || filePath.includes("node_modules")) { + return; + } + + const attributeExists = path.node.attributes.some( + (attribute) => + t.isJSXAttribute(attribute) && + t.isJSXIdentifier(attribute.name) && + attribute.name.name === "data-locator", + ); + + if (attributeExists) { + return; + } + + const lineNumber = path.node.loc?.start.line ?? "unknown"; + const columnNumber = path.node.loc?.start.column ?? "unknown"; + const locatorValue = `${filePath}:${lineNumber}:${columnNumber}`; + + path.node.attributes.push( + t.jsxAttribute( + t.jsxIdentifier("data-locator"), + t.stringLiteral(locatorValue), + ), + ); + }, + }, + }; +}; diff --git a/plugins/add-data-locator.d.ts b/plugins/add-data-locator.d.ts new file mode 100644 index 0000000..f47e71d --- /dev/null +++ b/plugins/add-data-locator.d.ts @@ -0,0 +1,4 @@ +import type { PluginObj } from '@babel/core'; + +declare function addDataLocator(api: { types: unknown }): PluginObj; +export default addDataLocator; diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..1689d87 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/aqila-emblem.svg b/public/aqila-emblem.svg new file mode 100644 index 0000000..395bbbf --- /dev/null +++ b/public/aqila-emblem.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/aqila-logo-bronze.png b/public/aqila-logo-bronze.png new file mode 100644 index 0000000..2301d98 Binary files /dev/null and b/public/aqila-logo-bronze.png differ diff --git a/public/aqila-logo-white.png b/public/aqila-logo-white.png new file mode 100644 index 0000000..bf59ec0 Binary files /dev/null and b/public/aqila-logo-white.png differ diff --git a/public/aqila-logo.png b/public/aqila-logo.png new file mode 100644 index 0000000..08d4fef Binary files /dev/null and b/public/aqila-logo.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..fa80c83 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..7202344 Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..5bd261b Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..94f2a7c Binary files /dev/null and b/public/favicon.png differ diff --git a/public/favicon.svg b/public/favicon.svg index 9af6928..e055e57 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,7 +1,64 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/App.tsx b/src/app/App.tsx index 0254cae..7b91ead 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -7,6 +7,7 @@ import AppRouter from '@/app/router' import { cn } from '@/lib/utils' import { usePanelRouting } from '@/hooks/use-panel-routing' import { useAuth } from '@/features/auth/use-auth' +import DevClickToComponent from '@/components/utils/dev-click-to-component' /* * شل اصلی داشبورد (دولایه و راست‌چین RTL): @@ -20,11 +21,12 @@ import { useAuth } from '@/features/auth/use-auth' export default function App() { const { isAuthenticated } = useAuth() - if (!isAuthenticated) { - return - } - - return + return ( + <> + {isAuthenticated ? : } + {import.meta.env.DEV && } + + ) } function AuthenticatedShell() { @@ -32,7 +34,7 @@ function AuthenticatedShell() { return ( <> - + diff --git a/src/components/AqilaLogo.tsx b/src/components/AqilaLogo.tsx new file mode 100644 index 0000000..c45003d --- /dev/null +++ b/src/components/AqilaLogo.tsx @@ -0,0 +1,84 @@ +import React from 'react' + +interface AqilaLogoProps extends React.SVGProps { + className?: string +} + +/** + * آیکون و نشان رسمی عقیله (برگرفته از اپلیکیشن فلاتر و هویت بصری سامانه) + */ +export function AqilaLogo({ className = 'size-6', ...props }: AqilaLogoProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default AqilaLogo diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index d9c5dad..313763a 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -16,6 +16,7 @@ import { import { useAuth, useLogout } from '@/features/auth/use-auth' import { navigationConfig } from '@/config/navigation' import { siteConfig } from '@/config/site' +import { AqilaLogo } from '@/components/AqilaLogo' type NavItemProps = { to?: string @@ -70,17 +71,17 @@ function SidebarUserFooter() { const firstLetter = user?.name?.[0] ?? user?.email?.[0]?.toUpperCase() ?? 'م' return ( - - -
-
+ + +
+
{firstLetter}
-
+
{user?.name?.trim() || 'مدیر کل سیستم'}
-
+
{user?.email ?? 'admin@gmail.com'}
@@ -103,28 +104,25 @@ function SidebarUserFooter() { export default function AppSidebar() { return ( - - -
-
- + + +
+
+
-
+
{siteConfig.name}
-
- {siteConfig.description} -
- - {navigationConfig.map((group) => ( - - {group.title} + + {navigationConfig.map((group, idx) => ( + + {group.title ? {group.title} : null} {group.items.map((item) => ( { + health.checkHealth(false) + const timer = setInterval(() => { + health.checkHealth(true) + }, 15000) + return () => clearInterval(timer) + }, []) + + return ( + <> + + + + + ) +} diff --git a/src/components/layout/ServerStatusDialog.tsx b/src/components/layout/ServerStatusDialog.tsx new file mode 100644 index 0000000..c03a59a --- /dev/null +++ b/src/components/layout/ServerStatusDialog.tsx @@ -0,0 +1,177 @@ +import { useState } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Ic } from '@/icons' +import { useServerHealthStore } from '@/stores/server-health' +import { useAuthStore } from '@/stores/auth' + +interface ServerStatusDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusDialogProps) { + const health = useServerHealthStore() + const user = useAuthStore((s) => s.user) + const [isRefreshing, setIsRefreshing] = useState(false) + + const handleRefresh = async () => { + setIsRefreshing(true) + await health.checkHealth(false) + setIsRefreshing(false) + } + + return ( + + + +
+
+
+ +
+
+ + وضعیت سیستم و سرور + + + خلاصه ارتباط سرور، پیکربندی API و اطلاعات حساب + +
+
+ + +
+
+ +
+ {/* بخش ۱: وضعیت سرور جنگو */} +
+
+ + + وضعیت سرور جنگو (Backend) + + + {health.status === 'connected' + ? 'آنلاین و متصل' + : health.status === 'checking' + ? 'در حال بررسی...' + : 'قطع ارتباط'} + +
+
+ {health.message} + {health.latencyMs !== undefined && health.status === 'connected' && ( + + {health.latencyMs} ms + + )} +
+
+ + {/* بخش ۲: پیکربندی API و سرور */} +
+
+ + + پیکربندی API و سرور + +
+
+
+ Target Server: + http://127.0.0.1:8000 +
+
+ API Prefix: + /api +
+
+ Auth Header: + Token (DRF) +
+
+
+ + {/* بخش ۳: اطلاعات حساب کاربری جاری */} +
+
+ + + اطلاعات حساب کاربری جاری + + + {user?.role === 'superadmin' + ? 'مدیر ارشد (Superuser)' + : user?.role === 'admin' + ? 'مدیر سیستم (Staff)' + : 'کاربر'} + +
+
+
+ نام و نام خانوادگی: + {user?.name || 'مدیر سیستم'} +
+
+ ایمیل: + + {user?.email || '-'} + +
+
+ شناسه کاربری (ID): + + #{user?.id ?? 1} + +
+
+
+
+
+
+ ) +} diff --git a/src/components/layout/ThemeToggle.tsx b/src/components/layout/ThemeToggle.tsx new file mode 100644 index 0000000..5933f53 --- /dev/null +++ b/src/components/layout/ThemeToggle.tsx @@ -0,0 +1,28 @@ +import { useThemeStore } from '@/stores/theme' +import { Switch } from '@/components/ui/switch' +import { Ic } from '@/icons' + +export default function ThemeToggle() { + const { isDark, toggleTheme } = useThemeStore() + + return ( +
+ + + +
+ ) +} diff --git a/src/components/layout/Topbar.tsx b/src/components/layout/Topbar.tsx index fb94288..7007e0c 100644 --- a/src/components/layout/Topbar.tsx +++ b/src/components/layout/Topbar.tsx @@ -1,6 +1,7 @@ import { useLocation } from 'react-router-dom' import { SidebarTrigger } from '@/components/ui/sidebar' -import GlobalSearchTrigger from '@/features/global-search/GlobalSearchTrigger' +import ThemeToggle from '@/components/layout/ThemeToggle' +import ServerHealthTrigger from '@/components/layout/ServerHealthTrigger' import { navigationConfig } from '@/config/navigation' import { siteConfig } from '@/config/site' @@ -19,21 +20,29 @@ export default function Topbar() { if (pathname === '/') currentTitle = 'داشبورد مدیریت' return ( -
- +
+
+ - {/* عنوان و مسیر */} -
-

- {siteConfig.shortName} / {currentTitle} -

-

- {currentTitle} -

+ {/* عنوان و مسیر */} +
+

+ {siteConfig.shortName} / {currentTitle} +

+

+ {currentTitle} +

+
- {/* ابزار جستجوی سراسری (Cmd+K) */} - + {/* ابزارهای هدر: وضعیت سرور، سوئیچ تم */} +
+ {/* نشانگر کوچک وضعیت اتصال سرور */} + + + {/* سوئیچ دارک و لایت */} + +
) } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 1ae3ca4..bd3742c 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -22,7 +22,7 @@ function DialogContent({ className, children, ...props }: React.ComponentProps