Skip to content

لوحة التحكم الإدارية

لوحة التحكم الإدارية هي لوحة إدارة كاملة الميزات تُقدّم على admin.myapp.test. توفر واجهات CRUD لكل مورد في النظام، وتنقل محمي بالصلاحيات، ولوحة تحكم بإحصائيات فورية. واجهة المستخدم بالكامل مبنية بمكونات shadcn/ui وتتبع تخطيط متسق: sidebar + header + محتوى.

بنية التخطيط

كل صفحة مُصادق عليها في لوحة الإدارة تُعرض ضمن تخطيط ثلاثي الأجزاء:

┌──────────────────────────────────────────────────────────┐
│ ┌──────────┐ ┌─────────────────────────────────────────┐ │
│ │          │ │              Header                     │ │
│ │          │ │  (تبديل السمة، اللغة، قائمة المستخدم)    │ │
│ │          │ ├─────────────────────────────────────────┤ │
│ │ Sidebar  │ │                                         │ │
│ │          │ │            المحتوى الرئيسي               │ │
│ │ (التنقل) │ │                                         │ │
│ │          │ │  ┌─────────────────────────────────┐     │ │
│ │          │ │  │      مكون الصفحة              │     │ │
│ │          │ │  │                                 │     │ │
│ │          │ │  │  (DataTable, نماذج, إلخ.)       │     │ │
│ │          │ │  │                                 │     │ │
│ │          │ │  └─────────────────────────────────┘     │ │
│ └──────────┘ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

التخطيط مُعرّف في app/(dashboard)/layout.tsx ويغلف جميع مسارات لوحة التحكم:

tsx
import { Sidebar } from "@/components/layout/sidebar";
import { Header } from "@/components/layout/header";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      <Sidebar />
      <div className="flex flex-1 flex-col overflow-hidden">
        <Header />
        <main className="flex-1 overflow-y-auto p-6">
          {children}
        </main>
      </div>
    </div>
  );
}

تنقل Sidebar

يعرض الـ sidebar عناصر التنقل بناءً على صلاحيات المستخدم المُصادق عليه. كل عنصر يُعيّن لسلسلة صلاحية -- إذا افتقر المستخدم للصلاحية المطلوبة، لا يُعرض العنصر إطلاقاً:

typescript
const navItems: NavItem[] = [
  {
    titleKey: "admin.nav.Dashboard",
    href: "/",
    icon: LayoutDashboard,
    permission: "dashboard.view",
  },
  {
    titleKey: "admin.nav.Users",
    icon: Users,
    permission: "users.view",
    children: [
      { titleKey: "admin.nav.All_Users", href: "/users", permission: "users.view" },
      { titleKey: "admin.nav.Create_User", href: "/users/create", permission: "users.create" },
    ],
  },
  {
    titleKey: "admin.nav.Contents",
    href: "/contents",
    icon: Newspaper,
    permission: "contents.view",
  },
  {
    titleKey: "admin.nav.Menus",
    href: "/menus",
    icon: Navigation,
    permission: "menus.view",
  },
  {
    titleKey: "admin.nav.Lookups",
    href: "/lookups",
    icon: ListTree,
    permission: "lookups.view",
  },
  {
    titleKey: "admin.nav.Media",
    href: "/media",
    icon: ImageIcon,
    permission: "media.view",
  },
  // ...additional items
];

كل NavItem يستخدم hook usePermission الذي يتحقق من مصفوفة صلاحيات المستخدم:

typescript
const hasPermission = usePermission(item.permission || "");

// If permission is required and user lacks it, hide the item
if (item.permission && !hasPermission) {
  return null;
}

نصيحة

عناصر التنقل مع children تُعرض كأقسام قابلة للطي باستخدام مكون Collapsible من Radix UI. الـ sidebar نفسه قابل للطي على سطح المكتب (وضع الأيقونات فقط) وينزلق كـ overlay على الجوال.

صفحة تسجيل الدخول

صفحة تسجيل الدخول للمدير (app/(auth)/login/page.tsx) توفر مصادقة مبنية على بيانات الاعتماد مع دعم OTP اختياري:

┌─────────────────────────────────┐
│       تسجيل دخول المدير          │
│                                 │
│  ┌───────────────────────────┐  │
│  │ البريد الإلكتروني          │  │
│  └───────────────────────────┘  │
│  ┌───────────────────────────┐  │
│  │ كلمة المرور                │  │
│  └───────────────────────────┘  │
│                                 │
│  [ تسجيل الدخول ]               │
│                                 │
│  ── أو ──                       │
│                                 │
│  [ تسجيل الدخول بـ OTP ]         │
└─────────────────────────────────┘

