fix roles view
This commit is contained in:
@@ -134,6 +134,42 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createPosition(req: AuthRequest, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.id;
|
||||||
|
const position = await adminService.createPosition(
|
||||||
|
{
|
||||||
|
title: req.body.title,
|
||||||
|
titleAr: req.body.titleAr,
|
||||||
|
departmentId: req.body.departmentId,
|
||||||
|
level: req.body.level,
|
||||||
|
code: req.body.code,
|
||||||
|
},
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
res.status(201).json(ResponseFormatter.success(position));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePosition(req: AuthRequest, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.id;
|
||||||
|
const position = await adminService.updatePosition(
|
||||||
|
req.params.id,
|
||||||
|
{
|
||||||
|
title: req.body.title,
|
||||||
|
titleAr: req.body.titleAr,
|
||||||
|
},
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
res.json(ResponseFormatter.success(position));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updatePositionPermissions(req: AuthRequest, res: Response, next: NextFunction) {
|
async updatePositionPermissions(req: AuthRequest, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const position = await adminService.updatePositionPermissions(
|
const position = await adminService.updatePositionPermissions(
|
||||||
|
|||||||
@@ -89,6 +89,34 @@ router.get(
|
|||||||
adminController.getPositions
|
adminController.getPositions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create role
|
||||||
|
router.post(
|
||||||
|
'/positions',
|
||||||
|
authorize('admin', 'roles', 'create'),
|
||||||
|
[
|
||||||
|
body('title').notEmpty().trim(),
|
||||||
|
body('titleAr').optional().isString().trim(),
|
||||||
|
body('departmentId').isUUID(),
|
||||||
|
body('level').optional().isInt({ min: 1 }),
|
||||||
|
body('code').optional().isString().trim(),
|
||||||
|
],
|
||||||
|
validate,
|
||||||
|
adminController.createPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update role name (title/titleAr)
|
||||||
|
router.put(
|
||||||
|
'/positions/:id',
|
||||||
|
authorize('admin', 'roles', 'update'),
|
||||||
|
[
|
||||||
|
param('id').isUUID(),
|
||||||
|
body('title').optional().notEmpty().trim(),
|
||||||
|
body('titleAr').optional().isString().trim(),
|
||||||
|
],
|
||||||
|
validate,
|
||||||
|
adminController.updatePosition
|
||||||
|
);
|
||||||
|
|
||||||
// Delete (soft delete) a role/position
|
// Delete (soft delete) a role/position
|
||||||
router.delete(
|
router.delete(
|
||||||
'/positions/:id',
|
'/positions/:id',
|
||||||
|
|||||||
@@ -39,6 +39,19 @@ export interface AuditLogFilters {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreatePositionData {
|
||||||
|
title: string;
|
||||||
|
titleAr?: string;
|
||||||
|
departmentId: string;
|
||||||
|
level?: number;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePositionData {
|
||||||
|
title?: string;
|
||||||
|
titleAr?: string;
|
||||||
|
}
|
||||||
|
|
||||||
class AdminService {
|
class AdminService {
|
||||||
// ========== USERS ==========
|
// ========== USERS ==========
|
||||||
|
|
||||||
@@ -94,7 +107,7 @@ class AdminService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const sanitized = users.map((u) => {
|
const sanitized = users.map((u) => {
|
||||||
const { password: _, ...rest } = u;
|
const { password: _, ...rest } = u as any;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,7 +137,7 @@ class AdminService {
|
|||||||
throw new AppError(404, 'المستخدم غير موجود - User not found');
|
throw new AppError(404, 'المستخدم غير موجود - User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { password: _, ...rest } = user;
|
const { password: _, ...rest } = user as any;
|
||||||
return rest;
|
return rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +236,7 @@ class AdminService {
|
|||||||
...(data.email && { email: data.email }),
|
...(data.email && { email: data.email }),
|
||||||
...(data.username && { username: data.username }),
|
...(data.username && { username: data.username }),
|
||||||
...(data.isActive !== undefined && { isActive: data.isActive }),
|
...(data.isActive !== undefined && { isActive: data.isActive }),
|
||||||
...(data.employeeId !== undefined && { employeeId: data.employeeId || null }),
|
...(data.employeeId !== undefined && { employeeId: (data.employeeId as any) || null }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.password && data.password.length >= 8) {
|
if (data.password && data.password.length >= 8) {
|
||||||
@@ -242,7 +255,7 @@ class AdminService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { password: _, ...sanitized } = user;
|
const { password: _, ...sanitized } = user as any;
|
||||||
|
|
||||||
await AuditLogger.log({
|
await AuditLogger.log({
|
||||||
entityType: 'USER',
|
entityType: 'USER',
|
||||||
@@ -273,7 +286,7 @@ class AdminService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { password: _, ...sanitized } = updated;
|
const { password: _, ...sanitized } = updated as any;
|
||||||
|
|
||||||
await AuditLogger.log({
|
await AuditLogger.log({
|
||||||
entityType: 'USER',
|
entityType: 'USER',
|
||||||
@@ -380,7 +393,7 @@ class AdminService {
|
|||||||
const positions = await prisma.position.findMany({
|
const positions = await prisma.position.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
include: {
|
include: {
|
||||||
department: { select: { name: true, nameAr: true } },
|
department: { select: { id: true, name: true, nameAr: true } },
|
||||||
permissions: true,
|
permissions: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
@@ -406,6 +419,120 @@ class AdminService {
|
|||||||
return withUserCount;
|
return withUserCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateUniqueCode(base: string) {
|
||||||
|
const cleaned = (base || 'ROLE')
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/[^A-Z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.slice(0, 18) || 'ROLE';
|
||||||
|
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
const suffix = Math.floor(1000 + Math.random() * 9000);
|
||||||
|
const code = `${cleaned}_${suffix}`;
|
||||||
|
const exists = await prisma.position.findUnique({ where: { code } });
|
||||||
|
if (!exists) return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
return `${cleaned}_${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPosition(data: CreatePositionData, createdById: string) {
|
||||||
|
const title = (data.title || '').trim();
|
||||||
|
const titleAr = (data.titleAr || '').trim();
|
||||||
|
|
||||||
|
if (!title && !titleAr) {
|
||||||
|
throw new AppError(400, 'اسم الدور مطلوب - Role name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const department = await prisma.department.findUnique({
|
||||||
|
where: { id: data.departmentId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!department || !department.isActive) {
|
||||||
|
throw new AppError(400, 'القسم غير موجود - Department not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = (data.code || '').trim();
|
||||||
|
if (code) {
|
||||||
|
code = code.toUpperCase().replace(/[^A-Z0-9_]+/g, '_');
|
||||||
|
const exists = await prisma.position.findUnique({ where: { code } });
|
||||||
|
if (exists) {
|
||||||
|
throw new AppError(400, 'الكود مستخدم بالفعل - Code already exists');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
code = await this.generateUniqueCode(title || titleAr || 'ROLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = Number.isFinite(data.level as any) ? Math.max(1, Number(data.level)) : 1;
|
||||||
|
|
||||||
|
const created = await prisma.position.create({
|
||||||
|
data: {
|
||||||
|
title: title || titleAr,
|
||||||
|
titleAr: titleAr || null,
|
||||||
|
code,
|
||||||
|
departmentId: data.departmentId,
|
||||||
|
level,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await AuditLogger.log({
|
||||||
|
entityType: 'POSITION',
|
||||||
|
entityId: created.id,
|
||||||
|
action: 'CREATE',
|
||||||
|
userId: createdById,
|
||||||
|
changes: {
|
||||||
|
created: {
|
||||||
|
title: created.title,
|
||||||
|
titleAr: created.titleAr,
|
||||||
|
code: created.code,
|
||||||
|
departmentId: created.departmentId,
|
||||||
|
level: created.level,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const all = await this.getPositions();
|
||||||
|
return all.find((p: any) => p.id === created.id) || created;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePosition(positionId: string, data: UpdatePositionData, updatedById: string) {
|
||||||
|
const existing = await prisma.position.findUnique({ where: { id: positionId } });
|
||||||
|
if (!existing) {
|
||||||
|
throw new AppError(404, 'الدور غير موجود - Position not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTitle = data.title !== undefined ? (data.title || '').trim() : existing.title;
|
||||||
|
const nextTitleAr = data.titleAr !== undefined ? (data.titleAr || '').trim() : (existing.titleAr || '');
|
||||||
|
|
||||||
|
const finalTitle = nextTitle || nextTitleAr;
|
||||||
|
if (!finalTitle) {
|
||||||
|
throw new AppError(400, 'اسم الدور مطلوب - Role name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.position.update({
|
||||||
|
where: { id: positionId },
|
||||||
|
data: {
|
||||||
|
title: finalTitle,
|
||||||
|
titleAr: nextTitleAr ? nextTitleAr : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await AuditLogger.log({
|
||||||
|
entityType: 'POSITION',
|
||||||
|
entityId: positionId,
|
||||||
|
action: 'UPDATE',
|
||||||
|
userId: updatedById,
|
||||||
|
changes: {
|
||||||
|
before: { title: existing.title, titleAr: existing.titleAr },
|
||||||
|
after: { title: updated.title, titleAr: updated.titleAr },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const all = await this.getPositions();
|
||||||
|
return all.find((p: any) => p.id === positionId) || updated;
|
||||||
|
}
|
||||||
|
|
||||||
async updatePositionPermissions(positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }>) {
|
async updatePositionPermissions(positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }>) {
|
||||||
const position = await prisma.position.findUnique({ where: { id: positionId } });
|
const position = await prisma.position.findUnique({ where: { id: positionId } });
|
||||||
if (!position) {
|
if (!position) {
|
||||||
@@ -427,7 +554,7 @@ class AdminService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position);
|
return this.getPositions().then((pos: any) => pos.find((p: any) => p.id === positionId) || position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { Shield, Edit, Trash2, Users, Check, X, Loader2 } from 'lucide-react';
|
import { Shield, Edit, Trash2, Users, Check, X, Loader2, Plus } from 'lucide-react';
|
||||||
import { positionsAPI } from '@/lib/api/admin';
|
import { positionsAPI } from '@/lib/api/admin';
|
||||||
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
|
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -49,7 +49,9 @@ function buildMatrixFromPermissions(permissions: PositionPermission[]): Record<s
|
|||||||
for (const m of MODULES) {
|
for (const m of MODULES) {
|
||||||
matrix[m.id] = {};
|
matrix[m.id] = {};
|
||||||
const perm = permissions.find((p) => p.module === m.id && (p.resource === '*' || p.resource === m.id));
|
const perm = permissions.find((p) => p.module === m.id && (p.resource === '*' || p.resource === m.id));
|
||||||
const hasAll = perm && (Array.isArray(perm.actions)
|
const hasAll =
|
||||||
|
perm &&
|
||||||
|
(Array.isArray(perm.actions)
|
||||||
? (perm.actions as string[]).includes('*') || (perm.actions as string[]).includes('all')
|
? (perm.actions as string[]).includes('*') || (perm.actions as string[]).includes('all')
|
||||||
: false);
|
: false);
|
||||||
for (const a of ACTIONS) {
|
for (const a of ACTIONS) {
|
||||||
@@ -63,14 +65,30 @@ export default function RolesManagement() {
|
|||||||
const [roles, setRoles] = useState<PositionRole[]>([]);
|
const [roles, setRoles] = useState<PositionRole[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
|
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Edit modal (name + permissions)
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
|
const [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
|
||||||
|
const [editTitle, setEditTitle] = useState('');
|
||||||
|
const [editTitleAr, setEditTitleAr] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
// Delete dialog
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [roleToDelete, setRoleToDelete] = useState<PositionRole | null>(null);
|
const [roleToDelete, setRoleToDelete] = useState<PositionRole | null>(null);
|
||||||
|
|
||||||
|
// Create modal
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [newTitle, setNewTitle] = useState('');
|
||||||
|
const [newTitleAr, setNewTitleAr] = useState('');
|
||||||
|
const [newDepartmentId, setNewDepartmentId] = useState('');
|
||||||
|
const [newLevel, setNewLevel] = useState<number>(1);
|
||||||
|
const [newCode, setNewCode] = useState('');
|
||||||
|
|
||||||
const fetchRoles = useCallback(async () => {
|
const fetchRoles = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -93,11 +111,38 @@ export default function RolesManagement() {
|
|||||||
|
|
||||||
const currentRole = roles.find((r) => r.id === selectedRoleId);
|
const currentRole = roles.find((r) => r.id === selectedRoleId);
|
||||||
|
|
||||||
|
// build departments options from existing roles
|
||||||
|
const departmentOptions = useMemo(() => {
|
||||||
|
const map = new Map<string, { id: string; label: string }>();
|
||||||
|
roles.forEach((r) => {
|
||||||
|
if (!r.departmentId) return;
|
||||||
|
const label = r.department?.nameAr || r.department?.name || r.departmentId;
|
||||||
|
if (!map.has(r.departmentId)) map.set(r.departmentId, { id: r.departmentId, label });
|
||||||
|
});
|
||||||
|
return Array.from(map.values());
|
||||||
|
}, [roles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentRole) {
|
if (currentRole) {
|
||||||
setPermissionMatrix(buildMatrixFromPermissions(currentRole.permissions || []));
|
setPermissionMatrix(buildMatrixFromPermissions(currentRole.permissions || []));
|
||||||
|
setEditTitle(currentRole.title || '');
|
||||||
|
setEditTitleAr(currentRole.titleAr || '');
|
||||||
}
|
}
|
||||||
}, [currentRole?.id, currentRole?.permissions]);
|
}, [currentRole?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// default department when opening create
|
||||||
|
if (!showCreateModal) return;
|
||||||
|
const fallback =
|
||||||
|
(selectedRoleId && roles.find(r => r.id === selectedRoleId)?.departmentId) ||
|
||||||
|
departmentOptions[0]?.id ||
|
||||||
|
'';
|
||||||
|
setNewDepartmentId(fallback);
|
||||||
|
setNewLevel(1);
|
||||||
|
setNewCode('');
|
||||||
|
setNewTitle('');
|
||||||
|
setNewTitleAr('');
|
||||||
|
}, [showCreateModal, selectedRoleId, roles, departmentOptions]);
|
||||||
|
|
||||||
const handleTogglePermission = (moduleId: string, actionId: string) => {
|
const handleTogglePermission = (moduleId: string, actionId: string) => {
|
||||||
setPermissionMatrix((prev) => ({
|
setPermissionMatrix((prev) => ({
|
||||||
@@ -109,16 +154,38 @@ export default function RolesManagement() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSavePermissions = async () => {
|
const handleSaveRole = async () => {
|
||||||
if (!selectedRoleId) return;
|
if (!selectedRoleId) return;
|
||||||
|
|
||||||
|
const titleFinal = (editTitle || '').trim() || (editTitleAr || '').trim();
|
||||||
|
if (!titleFinal) {
|
||||||
|
alert('الرجاء إدخال اسم الدور');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
|
// 1) update name (only if changed)
|
||||||
|
if (currentRole && (titleFinal !== currentRole.title || (editTitleAr || '') !== (currentRole.titleAr || ''))) {
|
||||||
|
await positionsAPI.update(selectedRoleId, {
|
||||||
|
title: titleFinal,
|
||||||
|
titleAr: (editTitleAr || '').trim() || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) update permissions
|
||||||
const permissions = buildPermissionsFromMatrix(permissionMatrix);
|
const permissions = buildPermissionsFromMatrix(permissionMatrix);
|
||||||
await positionsAPI.updatePermissions(selectedRoleId, permissions);
|
await positionsAPI.updatePermissions(selectedRoleId, permissions);
|
||||||
|
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
fetchRoles();
|
await fetchRoles();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
alert(err instanceof Error ? err.message : 'فشل حفظ الصلاحيات');
|
const msg =
|
||||||
|
(err as any)?.response?.data?.message ||
|
||||||
|
(err as any)?.response?.data?.error ||
|
||||||
|
(err as any)?.message ||
|
||||||
|
'فشل حفظ التغييرات';
|
||||||
|
alert(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -154,6 +221,45 @@ export default function RolesManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateRole = async () => {
|
||||||
|
const titleFinal = (newTitle || '').trim() || (newTitleAr || '').trim();
|
||||||
|
if (!titleFinal) {
|
||||||
|
alert('الرجاء إدخال اسم الدور');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newDepartmentId) {
|
||||||
|
alert('الرجاء اختيار قسم (Department)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
const created = await positionsAPI.create({
|
||||||
|
title: titleFinal,
|
||||||
|
titleAr: (newTitleAr || '').trim() || null,
|
||||||
|
departmentId: newDepartmentId,
|
||||||
|
level: Number.isFinite(newLevel) ? newLevel : 1,
|
||||||
|
code: (newCode || '').trim() || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowCreateModal(false);
|
||||||
|
await fetchRoles();
|
||||||
|
|
||||||
|
// select new role + open edit modal directly
|
||||||
|
setSelectedRoleId(created.id);
|
||||||
|
setShowEditModal(true);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg =
|
||||||
|
(err as any)?.response?.data?.message ||
|
||||||
|
(err as any)?.response?.data?.error ||
|
||||||
|
(err as any)?.message ||
|
||||||
|
'فشل إنشاء الدور';
|
||||||
|
alert(msg);
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectRole = (id: string) => {
|
const handleSelectRole = (id: string) => {
|
||||||
setSelectedRoleId(id);
|
setSelectedRoleId(id);
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
@@ -166,6 +272,15 @@ export default function RolesManagement() {
|
|||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">الأدوار والصلاحيات</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">الأدوار والصلاحيات</h1>
|
||||||
<p className="text-gray-600">إدارة أدوار المستخدمين ومصفوفة الصلاحيات</p>
|
<p className="text-gray-600">إدارة أدوار المستخدمين ومصفوفة الصلاحيات</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* رجعنا زر الإنشاء */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -192,12 +307,8 @@ export default function RolesManagement() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div className={`p-2 rounded-lg ${selectedRoleId === role.id ? 'bg-purple-600' : 'bg-purple-100'}`}>
|
||||||
className={`p-2 rounded-lg ${selectedRoleId === role.id ? 'bg-purple-600' : 'bg-purple-100'}`}
|
<Shield className={`h-5 w-5 ${selectedRoleId === role.id ? 'text-white' : 'text-purple-600'}`} />
|
||||||
>
|
|
||||||
<Shield
|
|
||||||
className={`h-5 w-5 ${selectedRoleId === role.id ? 'text-white' : 'text-purple-600'}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-gray-900">{role.titleAr || role.title}</h3>
|
<h3 className="font-bold text-gray-900">{role.titleAr || role.title}</h3>
|
||||||
@@ -205,14 +316,25 @@ export default function RolesManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
<span>{role.usersCount ?? role._count?.employees ?? 0} مستخدم</span>
|
<span>{role.usersCount ?? role._count?.employees ?? 0} مستخدم</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions: Edit + Delete */}
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openDeleteDialog(role);
|
||||||
|
}}
|
||||||
|
className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||||
|
title="حذف"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -224,17 +346,6 @@ export default function RolesManagement() {
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
openDeleteDialog(role);
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors"
|
|
||||||
title="حذف"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,10 +366,11 @@ export default function RolesManagement() {
|
|||||||
onClick={() => setShowEditModal(true)}
|
onClick={() => setShowEditModal(true)}
|
||||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
|
||||||
>
|
>
|
||||||
تعديل الصلاحيات
|
تعديل الاسم والصلاحيات
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4">مصفوفة الصلاحيات</h3>
|
<h3 className="text-lg font-bold text-gray-900 mb-4">مصفوفة الصلاحيات</h3>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -275,6 +387,7 @@ export default function RolesManagement() {
|
|||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{MODULES.map((module) => (
|
{MODULES.map((module) => (
|
||||||
<tr key={module.id} className="hover:bg-gray-50 transition-colors">
|
<tr key={module.id} className="hover:bg-gray-50 transition-colors">
|
||||||
@@ -284,15 +397,14 @@ export default function RolesManagement() {
|
|||||||
<p className="text-xs text-gray-600">{module.nameEn}</p>
|
<p className="text-xs text-gray-600">{module.nameEn}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{ACTIONS.map((action) => {
|
{ACTIONS.map((action) => {
|
||||||
const hasPermission = permissionMatrix[module.id]?.[action.id];
|
const hasPermission = permissionMatrix[module.id]?.[action.id];
|
||||||
return (
|
return (
|
||||||
<td key={action.id} className="px-4 py-4 text-center">
|
<td key={action.id} className="px-4 py-4 text-center">
|
||||||
<div
|
<div
|
||||||
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all mx-auto ${
|
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all mx-auto ${
|
||||||
hasPermission
|
hasPermission ? 'bg-green-500 text-white shadow-md' : 'bg-gray-200 text-gray-500'
|
||||||
? 'bg-green-500 text-white shadow-md'
|
|
||||||
: 'bg-gray-200 text-gray-500'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{hasPermission ? <Check className="h-6 w-6" /> : <X className="h-6 w-6" />}
|
{hasPermission ? <Check className="h-6 w-6" /> : <X className="h-6 w-6" />}
|
||||||
@@ -318,6 +430,103 @@ export default function RolesManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Create Role Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showCreateModal}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
title="إضافة دور جديد"
|
||||||
|
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={newTitle}
|
||||||
|
onChange={(e) => setNewTitle(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
placeholder="e.g. Sales representative"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">الاسم (عربي)</label>
|
||||||
|
<input
|
||||||
|
value={newTitleAr}
|
||||||
|
onChange={(e) => setNewTitleAr(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
placeholder="مثال: مندوب مبيعات"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">القسم (Department)</label>
|
||||||
|
<select
|
||||||
|
value={newDepartmentId}
|
||||||
|
onChange={(e) => setNewDepartmentId(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 bg-white"
|
||||||
|
>
|
||||||
|
{departmentOptions.length === 0 ? (
|
||||||
|
<option value="">لا يوجد أقسام متاحة</option>
|
||||||
|
) : (
|
||||||
|
departmentOptions.map((d) => (
|
||||||
|
<option key={d.id} value={d.id}>
|
||||||
|
{d.label}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{departmentOptions.length === 0 && (
|
||||||
|
<p className="text-xs text-red-600 mt-1">
|
||||||
|
لا يوجد أقسام ضمن البيانات الحالية. (DepartmentId مطلوب لإنشاء الدور)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Level</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={newLevel}
|
||||||
|
onChange={(e) => setNewLevel(parseInt(e.target.value || '1', 10))}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
min={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">Code (اختياري)</label>
|
||||||
|
<input
|
||||||
|
value={newCode}
|
||||||
|
onChange={(e) => setNewCode(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
placeholder="e.g. SALES_REP (إذا تركته فاضي النظام يولّد تلقائياً)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="px-5 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||||
|
disabled={creating}
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCreateRole}
|
||||||
|
className="px-5 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold disabled:opacity-50 inline-flex items-center gap-2"
|
||||||
|
disabled={creating}
|
||||||
|
>
|
||||||
|
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||||
|
{creating ? 'جاري الإنشاء...' : 'إنشاء'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
{showDeleteDialog && roleToDelete && (
|
{showDeleteDialog && roleToDelete && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
@@ -376,15 +585,37 @@ export default function RolesManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Permissions Modal */}
|
{/* Edit Modal: name + permissions */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showEditModal}
|
isOpen={showEditModal}
|
||||||
onClose={() => setShowEditModal(false)}
|
onClose={() => setShowEditModal(false)}
|
||||||
title={`تعديل صلاحيات: ${currentRole?.titleAr || currentRole?.title || ''}`}
|
title={`تعديل: ${currentRole?.titleAr || currentRole?.title || ''}`}
|
||||||
size="2xl"
|
size="2xl"
|
||||||
>
|
>
|
||||||
{currentRole && (
|
{currentRole && (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Name edit */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">الاسم (English)</label>
|
||||||
|
<input
|
||||||
|
value={editTitle}
|
||||||
|
onChange={(e) => setEditTitle(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-1">الاسم (عربي)</label>
|
||||||
|
<input
|
||||||
|
value={editTitleAr}
|
||||||
|
onChange={(e) => setEditTitleAr(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Permissions */}
|
||||||
<div className="overflow-x-auto mb-6">
|
<div className="overflow-x-auto mb-6">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -397,12 +628,14 @@ export default function RolesManagement() {
|
|||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{MODULES.map((module) => (
|
{MODULES.map((module) => (
|
||||||
<tr key={module.id}>
|
<tr key={module.id}>
|
||||||
<td className="px-4 py-4">
|
<td className="px-4 py-4">
|
||||||
<p className="font-semibold text-gray-900">{module.name}</p>
|
<p className="font-semibold text-gray-900">{module.name}</p>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{ACTIONS.map((action) => {
|
{ACTIONS.map((action) => {
|
||||||
const hasPermission = permissionMatrix[module.id]?.[action.id];
|
const hasPermission = permissionMatrix[module.id]?.[action.id];
|
||||||
return (
|
return (
|
||||||
@@ -424,18 +657,21 @@ export default function RolesManagement() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowEditModal(false)}
|
onClick={() => setShowEditModal(false)}
|
||||||
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium"
|
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium"
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
إلغاء
|
إلغاء
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSavePermissions}
|
onClick={handleSaveRole}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold disabled:opacity-50"
|
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold disabled:opacity-50 inline-flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||||
{saving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
|
{saving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
4
frontend/src/app/icon.svg
Normal file
4
frontend/src/app/icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
|
<rect width="128" height="128" rx="24" fill="#2563EB"/>
|
||||||
|
<text x="64" y="82" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="72" font-weight="800" fill="#FFFFFF">Z</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
@@ -1,69 +1,120 @@
|
|||||||
import type { Metadata } from 'next'
|
'use client'
|
||||||
import { Cairo, Readex_Pro } from 'next/font/google'
|
|
||||||
import './globals.css'
|
|
||||||
import { Providers } from './providers'
|
|
||||||
import { AuthProvider } from '@/contexts/AuthContext'
|
|
||||||
import { LanguageProvider } from '@/contexts/LanguageContext'
|
|
||||||
import { Toaster } from 'react-hot-toast'
|
|
||||||
|
|
||||||
const cairo = Cairo({
|
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||||
subsets: ['latin', 'arabic'],
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
variable: '--font-cairo',
|
import Link from 'next/link'
|
||||||
display: 'swap',
|
import { usePathname } from 'next/navigation'
|
||||||
})
|
import {
|
||||||
|
Users,
|
||||||
|
Shield,
|
||||||
|
Database,
|
||||||
|
Settings,
|
||||||
|
FileText,
|
||||||
|
Activity,
|
||||||
|
Mail,
|
||||||
|
Key,
|
||||||
|
Clock,
|
||||||
|
Building2,
|
||||||
|
LogOut,
|
||||||
|
LayoutDashboard,
|
||||||
|
Users2
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
const readexPro = Readex_Pro({
|
function AdminLayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
subsets: ['latin', 'arabic'],
|
const { user, logout } = useAuth()
|
||||||
variable: '--font-readex',
|
const pathname = usePathname()
|
||||||
display: 'swap',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
const menuItems = [
|
||||||
title: 'Z.CRM - نظام إدارة علاقات العملاء',
|
{ icon: LayoutDashboard, label: 'لوحة التحكم', href: '/admin', exact: true },
|
||||||
description: 'Enterprise CRM System for Contact Management, Sales, HR, Inventory, Projects, and Marketing',
|
{ icon: Users, label: 'إدارة المستخدمين', href: '/admin/users' },
|
||||||
|
{ icon: Shield, label: 'الأدوار والصلاحيات', href: '/admin/roles' },
|
||||||
|
{ icon: Users2, label: 'مجموعات الصلاحيات', href: '/admin/permission-groups' },
|
||||||
|
{ icon: Database, label: 'النسخ الاحتياطي', href: '/admin/backup' },
|
||||||
|
{ icon: Settings, label: 'إعدادات النظام', href: '/admin/settings' },
|
||||||
|
{ icon: FileText, label: 'سجل العمليات', href: '/admin/audit-logs' },
|
||||||
|
{ icon: Activity, label: 'صحة النظام', href: '/admin/health' },
|
||||||
|
{ icon: Mail, label: 'إعدادات البريد', href: '/admin/email' },
|
||||||
|
{ icon: Key, label: 'مفاتيح API', href: '/admin/api-keys' },
|
||||||
|
{ icon: Clock, label: 'المهام المجدولة', href: '/admin/scheduled-jobs' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const isActive = (href: string, exact?: boolean) => {
|
||||||
|
if (exact) {
|
||||||
|
return pathname === href
|
||||||
|
}
|
||||||
|
return pathname.startsWith(href)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
<body className={`${readexPro.variable} ${cairo.variable} font-readex`}>
|
<aside className="w-64 bg-white border-l shadow-lg fixed h-full overflow-y-auto">
|
||||||
<LanguageProvider>
|
<div className="p-6 border-b">
|
||||||
<AuthProvider>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Providers>{children}</Providers>
|
<div className="bg-red-600 p-2 rounded-lg">
|
||||||
<Toaster
|
<Shield className="h-6 w-6 text-white" />
|
||||||
position="top-center"
|
</div>
|
||||||
reverseOrder={false}
|
<div>
|
||||||
toastOptions={{
|
<h2 className="text-lg font-bold text-gray-900">لوحة الإدارة</h2>
|
||||||
duration: 4000,
|
<p className="text-xs text-gray-600">System Admin</p>
|
||||||
style: {
|
</div>
|
||||||
background: '#fff',
|
</div>
|
||||||
color: '#363636',
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||||
fontFamily: 'var(--font-readex)',
|
<p className="text-xs font-semibold text-red-900">{user?.username}</p>
|
||||||
},
|
<p className="text-xs text-red-700">{user?.role?.name}</p>
|
||||||
success: {
|
</div>
|
||||||
duration: 3000,
|
</div>
|
||||||
iconTheme: {
|
|
||||||
primary: '#10B981',
|
<nav className="p-4">
|
||||||
secondary: '#fff',
|
{menuItems.map((item) => {
|
||||||
},
|
const Icon = item.icon
|
||||||
},
|
const active = isActive(item.href, item.exact)
|
||||||
error: {
|
return (
|
||||||
duration: 5000,
|
<Link
|
||||||
iconTheme: {
|
key={item.href}
|
||||||
primary: '#EF4444',
|
href={item.href}
|
||||||
secondary: '#fff',
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg mb-2 transition-all ${
|
||||||
},
|
active
|
||||||
},
|
? 'bg-red-600 text-white shadow-md'
|
||||||
}}
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
/>
|
}`}
|
||||||
</AuthProvider>
|
>
|
||||||
</LanguageProvider>
|
<Icon className="h-5 w-5" />
|
||||||
</body>
|
<span className="font-medium">{item.label}</span>
|
||||||
</html>
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<hr className="my-4 border-gray-200" />
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-lg mb-2 text-gray-700 hover:bg-gray-100 transition-all"
|
||||||
|
>
|
||||||
|
<Building2 className="h-5 w-5" />
|
||||||
|
<span className="font-medium">العودة للنظام</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-red-600 hover:bg-red-50 transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="h-5 w-5" />
|
||||||
|
<span className="font-medium">تسجيل الخروج</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="mr-64 flex-1 p-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AdminLayoutContent>{children}</AdminLayoutContent>
|
||||||
|
</ProtectedRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ export const statsAPI = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Positions (Roles) API - maps to HR positions with permissions
|
// Positions (Roles) API
|
||||||
export interface PositionPermission {
|
export interface PositionPermission {
|
||||||
id: string;
|
id: string;
|
||||||
module: string;
|
module: string;
|
||||||
@@ -126,18 +126,43 @@ export interface PositionRole {
|
|||||||
title: string;
|
title: string;
|
||||||
titleAr?: string | null;
|
titleAr?: string | null;
|
||||||
code: string;
|
code: string;
|
||||||
department?: { name: string; nameAr?: string | null };
|
level: number;
|
||||||
|
departmentId: string;
|
||||||
|
department?: { id?: string; name: string; nameAr?: string | null };
|
||||||
permissions: PositionPermission[];
|
permissions: PositionPermission[];
|
||||||
usersCount: number;
|
usersCount: number;
|
||||||
_count?: { employees: number };
|
_count?: { employees: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreatePositionPayload {
|
||||||
|
title: string;
|
||||||
|
titleAr?: string | null;
|
||||||
|
departmentId: string;
|
||||||
|
level?: number;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePositionPayload {
|
||||||
|
title?: string;
|
||||||
|
titleAr?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const positionsAPI = {
|
export const positionsAPI = {
|
||||||
getAll: async (): Promise<PositionRole[]> => {
|
getAll: async (): Promise<PositionRole[]> => {
|
||||||
const response = await api.get('/admin/positions');
|
const response = await api.get('/admin/positions');
|
||||||
return response.data.data || [];
|
return response.data.data || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create: async (payload: CreatePositionPayload): Promise<PositionRole> => {
|
||||||
|
const response = await api.post('/admin/positions', payload);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (positionId: string, payload: UpdatePositionPayload): Promise<PositionRole> => {
|
||||||
|
const response = await api.put(`/admin/positions/${positionId}`, payload);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
delete: async (positionId: string): Promise<void> => {
|
delete: async (positionId: string): Promise<void> => {
|
||||||
await api.delete(`/admin/positions/${positionId}`);
|
await api.delete(`/admin/positions/${positionId}`);
|
||||||
},
|
},
|
||||||
@@ -153,7 +178,7 @@ export const positionsAPI = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Roles API - alias for positions (for compatibility with existing frontend)
|
// Roles API - alias for positions
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -235,7 +260,7 @@ export const auditLogsAPI = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// System Settings API (placeholder - out of scope)
|
// System Settings API (placeholder)
|
||||||
export interface SystemSetting {
|
export interface SystemSetting {
|
||||||
key: string;
|
key: string;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
@@ -254,7 +279,7 @@ export const settingsAPI = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// System Health API (placeholder - optional)
|
// System Health API (placeholder)
|
||||||
export interface SystemHealth {
|
export interface SystemHealth {
|
||||||
status: string;
|
status: string;
|
||||||
database: string;
|
database: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user