لوحة التحكم الإدارية
لوحة التحكم الإدارية هي لوحة إدارة كاملة الميزات تُقدّم على admin.myapp.test. توفر واجهات CRUD لكل مورد في النظام، وتنقل محمي بالصلاحيات، ولوحة تحكم بإحصائيات فورية. واجهة المستخدم بالكامل مبنية بمكونات shadcn/ui وتتبع تخطيط متسق: sidebar + header + محتوى.
بنية التخطيط
كل صفحة مُصادق عليها في لوحة الإدارة تُعرض ضمن تخطيط ثلاثي الأجزاء:
┌──────────────────────────────────────────────────────────┐
│ ┌──────────┐ ┌─────────────────────────────────────────┐ │
│ │ │ │ Header │ │
│ │ │ │ (تبديل السمة، اللغة، قائمة المستخدم) │ │
│ │ │ ├─────────────────────────────────────────┤ │
│ │ Sidebar │ │ │ │
│ │ │ │ المحتوى الرئيسي │ │
│ │ (التنقل) │ │ │ │
│ │ │ │ ┌─────────────────────────────────┐ │ │
│ │ │ │ │ مكون الصفحة │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ (DataTable, نماذج, إلخ.) │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ └─────────────────────────────────┘ │ │
│ └──────────┘ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘التخطيط مُعرّف في app/(dashboard)/layout.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 عناصر التنقل بناءً على صلاحيات المستخدم المُصادق عليه. كل عنصر يُعيّن لسلسلة صلاحية -- إذا افتقر المستخدم للصلاحية المطلوبة، لا يُعرض العنصر إطلاقاً:
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 الذي يتحقق من مصفوفة صلاحيات المستخدم:
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) تعرض الإحصائيات والنشاط الأخير:
| القسم | المحتوى |
|---|---|
| بطاقات الإحصائيات | إجمالي المستخدمين، الأدوار النشطة، صفحات المحتوى، أحداث المراجعة الأخيرة |
| النشاط الأخير | أحدث إدخالات سجل المراجعة مع المستخدم والإجراء والمورد والطابع الزمني |
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 الإدارية:
| الوحدة | المسار | الصلاحيات | الميزات |
|---|---|---|---|
| المستخدمون | /users | users.view/create/edit/delete | عرض، إنشاء، تعديل، حذف، تعيين الأدوار |
| الأدوار | /roles | roles.view/create/edit/delete | عرض، إنشاء، تعديل، تعيين الصلاحيات |
| الصلاحيات | /permissions | permissions.view | عرض تسلسل هرمي للقراءة فقط |
| المحتويات | /contents | contents.view/create/edit/delete | محرر نص غني، ترجمات، SEO، وسائط |
| القوائم | /menus | menus.view/create/edit/delete | منشئ تنقل تسلسلي |
| البيانات المرجعية | /lookups | lookups.view/create/edit/delete | بيانات مرجعية مع أيقونات وألوان |
| الوسائط | /media | media.view/upload/delete | رفع بالسحب والإفلات، متصفح الملفات، نسخ الرابط |
| الترجمات | /translations | translations.view/edit | محرر ترجمة مفتاح-قيمة |
| الإعدادات | /settings | settings.view/edit | إعدادات التطبيق مُجمّعة |
| مفاتيح API | /api-keys | api_keys.view/create/delete | توليد مفاتيح مع تحديد نطاق الصلاحيات |
| سجلات المراجعة | /audit | audit.view | سجل نشاط للقراءة فقط مع فلاتر |
| الملف الشخصي | /profile | (أي مستخدم مُصادق عليه) | تعديل الاسم، البريد، كلمة المرور، الصورة الرمزية |
اصطلاح بنية الصفحة
كل وحدة CRUD تتبع تخطيط ملفات متسق:
app/(dashboard)/users/
├── page.tsx # صفحة القائمة مع DataTable
├── columns.tsx # تعريفات الأعمدة لـ DataTable
├── create/
│ └── page.tsx # نموذج الإنشاء
└── [id]/
├── page.tsx # صفحة العرض/التفاصيل
└── edit/
└── page.tsx # نموذج التعديلصفحة قائمة المستخدمين
صفحة قائمة المستخدمين توضح نمط CRUD القياسي المُستخدم في جميع أنحاء لوحة الإدارة:
"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 يعرض الصلاحيات مُجمّعة حسب الفئة مع عناصر تحكم تحديد الكل:
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 |
visibility | public أو auth أو guest |
parent_id | للتداخل (تسلسلي) |
sort_order | ترتيب العرض |
يمكن إعادة ترتيب عناصر القائمة بالسحب والإفلات وتداخلها لإنشاء هياكل تنقل متعددة المستويات.
صفحة الإعدادات
صفحة الإعدادات تعرض إعدادات التطبيق مُجمّعة حسب الفئة. كل مجموعة تُعرض كبطاقة بحقولها:
// Settings organized by group
const groups = [
{ key: "general", label: "الإعدادات العامة" },
{ key: "auth", label: "المصادقة" },
{ key: "email", label: "إعدادات البريد الإلكتروني" },
{ key: "storage", label: "إعدادات التخزين" },
];الإعدادات تدعم أنواع إدخال متنوعة (نص، رقم، boolean، select) وتُحفظ كأزواج مفتاح-قيمة عبر Settings API.
سجلات المراجعة
عارض سجل المراجعة يوفر عرض للقراءة فقط لنشاط النظام مع فلترة:
| الفلتر | الخيارات |
|---|---|
| الإجراء | إنشاء، تحديث، حذف، تسجيل دخول، تسجيل خروج |
| المورد | المستخدمون، الأدوار، المحتويات، الإعدادات، إلخ. |
| المستخدم | فلترة حسب المستخدم المنفذ |
| نطاق التاريخ | منتقي تاريخ البداية والنهاية |
كل إدخال سجل يعرض المستخدم الذي نفذ الإجراء، والمورد المتأثر، ونوع الإجراء، والطابع الزمني، واختيارياً البيانات المتغيرة (فرق قبل/بعد).
بوابة الصلاحيات
فحوصات الصلاحيات تحدث على مستويين في لوحة التحكم الإدارية:
- مستوى التنقل -- عناصر Sidebar تُخفى إذا افتقر المستخدم للصلاحية المطلوبة
- مستوى الصفحة -- أزرار الإجراءات (إنشاء، تعديل، حذف) تُعرض بشكل شرطي
// 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، الذي يُخزّن قائمة صلاحيات المستخدم الكاملة بعد تسجيل الدخول:
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 تُعرض مرة واحدة فقط عند الإنشاء. بعد إغلاق حوار الإنشاء، لا يمكن استرجاع السر مرة أخرى. انصح المستخدمين بنسخ المفتاح فوراً.
ما التالي
- تطبيق الويب — بنية التطبيق العام
- المكونات — DataTable وPermissionGate ومكونات أخرى
- النماذج والتحقق — أنماط النماذج المُستخدمة في صفحات CRUD الإدارية
- جلب البيانات — عميل API وأنماط تحميل البيانات