عند نجاح تسجيل الدخول، يُخزّن JWT access token في localStorage ويُعاد توجيه المستخدم للوحة التحكم. ثم يستدعي AuthProvider نقطة /auth/me لتحميل ملف المستخدم وأدواره وصلاحياته.

صفحة لوحة التحكم

صفحة لوحة التحكم (app/(dashboard)/page.tsx) تعرض الإحصائيات والنشاط الأخير:

القسمالمحتوى
بطاقات الإحصائياتإجمالي المستخدمين، الأدوار النشطة، صفحات المحتوى، أحداث المراجعة الأخيرة
النشاط الأخيرأحدث إدخالات سجل المراجعة مع المستخدم والإجراء والمورد والطابع الزمني
tsx
import { Stats } from "@/components/dashboard/stats";
import { RecentActivity } from "@/components/dashboard/recent-activity";

export default function DashboardPage() {
  return (
    <div className="space-y-8">
      <div>
        <h1 className="text-3xl font-bold tracking-tight">لوحة التحكم</h1>
        <p className="text-muted-foreground">
          نظرة عامة على تطبيقك
        </p>
      </div>
      <Stats />
      <RecentActivity />
    </div>
  );
}

صفحات CRUD

كل مورد يتبع نفس نمط بنية الصفحات. هذه القائمة الكاملة لوحدات CRUD الإدارية:

الوحدةالمسارالصلاحياتالميزات
المستخدمون/usersusers.view/create/edit/deleteعرض، إنشاء، تعديل، حذف، تعيين الأدوار
الأدوار/rolesroles.view/create/edit/deleteعرض، إنشاء، تعديل، تعيين الصلاحيات
الصلاحيات/permissionspermissions.viewعرض تسلسل هرمي للقراءة فقط
المحتويات/contentscontents.view/create/edit/deleteمحرر نص غني، ترجمات، SEO، وسائط
القوائم/menusmenus.view/create/edit/deleteمنشئ تنقل تسلسلي
البيانات المرجعية/lookupslookups.view/create/edit/deleteبيانات مرجعية مع أيقونات وألوان
الوسائط/mediamedia.view/upload/deleteرفع بالسحب والإفلات، متصفح الملفات، نسخ الرابط
الترجمات/translationstranslations.view/editمحرر ترجمة مفتاح-قيمة
الإعدادات/settingssettings.view/editإعدادات التطبيق مُجمّعة
مفاتيح API/api-keysapi_keys.view/create/deleteتوليد مفاتيح مع تحديد نطاق الصلاحيات
سجلات المراجعة/auditaudit.viewسجل نشاط للقراءة فقط مع فلاتر
الملف الشخصي/profile(أي مستخدم مُصادق عليه)تعديل الاسم، البريد، كلمة المرور، الصورة الرمزية

اصطلاح بنية الصفحة

كل وحدة CRUD تتبع تخطيط ملفات متسق:

app/(dashboard)/users/
├── page.tsx           # صفحة القائمة مع DataTable
├── columns.tsx        # تعريفات الأعمدة لـ DataTable
├── create/
│   └── page.tsx       # نموذج الإنشاء
└── [id]/
    ├── page.tsx       # صفحة العرض/التفاصيل
    └── edit/
        └── page.tsx   # نموذج التعديل

صفحة قائمة المستخدمين

صفحة قائمة المستخدمين توضح نمط CRUD القياسي المُستخدم في جميع أنحاء لوحة الإدارة:

tsx
"use client";

import * as React from "react";
import Link from "next/link";
import { toast } from "sonner";
import { DataTable } from "@/components/ui/data-table";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { usePermission } from "@/components/providers/auth-provider";
import { useTranslation } from "@/components/providers/i18n-provider";
import { api, getErrorMessage } from "@/lib/api";
import { getColumns } from "./columns";
import type { User } from "@/lib/types";
import { Plus } from "lucide-react";

