RBAC: Phase 1-3, Total Salary fix, employee creation fix, permission groups, backup script
Made-with: Cursor
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user