add permission-groups
This commit is contained in:
259
frontend/src/app/admin/permission-groups/page.tsx
Normal file
259
frontend/src/app/admin/permission-groups/page.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { Plus, Edit, Trash2, Users2 } from 'lucide-react'
|
||||||
|
import Modal from '@/components/Modal'
|
||||||
|
|
||||||
|
type PermissionGroup = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
nameAr?: string
|
||||||
|
modules: string[]
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODULES = [
|
||||||
|
{ id: 'contacts', name: 'إدارة جهات الاتصال' },
|
||||||
|
{ id: 'crm', name: 'إدارة علاقات العملاء' },
|
||||||
|
{ id: 'inventory', name: 'المخزون والأصول' },
|
||||||
|
{ id: 'projects', name: 'المهام والمشاريع' },
|
||||||
|
{ id: 'hr', name: 'الموارد البشرية' },
|
||||||
|
{ id: 'marketing', name: 'التسويق' },
|
||||||
|
{ id: 'admin', name: 'لوحة الإدارة' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'permissionGroups'
|
||||||
|
|
||||||
|
export default function PermissionGroupsPage() {
|
||||||
|
const [groups, setGroups] = useState<PermissionGroup[]>([])
|
||||||
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
const [editing, setEditing] = useState<PermissionGroup | null>(null)
|
||||||
|
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [nameAr, setNameAr] = useState('')
|
||||||
|
const [selectedModules, setSelectedModules] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (raw) setGroups(JSON.parse(raw))
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(groups))
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [groups])
|
||||||
|
|
||||||
|
const baseModulesMap = useMemo(() => {
|
||||||
|
const m: Record<string, boolean> = {}
|
||||||
|
MODULES.forEach(x => (m[x.id] = false))
|
||||||
|
return m
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditing(null)
|
||||||
|
setName('')
|
||||||
|
setNameAr('')
|
||||||
|
setSelectedModules({ ...baseModulesMap })
|
||||||
|
setShowModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (g: PermissionGroup) => {
|
||||||
|
setEditing(g)
|
||||||
|
setName(g.name || '')
|
||||||
|
setNameAr(g.nameAr || '')
|
||||||
|
const m = { ...baseModulesMap }
|
||||||
|
g.modules.forEach(id => (m[id] = true))
|
||||||
|
setSelectedModules(m)
|
||||||
|
setShowModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleModule = (id: string) => {
|
||||||
|
setSelectedModules(prev => ({ ...prev, [id]: !prev[id] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
const finalName = name.trim() || nameAr.trim()
|
||||||
|
if (!finalName) {
|
||||||
|
alert('الرجاء إدخال اسم المجموعة')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mods = Object.keys(selectedModules).filter(k => selectedModules[k])
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
setGroups(prev =>
|
||||||
|
prev.map(g =>
|
||||||
|
g.id === editing.id
|
||||||
|
? { ...g, name: finalName, nameAr: nameAr.trim() || undefined, modules: mods }
|
||||||
|
: g
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const id = crypto?.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`
|
||||||
|
setGroups(prev => [
|
||||||
|
{ id, name: finalName, nameAr: nameAr.trim() || undefined, modules: mods, createdAt: now },
|
||||||
|
...prev,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = (g: PermissionGroup) => {
|
||||||
|
const ok = confirm(`هل أنت متأكد بحذف مجموعة الصلاحيات؟: ${g.nameAr || g.name} ؟`)
|
||||||
|
if (!ok) return
|
||||||
|
setGroups(prev => prev.filter(x => x.id !== g.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">مجموعات الصلاحيات</h1>
|
||||||
|
<p className="text-gray-600">إدارة مجموعات لتجميع الوحدات بشكل أسرع</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={openCreate}
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-semibold"
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
إضافة مجموعة
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{groups.length === 0 ? (
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-10 text-center">
|
||||||
|
<Users2 className="h-14 w-14 text-gray-300 mx-auto mb-3" />
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 mb-1">لا توجد مجموعات</h3>
|
||||||
|
<p className="text-gray-600">قم بإضافة مجموعة صلاحيات لتسهيل إدارة الأدوار.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{groups.map(g => (
|
||||||
|
<div key={g.id} className="bg-white rounded-xl border border-gray-200 p-5">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900">{g.nameAr || g.name}</h3>
|
||||||
|
<p className="text-xs text-gray-600">{g.name}</p>
|
||||||
|
<p className="text-sm text-gray-700 mt-3">
|
||||||
|
الوحدات: <span className="font-semibold">{g.modules.length}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => openEdit(g)}
|
||||||
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg"
|
||||||
|
title="تعديل"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => remove(g)}
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg"
|
||||||
|
title="حذف"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{g.modules.map(id => {
|
||||||
|
const m = MODULES.find(x => x.id === id)
|
||||||
|
return (
|
||||||
|
<span key={id} className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded-lg">
|
||||||
|
{m?.name || id}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={showModal}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
title={editing ? 'تعديل مجموعة الصلاحيات' : 'إضافة مجموعة صلاحيات'}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">الاسم (English)</label>
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
placeholder="e.g. Sales Group"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">الاسم (عربي)</label>
|
||||||
|
<input
|
||||||
|
value={nameAr}
|
||||||
|
onChange={e => setNameAr(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
placeholder="مثال: مجموعة المبيعات"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-2">الوحدات ضمن المجموعة</label>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{MODULES.map(m => (
|
||||||
|
<button
|
||||||
|
key={m.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleModule(m.id)}
|
||||||
|
className={`flex items-center justify-between px-3 py-2 rounded-lg border ${
|
||||||
|
selectedModules[m.id]
|
||||||
|
? 'border-green-500 bg-green-50'
|
||||||
|
: 'border-gray-200 bg-white hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-gray-800">{m.name}</span>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-0.5 rounded ${
|
||||||
|
selectedModules[m.id] ? 'bg-green-600 text-white' : 'bg-gray-200 text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedModules[m.id] ? 'ضمن المجموعة' : 'غير محدد'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
className="px-5 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={save}
|
||||||
|
className="px-5 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold"
|
||||||
|
>
|
||||||
|
حفظ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user