export default function UsersPage() {
  const { t } = useTranslation();
  const canCreate = usePermission("users.create");
  const canEdit = usePermission("users.edit");
  const canDelete = usePermission("users.delete");

  const [users, setUsers] = React.useState<User[]>([]);
  const [isLoading, setIsLoading] = React.useState(true);
  const [deleteUser, setDeleteUser] = React.useState<User | null>(null);
  const [isDeleting, setIsDeleting] = React.useState(false);

  const fetchUsers = React.useCallback(async () => {
    setIsLoading(true);
    try {
      const response = await api.get<{
        data: { data: User[]; meta: unknown };
      }>("/admin/users");
      setUsers(response.data.data);
    } catch (error) {
      toast.error(getErrorMessage(error));
    } finally {
      setIsLoading(false);
    }
  }, []);

  React.useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  const handleDelete = async () => {
    if (!deleteUser) return;
    setIsDeleting(true);
    try {
      await api.delete(`/admin/users/${deleteUser.id}`);
      toast.success(t("admin.users.User_deleted_successfully"));
      setDeleteUser(null);
      fetchUsers();
    } catch (error) {
      toast.error(getErrorMessage(error));
    } finally {
      setIsDeleting(false);
    }
  };

  const columns = React.useMemo(
    () => getColumns({ onDelete: setDeleteUser, canEdit, canDelete }),
    [canEdit, canDelete]
  );

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold tracking-tight">
            {t("admin.users.Users")}
          </h1>
        </div>
        {canCreate && (
          <Button asChild>
            <Link href="/users/create">
              <Plus className="mr-2 h-4 w-4" />
              {t("admin.users.Add_User")}
            </Link>
          </Button>
        )}
      </div>

      <DataTable
        columns={columns}
        data={users}
        searchKey="name"
        searchPlaceholder={t("admin.users.Filter_users")}
      />

      <ConfirmDialog
        open={!!deleteUser}
        onOpenChange={(open) => !open && setDeleteUser(null)}
        title={t("admin.users.Delete_User")}
        description={t("admin.users.Are_you_sure_you_want_to_delete_this_user?")}
        confirmText={t("common.Delete")}
        variant="destructive"
        isLoading={isDeleting}
        onConfirm={handleDelete}
      />
    </div>
  );
}

تحذير

كل زر إجراء وعنصر قائمة يتحقق من الصلاحيات قبل العرض. hook usePermission يُرجع false إذا افتقر المستخدم للصلاحية المحددة، مما يسمح لك بإظهار أو إخفاء عناصر التحكم للإنشاء والتعديل والحذف بشكل شرطي.

إدارة الأدوار

صفحة إدارة الأدوار تتضمن مصفوفة صلاحيات لتعيين الصلاحيات للأدوار. مكون PermissionSelector يعرض الصلاحيات مُجمّعة حسب الفئة مع عناصر تحكم تحديد الكل:

tsx
import { PermissionSelector } from "@/components/ui/permission-selector";

<PermissionSelector
  permissions={allPermissions}
  selected={selectedPermissionIds}
  onChange={setSelectedPermissionIds}
/>

الصلاحيات تُعرض في شجرة تسلسلية:

المستخدمون
  ├── [x] عرض المستخدمين
  ├── [x] إنشاء المستخدمين
  ├── [x] تعديل المستخدمين
  └── [ ] حذف المستخدمين

المحتويات
  ├── [x] عرض المحتويات
  ├── [x] إنشاء المحتويات
  ├── [x] تعديل المحتويات
  └── [x] حذف المحتويات

إدارة المحتوى

محرر المحتوى يوفر تجربة تحرير غنية مع دعم متعدد اللغات وحقول SEO وإدارة الوسائط:

┌─────────────────────────────────────────────────┐
│  إنشاء محتوى                                     │
│                                                  │
│  ┌─────────────────────────────────────────────┐ │
│  │ [الإنجليزية] [العربية]                       │ │
│  │                                             │ │
│  │ العنوان:    [________________________]        │ │
│  │ الملخص:    [________________________]        │ │
│  │ التفاصيل: ┌─────────────────────────┐       │ │
│  │           │  محرر نص غني           │       │ │
│  │           │  (TinyMCE)              │       │ │
│  │           └─────────────────────────┘       │ │
│  │ عنوان SEO: [_______________________]        │ │
│  │ وصف SEO:  [_______________________]        │ │
│  └─────────────────────────────────────────────┘ │
│                                                  │
│  الـ Slug:    [________________________]          │
│  الحالة:     [منشور v]                           │
│  الصورة:     [رفع صورة]                          │
│                                                  │
│  [حفظ المحتوى]                                    │
└─────────────────────────────────────────────────┘

مكون ContentTranslationsInput يتعامل مع تبويبات لكل لغة للعنوان والملخص والتفاصيل (نص غني) ونص الزر وبيانات SEO الوصفية.

إدارة القوائم

منشئ القوائم يوفر واجهة شجرية لإدارة قوائم التنقل. كل عنصر قائمة يدعم:

الحقلالوصف
titleنص العرض (قابل للترجمة)
urlوجهة الرابط
target_self أو _blank
iconاسم أيقونة Lucide
visibilitypublic أو auth أو guest
parent_idللتداخل (تسلسلي)
sort_orderترتيب العرض

يمكن إعادة ترتيب عناصر القائمة بالسحب والإفلات وتداخلها لإنشاء هياكل تنقل متعددة المستويات.

