RBAC: Phase 1-3, Total Salary fix, employee creation fix, permission groups, backup script

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-04 19:31:08 +04:00
parent 6034f774ed
commit 8edeaf10f5
46 changed files with 2751 additions and 598 deletions

View File

@@ -1,9 +1,10 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Shield, Edit, Users, Check, X } from 'lucide-react';
import { Shield, Edit, Users, Check, X, Plus } from 'lucide-react';
import { positionsAPI } from '@/lib/api/admin';
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
import { departmentsAPI } from '@/lib/api/employees';
import type { PositionRole, PositionPermission, CreatePositionData } from '@/lib/api/admin';
import Modal from '@/components/Modal';
import LoadingSpinner from '@/components/LoadingSpinner';
@@ -59,12 +60,25 @@ function buildMatrixFromPermissions(permissions: PositionPermission[]): Record<s
return matrix;
}
const initialCreateForm: CreatePositionData & { description?: string } = {
title: '',
titleAr: '',
code: '',
departmentId: '',
level: 5,
description: '',
};
export default function RolesManagement() {
const [roles, setRoles] = useState<PositionRole[]>([]);
const [departments, setDepartments] = useState<{ id: string; name: string; nameAr?: string | null }[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
const [showEditModal, setShowEditModal] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false);
const [createForm, setCreateForm] = useState(initialCreateForm);
const [createErrors, setCreateErrors] = useState<Record<string, string>>({});
const [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
const [saving, setSaving] = useState(false);
@@ -88,8 +102,44 @@ export default function RolesManagement() {
fetchRoles();
}, [fetchRoles]);
useEffect(() => {
departmentsAPI.getAll().then((depts) => setDepartments(depts)).catch(() => {});
}, []);
const currentRole = roles.find((r) => r.id === selectedRoleId);
const handleCreateRole = async (e: React.FormEvent) => {
e.preventDefault();
const errs: Record<string, string> = {};
if (!createForm.title?.trim()) errs.title = 'Required';
if (!createForm.code?.trim()) errs.code = 'Required';
if (!createForm.departmentId) errs.departmentId = 'Required';
setCreateErrors(errs);
if (Object.keys(errs).length > 0) return;
setSaving(true);
try {
const position = await positionsAPI.create({
title: createForm.title.trim(),
titleAr: createForm.titleAr?.trim() || undefined,
code: createForm.code.trim(),
departmentId: createForm.departmentId,
level: createForm.level ?? 5,
description: createForm.description?.trim() || undefined,
});
setShowCreateModal(false);
setCreateForm(initialCreateForm);
setCreateErrors({});
await fetchRoles();
setSelectedRoleId(position.id);
setShowEditModal(true);
} catch (err: unknown) {
setCreateErrors({ form: err instanceof Error ? err.message : 'Failed to create role' });
} finally {
setSaving(false);
}
};
useEffect(() => {
if (currentRole) {
setPermissionMatrix(buildMatrixFromPermissions(currentRole.permissions || []));
@@ -133,6 +183,13 @@ export default function RolesManagement() {
<h1 className="text-3xl font-bold text-gray-900 mb-2">الأدوار والصلاحيات</h1>
<p className="text-gray-600">إدارة أدوار المستخدمين ومصفوفة الصلاحيات</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-all shadow-md hover:shadow-lg"
>
<Plus className="h-5 w-5" />
<span className="font-semibold">إضافة دور</span>
</button>
</div>
{loading ? (
@@ -269,6 +326,104 @@ export default function RolesManagement() {
</div>
)}
{/* Create Role Modal */}
<Modal
isOpen={showCreateModal}
onClose={() => {
setShowCreateModal(false);
setCreateForm(initialCreateForm);
setCreateErrors({});
}}
title="إضافة دور جديد"
size="md"
>
<form onSubmit={handleCreateRole} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title (English) *</label>
<input
type="text"
value={createForm.title}
onChange={(e) => setCreateForm((p) => ({ ...p, title: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
placeholder="e.g. Sales Representative"
/>
{createErrors.title && <p className="text-red-500 text-xs mt-1">{createErrors.title}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title (Arabic)</label>
<input
type="text"
value={createForm.titleAr || ''}
onChange={(e) => setCreateForm((p) => ({ ...p, titleAr: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
placeholder="مندوب مبيعات"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Code *</label>
<input
type="text"
value={createForm.code}
onChange={(e) => setCreateForm((p) => ({ ...p, code: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
placeholder="SALES_REP"
/>
{createErrors.code && <p className="text-red-500 text-xs mt-1">{createErrors.code}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Department *</label>
<select
value={createForm.departmentId}
onChange={(e) => setCreateForm((p) => ({ ...p, departmentId: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
>
<option value="">Select department</option>
{departments.map((d) => (
<option key={d.id} value={d.id}>{d.nameAr || d.name}</option>
))}
</select>
{createErrors.departmentId && <p className="text-red-500 text-xs mt-1">{createErrors.departmentId}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Level</label>
<input
type="number"
min={1}
value={createForm.level ?? 5}
onChange={(e) => setCreateForm((p) => ({ ...p, level: parseInt(e.target.value, 10) || 5 }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
value={createForm.description || ''}
onChange={(e) => setCreateForm((p) => ({ ...p, description: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
rows={2}
placeholder="Optional description"
/>
</div>
{createErrors.form && <p className="text-red-500 text-sm">{createErrors.form}</p>}
<div className="flex gap-3 justify-end pt-4">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold disabled:opacity-50"
>
{saving ? 'Creating...' : 'Create Role'}
</button>
</div>
</form>
</Modal>
{/* Edit Permissions Modal */}
<Modal
isOpen={showEditModal}