صفحة الإعدادات

صفحة الإعدادات تعرض إعدادات التطبيق مُجمّعة حسب الفئة. كل مجموعة تُعرض كبطاقة بحقولها:

tsx
// Settings organized by group
const groups = [
  { key: "general", label: "الإعدادات العامة" },
  { key: "auth", label: "المصادقة" },
  { key: "email", label: "إعدادات البريد الإلكتروني" },
  { key: "storage", label: "إعدادات التخزين" },
];

الإعدادات تدعم أنواع إدخال متنوعة (نص، رقم، boolean، select) وتُحفظ كأزواج مفتاح-قيمة عبر Settings API.

سجلات المراجعة

عارض سجل المراجعة يوفر عرض للقراءة فقط لنشاط النظام مع فلترة:

الفلترالخيارات
الإجراءإنشاء، تحديث، حذف، تسجيل دخول، تسجيل خروج
الموردالمستخدمون، الأدوار، المحتويات، الإعدادات، إلخ.
المستخدمفلترة حسب المستخدم المنفذ
نطاق التاريخمنتقي تاريخ البداية والنهاية

كل إدخال سجل يعرض المستخدم الذي نفذ الإجراء، والمورد المتأثر، ونوع الإجراء، والطابع الزمني، واختيارياً البيانات المتغيرة (فرق قبل/بعد).

بوابة الصلاحيات

فحوصات الصلاحيات تحدث على مستويين في لوحة التحكم الإدارية:

  1. مستوى التنقل -- عناصر Sidebar تُخفى إذا افتقر المستخدم للصلاحية المطلوبة
  2. مستوى الصفحة -- أزرار الإجراءات (إنشاء، تعديل، حذف) تُعرض بشكل شرطي
typescript
// Check single permission
const canEdit = usePermission("users.edit");

// Usage in JSX
{canEdit && (
  <Button asChild>
    <Link href={`/users/${user.id}/edit`}>تعديل</Link>
  </Button>
)}

hook usePermission يقرأ من سياق AuthProvider، الذي يُخزّن قائمة صلاحيات المستخدم الكاملة بعد تسجيل الدخول:

typescript
export function usePermission(permission: string): boolean {
  const { user } = useAuth();

  if (!user) return false;
  if (user.is_super_admin) return true;

  return user.permissions?.includes(permission) ?? false;
}

نصيحة

المديرون الخارقون (is_super_admin: true) يتجاوزون جميع فحوصات الصلاحيات. يُعالج هذا في كلتا طبقتي الواجهة الأمامية (كل واجهة المستخدم مرئية) والخلفية (middleware يتخطى RBAC).

تدفق المصادقة

تطبيق الإدارة يستخدم AuthProvider مخصص يغلف جميع الصفحات. عند التحميل، يستدعي /auth/me للتحقق من JWT token الحالي:

┌──────────┐    ┌──────────────┐    ┌──────────────┐
│  تحميل   │───>│  فحص Token   │───>│  /auth/me    │
└──────────┘    └──────┬───────┘    └──────┬───────┘
                       │                   │
                ┌──────┴──────┐     ┌──────┴──────┐
                │ لا يوجد     │     │ Token صالح  │
                │ Token       │     │ تعيين حالة  │
                │ (مسار عام؟  │     │ المستخدم    │
                │  حسناً)     │     └─────────────┘
                └──────┬──────┘

                ┌──────┴──────┐
                │ مسار محمي؟  │
                │ إعادة       │
                │ التوجيه    │
                │ لـ /login   │
                └─────────────┘

دعم السمات

لوحة التحكم الإدارية تدعم السمات الداكنة والفاتحة عبر ThemeProvider (مدعوم بـ next-themes). زر ThemeToggle في الـ header يُبدّل بين الأوضاع. تفضيل السمة يُحفظ في localStorage.

إدارة مفاتيح API

صفحة إدارة مفاتيح API تسمح للمديرين بإنشاء وإلغاء مفاتيح API للوصول البرمجي:

  • إنشاء مفتاح -- تعيين اسم، اختيار صلاحيات، وتحديد تاريخ انتهاء اختيارياً
  • عرض المفاتيح -- عرض جميع المفاتيح مع طابع آخر استخدام والحالة
  • إلغاء مفتاح -- تعطيل مفتاح بشكل دائم (لا يمكن إعادة تفعيله)

تحذير

أسرار مفاتيح API تُعرض مرة واحدة فقط عند الإنشاء. بعد إغلاق حوار الإنشاء، لا يمكن استرجاع السر مرة أخرى. انصح المستخدمين بنسخ المفتاح فوراً.

ما التالي

Released under the MIT License.