Admin panel: real data - backend API, users/audit/roles/stats, frontend wired

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Talal Sharabi
2026-02-19 15:40:34 +04:00
parent 680ba3871e
commit 842678674b
11 changed files with 2159 additions and 809 deletions

View File

@@ -88,6 +88,16 @@ async function main() {
});
}
// Admin permission for GM
await prisma.positionPermission.create({
data: {
positionId: gmPosition.id,
module: 'admin',
resource: '*',
actions: ['*'],
},
});
// Create Permissions for Sales Manager
await prisma.positionPermission.createMany({
data: [

View File

@@ -0,0 +1,41 @@
/**
* Add admin permission for GM position.
* Run this for existing databases where seed was run before admin module existed:
* npx ts-node scripts/add-admin-permission.ts
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const gmPosition = await prisma.position.findFirst({ where: { code: 'GM' } });
if (!gmPosition) {
console.log('GM position not found. Run full seed first.');
process.exit(1);
}
const existing = await prisma.positionPermission.findFirst({
where: { positionId: gmPosition.id, module: 'admin' },
});
if (existing) {
console.log('Admin permission already exists for GM.');
process.exit(0);
}
await prisma.positionPermission.create({
data: {
positionId: gmPosition.id,
module: 'admin',
resource: '*',
actions: ['*'],
},
});
console.log('Admin permission added for GM position.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,150 @@
import { Response, NextFunction } from 'express';
import { adminService } from './admin.service';
import { AuthRequest } from '../../shared/middleware/auth';
import { ResponseFormatter } from '../../shared/utils/responseFormatter';
class AdminController {
// ========== USERS ==========
async findAllUsers(req: AuthRequest, res: Response, next: NextFunction) {
try {
const filters = {
search: req.query.search as string | undefined,
isActive: req.query.status === 'active' ? true : req.query.status === 'inactive' ? false : undefined,
positionId: req.query.positionId as string | undefined,
page: parseInt(req.query.page as string) || 1,
pageSize: parseInt(req.query.pageSize as string) || 20,
};
const result = await adminService.findAllUsers(filters);
res.json(ResponseFormatter.paginated(result.users, result.total, result.page, result.pageSize));
} catch (error) {
next(error);
}
}
async findUserById(req: AuthRequest, res: Response, next: NextFunction) {
try {
const user = await adminService.findUserById(req.params.id);
res.json(ResponseFormatter.success(user));
} catch (error) {
next(error);
}
}
async createUser(req: AuthRequest, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
const user = await adminService.createUser(
{
email: req.body.email,
username: req.body.username,
password: req.body.password,
employeeId: req.body.employeeId,
isActive: req.body.isActive ?? true,
},
userId
);
res.status(201).json(ResponseFormatter.success(user));
} catch (error) {
next(error);
}
}
async updateUser(req: AuthRequest, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
const user = await adminService.updateUser(
req.params.id,
{
email: req.body.email,
username: req.body.username,
password: req.body.password,
employeeId: req.body.employeeId,
isActive: req.body.isActive,
},
userId
);
res.json(ResponseFormatter.success(user));
} catch (error) {
next(error);
}
}
async toggleUserActive(req: AuthRequest, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
const user = await adminService.toggleUserActive(req.params.id, userId);
res.json(ResponseFormatter.success(user));
} catch (error) {
next(error);
}
}
async deleteUser(req: AuthRequest, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
await adminService.deleteUser(req.params.id, userId);
res.json(ResponseFormatter.success(null, 'User deactivated successfully'));
} catch (error) {
next(error);
}
}
// ========== AUDIT LOGS ==========
async getAuditLogs(req: AuthRequest, res: Response, next: NextFunction) {
try {
const filters = {
entityType: req.query.entityType as string | undefined,
action: req.query.action as string | undefined,
userId: req.query.userId as string | undefined,
startDate: req.query.startDate as string | undefined,
endDate: req.query.endDate as string | undefined,
page: parseInt(req.query.page as string) || 1,
pageSize: parseInt(req.query.pageSize as string) || 20,
};
const result = await adminService.getAuditLogs(filters);
res.json(
ResponseFormatter.paginated(result.logs, result.total, result.page, result.pageSize)
);
} catch (error) {
next(error);
}
}
// ========== STATS ==========
async getStats(req: AuthRequest, res: Response, next: NextFunction) {
try {
const stats = await adminService.getStats();
res.json(ResponseFormatter.success(stats));
} catch (error) {
next(error);
}
}
// ========== POSITIONS (ROLES) ==========
async getPositions(req: AuthRequest, res: Response, next: NextFunction) {
try {
const positions = await adminService.getPositions();
res.json(ResponseFormatter.success(positions));
} catch (error) {
next(error);
}
}
async updatePositionPermissions(req: AuthRequest, res: Response, next: NextFunction) {
try {
const position = await adminService.updatePositionPermissions(
req.params.id,
req.body.permissions
);
res.json(ResponseFormatter.success(position));
} catch (error) {
next(error);
}
}
}
export const adminController = new AdminController();

View File

@@ -0,0 +1,103 @@
import { Router } from 'express';
import { body, param } from 'express-validator';
import { adminController } from './admin.controller';
import { authenticate, authorize } from '../../shared/middleware/auth';
import { validate } from '../../shared/middleware/validation';
const router = Router();
router.use(authenticate);
// ========== USERS ==========
router.get(
'/users',
authorize('admin', 'users', 'read'),
adminController.findAllUsers
);
router.get(
'/users/:id',
authorize('admin', 'users', 'read'),
param('id').isUUID(),
validate,
adminController.findUserById
);
router.post(
'/users',
authorize('admin', 'users', 'create'),
[
body('email').isEmail(),
body('username').notEmpty().trim(),
body('password').isLength({ min: 8 }),
body('employeeId').isUUID(),
],
validate,
adminController.createUser
);
router.put(
'/users/:id',
authorize('admin', 'users', 'update'),
[
param('id').isUUID(),
body('email').optional().isEmail(),
body('username').optional().notEmpty().trim(),
body('password').optional().isLength({ min: 8 }),
],
validate,
adminController.updateUser
);
router.patch(
'/users/:id/toggle-active',
authorize('admin', 'users', 'update'),
param('id').isUUID(),
validate,
adminController.toggleUserActive
);
router.delete(
'/users/:id',
authorize('admin', 'users', 'delete'),
param('id').isUUID(),
validate,
adminController.deleteUser
);
// ========== AUDIT LOGS ==========
router.get(
'/audit-logs',
authorize('admin', 'audit-logs', 'read'),
adminController.getAuditLogs
);
// ========== STATS ==========
router.get(
'/stats',
authorize('admin', 'stats', 'read'),
adminController.getStats
);
// ========== POSITIONS (ROLES) ==========
router.get(
'/positions',
authorize('admin', 'roles', 'read'),
adminController.getPositions
);
router.put(
'/positions/:id/permissions',
authorize('admin', 'roles', 'update'),
[
param('id').isUUID(),
body('permissions').isArray(),
],
validate,
adminController.updatePositionPermissions
);
export default router;

View File

@@ -0,0 +1,434 @@
import bcrypt from 'bcryptjs';
import prisma from '../../config/database';
import { AppError } from '../../shared/middleware/errorHandler';
import { AuditLogger } from '../../shared/utils/auditLogger';
import { config } from '../../config';
import { Prisma } from '@prisma/client';
export interface UserFilters {
search?: string;
isActive?: boolean;
positionId?: string;
page?: number;
pageSize?: number;
}
export interface CreateUserData {
email: string;
username: string;
password: string;
employeeId: string;
isActive?: boolean;
}
export interface UpdateUserData {
email?: string;
username?: string;
password?: string;
employeeId?: string;
isActive?: boolean;
}
export interface AuditLogFilters {
entityType?: string;
action?: string;
userId?: string;
startDate?: string;
endDate?: string;
page?: number;
pageSize?: number;
}
class AdminService {
// ========== USERS ==========
async findAllUsers(filters: UserFilters) {
const page = filters.page || 1;
const pageSize = Math.min(filters.pageSize || 20, 100);
const skip = (page - 1) * pageSize;
const where: Prisma.UserWhereInput = {};
if (filters.isActive !== undefined) {
where.isActive = filters.isActive;
}
if (filters.positionId) {
where.employee = {
positionId: filters.positionId,
};
}
if (filters.search?.trim()) {
const search = filters.search.trim().toLowerCase();
where.OR = [
{ username: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
{
employee: {
OR: [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
],
},
},
];
}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: pageSize,
include: {
employee: {
include: {
position: { select: { id: true, title: true, titleAr: true } },
department: { select: { name: true, nameAr: true } },
},
},
},
orderBy: { createdAt: 'desc' },
}),
prisma.user.count({ where }),
]);
const sanitized = users.map((u) => {
const { password: _, ...rest } = u;
return rest;
});
return {
users: sanitized,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
async findUserById(id: string) {
const user = await prisma.user.findUnique({
where: { id },
include: {
employee: {
include: {
position: { include: { permissions: true } },
department: true,
},
},
},
});
if (!user) {
throw new AppError(404, 'المستخدم غير موجود - User not found');
}
const { password: _, ...rest } = user;
return rest;
}
async createUser(data: CreateUserData, createdById: string) {
const employee = await prisma.employee.findUnique({
where: { id: data.employeeId },
});
if (!employee) {
throw new AppError(400, 'الموظف غير موجود - Employee not found');
}
if (employee.status !== 'ACTIVE') {
throw new AppError(400, 'الموظف غير نشط - Employee must be ACTIVE');
}
const existingUser = await prisma.user.findFirst({
where: { employeeId: data.employeeId },
});
if (existingUser) {
throw new AppError(400, 'هذا الموظف مرتبط بحساب مستخدم بالفعل - Employee already has a user account');
}
const emailExists = await prisma.user.findUnique({
where: { email: data.email },
});
if (emailExists) {
throw new AppError(400, 'البريد الإلكتروني مستخدم بالفعل - Email already in use');
}
const usernameExists = await prisma.user.findUnique({
where: { username: data.username },
});
if (usernameExists) {
throw new AppError(400, 'اسم المستخدم مستخدم بالفعل - Username already in use');
}
const hashedPassword = await bcrypt.hash(data.password, config.security?.bcryptRounds || 10);
const user = await prisma.user.create({
data: {
email: data.email,
username: data.username,
password: hashedPassword,
employeeId: data.employeeId,
isActive: data.isActive ?? true,
},
select: {
id: true,
email: true,
username: true,
employeeId: true,
isActive: true,
lastLogin: true,
createdAt: true,
employee: {
include: {
position: { select: { title: true, titleAr: true } },
},
},
},
});
await AuditLogger.log({
entityType: 'USER',
entityId: user.id,
action: 'CREATE',
userId: createdById,
changes: { created: { email: user.email, username: user.username } },
});
return user;
}
async updateUser(id: string, data: UpdateUserData, updatedById: string) {
const existing = await prisma.user.findUnique({ where: { id } });
if (!existing) {
throw new AppError(404, 'المستخدم غير موجود - User not found');
}
if (data.email && data.email !== existing.email) {
const emailExists = await prisma.user.findUnique({ where: { email: data.email } });
if (emailExists) {
throw new AppError(400, 'البريد الإلكتروني مستخدم بالفعل - Email already in use');
}
}
if (data.username && data.username !== existing.username) {
const usernameExists = await prisma.user.findUnique({ where: { username: data.username } });
if (usernameExists) {
throw new AppError(400, 'اسم المستخدم مستخدم بالفعل - Username already in use');
}
}
const updateData: Prisma.UserUpdateInput = {
...(data.email && { email: data.email }),
...(data.username && { username: data.username }),
...(data.isActive !== undefined && { isActive: data.isActive }),
...(data.employeeId !== undefined && { employeeId: data.employeeId || null }),
};
if (data.password && data.password.length >= 8) {
updateData.password = await bcrypt.hash(data.password, config.security?.bcryptRounds || 10);
}
const user = await prisma.user.update({
where: { id },
data: updateData,
include: {
employee: {
include: {
position: { select: { title: true, titleAr: true } },
},
},
},
});
const { password: _, ...sanitized } = user;
await AuditLogger.log({
entityType: 'USER',
entityId: id,
action: 'UPDATE',
userId: updatedById,
changes: { before: existing, after: sanitized },
});
return sanitized;
}
async toggleUserActive(id: string, userId: string) {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) {
throw new AppError(404, 'المستخدم غير موجود - User not found');
}
const updated = await prisma.user.update({
where: { id },
data: { isActive: !user.isActive },
include: {
employee: {
include: {
position: { select: { title: true, titleAr: true } },
},
},
},
});
const { password: _, ...sanitized } = updated;
await AuditLogger.log({
entityType: 'USER',
entityId: id,
action: user.isActive ? 'DEACTIVATE' : 'ACTIVATE',
userId,
changes: { isActive: updated.isActive },
});
return sanitized;
}
async deleteUser(id: string, deletedById: string) {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) {
throw new AppError(404, 'المستخدم غير موجود - User not found');
}
await prisma.user.update({
where: { id },
data: { isActive: false },
});
await AuditLogger.log({
entityType: 'USER',
entityId: id,
action: 'DELETE',
userId: deletedById,
changes: { softDeleted: true, email: user.email },
});
return { success: true, message: 'User deactivated successfully' };
}
// ========== AUDIT LOGS ==========
async getAuditLogs(filters: AuditLogFilters) {
const page = filters.page || 1;
const pageSize = Math.min(filters.pageSize || 20, 100);
const skip = (page - 1) * pageSize;
const where: Prisma.AuditLogWhereInput = {};
if (filters.entityType) where.entityType = filters.entityType;
if (filters.action) where.action = filters.action;
if (filters.userId) where.userId = filters.userId;
if (filters.startDate || filters.endDate) {
where.createdAt = {};
if (filters.startDate) where.createdAt.gte = new Date(filters.startDate);
if (filters.endDate) where.createdAt.lte = new Date(filters.endDate);
}
const [logs, total] = await Promise.all([
prisma.auditLog.findMany({
where,
skip,
take: pageSize,
include: {
user: {
select: { id: true, username: true, email: true },
},
},
orderBy: { createdAt: 'desc' },
}),
prisma.auditLog.count({ where }),
]);
return {
logs,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
// ========== STATS ==========
async getStats() {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [totalUsers, activeUsers, inactiveUsers, loginsToday] = await Promise.all([
prisma.user.count(),
prisma.user.count({ where: { isActive: true } }),
prisma.user.count({ where: { isActive: false } }),
prisma.user.count({
where: { lastLogin: { gte: today } },
}),
]);
return {
totalUsers,
activeUsers,
inactiveUsers,
loginsToday,
};
}
// ========== POSITIONS (ROLES) ==========
async getPositions() {
const positions = await prisma.position.findMany({
where: { isActive: true },
include: {
department: { select: { name: true, nameAr: true } },
permissions: true,
_count: {
select: {
employees: true,
},
},
},
orderBy: { level: 'asc' },
});
const withUserCount = await Promise.all(
positions.map(async (p) => {
const usersCount = await prisma.user.count({
where: {
employee: { positionId: p.id },
isActive: true,
},
});
return { ...p, usersCount };
})
);
return withUserCount;
}
async updatePositionPermissions(positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }>) {
const position = await prisma.position.findUnique({ where: { id: positionId } });
if (!position) {
throw new AppError(404, 'الدور غير موجود - Position not found');
}
await prisma.positionPermission.deleteMany({
where: { positionId },
});
if (permissions.length > 0) {
await prisma.positionPermission.createMany({
data: permissions.map((p) => ({
positionId,
module: p.module,
resource: p.resource,
actions: p.actions,
})),
});
}
return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position);
}
}
export const adminService = new AdminService();

View File

@@ -1,4 +1,5 @@
import { Router } from 'express';
import adminRoutes from '../modules/admin/admin.routes';
import authRoutes from '../modules/auth/auth.routes';
import contactsRoutes from '../modules/contacts/contacts.routes';
import crmRoutes from '../modules/crm/crm.routes';
@@ -10,6 +11,7 @@ import marketingRoutes from '../modules/marketing/marketing.routes';
const router = Router();
// Module routes
router.use('/admin', adminRoutes);
router.use('/auth', authRoutes);
router.use('/contacts', contactsRoutes);
router.use('/crm', crmRoutes);

View File

@@ -1,65 +1,79 @@
'use client'
'use client';
import { FileText, Filter, Download, User, Clock, Activity } from 'lucide-react'
import { useState, useEffect, useCallback } from 'react';
import { FileText, Filter, Download, User, Clock, Activity } from 'lucide-react';
import { auditLogsAPI } from '@/lib/api/admin';
import type { AuditLog } from '@/lib/api/admin';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function AuditLogs() {
const logs = [
{
id: '1',
user: 'أحمد محمد',
action: 'قام بإنشاء مستخدم جديد',
module: 'إدارة المستخدمين',
details: 'إنشاء مستخدم: mohammed.ali@example.com',
ip: '192.168.1.100',
timestamp: '2024-01-06 14:30:15',
level: 'info'
},
{
id: '2',
user: 'فاطمة الزهراني',
action: 'قامت بتعديل صلاحيات دور',
module: 'الأدوار والصلاحيات',
details: 'تعديل صلاحيات دور "مدير المبيعات"',
ip: '192.168.1.101',
timestamp: '2024-01-06 13:45:30',
level: 'warning'
},
{
id: '3',
user: 'النظام',
action: 'تم إنشاء نسخة احتياطية تلقائية',
module: 'النسخ الاحتياطي',
details: 'نسخة احتياطية تلقائية - 45.2 MB',
ip: 'system',
timestamp: '2024-01-06 02:00:00',
level: 'success'
},
{
id: '4',
user: 'محمد خالد',
action: 'محاولة تسجيل دخول فاشلة',
module: 'المصادقة',
details: 'محاولة تسجيل دخول فاشلة لـ: admin@example.com',
ip: '192.168.1.150',
timestamp: '2024-01-06 11:20:45',
level: 'error'
}
]
const [logs, setLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(20);
const [entityType, setEntityType] = useState('');
const [action, setAction] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const getLevelColor = (level: string) => {
switch (level) {
case 'success':
return 'bg-green-100 text-green-800'
case 'info':
return 'bg-blue-100 text-blue-800'
case 'warning':
return 'bg-yellow-100 text-yellow-800'
case 'error':
return 'bg-red-100 text-red-800'
default:
return 'bg-gray-100 text-gray-800'
const fetchLogs = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await auditLogsAPI.getAll({
entityType: entityType || undefined,
action: action || undefined,
startDate: startDate || undefined,
endDate: endDate || undefined,
page,
pageSize,
});
setLogs(res.data);
setTotal(res.pagination.total);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'فشل تحميل السجل');
} finally {
setLoading(false);
}
}
}, [entityType, action, startDate, endDate, page, pageSize]);
useEffect(() => {
fetchLogs();
}, [fetchLogs]);
const formatDate = (d: string) =>
new Date(d).toLocaleString('ar-SA', { dateStyle: 'medium', timeStyle: 'short' });
const getActionLabel = (a: string) => {
const labels: Record<string, string> = {
CREATE: 'إنشاء',
UPDATE: 'تحديث',
DELETE: 'حذف',
ACTIVATE: 'تفعيل',
DEACTIVATE: 'تعطيل',
};
return labels[a] || a;
};
const handleExport = () => {
const headers = ['المستخدم', 'نوع الكيان', 'الإجراء', 'التاريخ'];
const rows = logs.map((l) => [
l.user?.username || l.userId,
l.entityType,
getActionLabel(l.action),
formatDate(l.createdAt),
]);
const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n');
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `audit-logs-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div>
@@ -68,122 +82,159 @@ export default function AuditLogs() {
<h1 className="text-3xl font-bold text-gray-900 mb-2">سجل العمليات</h1>
<p className="text-gray-600">عرض وتتبع جميع العمليات التي تمت على النظام</p>
</div>
<button className="flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-all shadow-md hover:shadow-lg">
<button
onClick={handleExport}
className="flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-all shadow-md hover:shadow-lg"
>
<Download className="h-5 w-5" />
<span className="font-semibold">تصدير السجل</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{[
{ label: 'إجمالي العمليات', value: '1,234', color: 'bg-blue-500' },
{ label: 'اليوم', value: '45', color: 'bg-green-500' },
{ label: 'الأسبوع', value: '312', color: 'bg-purple-500' },
{ label: 'أخطاء', value: '3', color: 'bg-red-500' }
].map((stat, index) => (
<div key={index} className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className={`${stat.color} w-12 h-12 rounded-lg flex items-center justify-center mb-3`}>
<Activity className="h-6 w-6 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-1">{stat.value}</h3>
<p className="text-sm text-gray-600">{stat.label}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="bg-blue-500 w-12 h-12 rounded-lg flex items-center justify-center mb-3">
<Activity className="h-6 w-6 text-white" />
</div>
))}
<h3 className="text-2xl font-bold text-gray-900 mb-1">{total}</h3>
<p className="text-sm text-gray-600">إجمالي العمليات في الصفحة الحالية</p>
</div>
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="bg-green-500 w-12 h-12 rounded-lg flex items-center justify-center mb-3">
<FileText className="h-6 w-6 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-1">{logs.length}</h3>
<p className="text-sm text-gray-600">عرض في هذه الصفحة</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-md p-6 mb-8 border border-gray-100">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<input
type="text"
placeholder="بحث..."
placeholder="نوع الكيان..."
value={entityType}
onChange={(e) => setEntityType(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<select className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">جميع الوحدات</option>
<option value="users">إدارة المستخدمين</option>
<option value="roles">الأدوار</option>
<option value="backup">النسخ الاحتياطي</option>
</select>
<select className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">جميع المستويات</option>
<option value="success">نجاح</option>
<option value="info">معلومات</option>
<option value="warning">تحذير</option>
<option value="error">خطأ</option>
<select
value={action}
onChange={(e) => setAction(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">جميع الإجراءات</option>
<option value="CREATE">إنشاء</option>
<option value="UPDATE">تحديث</option>
<option value="DELETE">حذف</option>
<option value="ACTIVATE">تفعيل</option>
<option value="DEACTIVATE">تعطيل</option>
</select>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
onClick={() => setPage(1)}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium"
>
بحث
</button>
</div>
<div className="bg-white rounded-xl shadow-md border border-gray-100 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المستخدم</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الإجراء</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الوحدة</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التفاصيل</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التاريخ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المستوى</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{logs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-gray-400" />
<span className="font-medium text-gray-900 text-sm">{log.user}</span>
</div>
</td>
<td className="px-6 py-4">
<span className="text-sm text-gray-700">{log.action}</span>
</td>
<td className="px-6 py-4">
<span className="text-sm font-medium text-blue-600">{log.module}</span>
</td>
<td className="px-6 py-4">
<span className="text-sm text-gray-600">{log.details}</span>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-700">{log.timestamp}</span>
</div>
</td>
<td className="px-6 py-4">
<span className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getLevelColor(log.level)}`}>
{log.level}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<p className="text-sm text-gray-600">
عرض 1-4 من 1,234 عملية
</p>
<div className="flex gap-2">
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50">
السابق
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium">
1
</button>
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50">
التالي
</button>
{loading ? (
<div className="p-12 flex justify-center">
<LoadingSpinner />
</div>
</div>
) : error ? (
<div className="p-12 text-center text-red-600">{error}</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المستخدم</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">نوع الكيان</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الإجراء</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التفاصيل</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التاريخ</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{logs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-gray-400" />
<span className="font-medium text-gray-900 text-sm">
{log.user?.username || log.userId}
</span>
</div>
</td>
<td className="px-6 py-4">
<span className="text-sm font-medium text-blue-600">{log.entityType}</span>
</td>
<td className="px-6 py-4">
<span className="text-sm text-gray-700">{getActionLabel(log.action)}</span>
</td>
<td className="px-6 py-4">
<span className="text-sm text-gray-600">
{log.entityType} #{log.entityId.slice(0, 8)}...
</span>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-700">{formatDate(log.createdAt)}</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<p className="text-sm text-gray-600">
عرض{' '}
<span className="font-semibold">
{logs.length ? (page - 1) * pageSize + 1 : 0}-{Math.min(page * pageSize, total)}
</span>{' '}
من <span className="font-semibold">{total}</span> عملية
</p>
<div className="flex gap-2">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
السابق
</button>
<span className="px-4 py-2 text-sm text-gray-600">
صفحة {page} من {Math.max(1, Math.ceil(total / pageSize))}
</span>
<button
onClick={() => setPage((p) => Math.min(Math.ceil(total / pageSize) || 1, p + 1))}
disabled={page >= Math.ceil(total / pageSize)}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
التالي
</button>
</div>
</div>
</>
)}
</div>
</div>
)
);
}

View File

@@ -1,6 +1,7 @@
'use client'
'use client';
import { useAuth } from '@/contexts/AuthContext'
import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import {
Users,
Shield,
@@ -8,83 +9,105 @@ import {
Activity,
AlertCircle,
CheckCircle,
TrendingUp,
Server
} from 'lucide-react'
Server,
} from 'lucide-react';
import { statsAPI, auditLogsAPI } from '@/lib/api/admin';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function AdminDashboard() {
const { user } = useAuth()
const { user } = useAuth();
const [stats, setStats] = useState<{ totalUsers: number; activeUsers: number; inactiveUsers: number; loginsToday: number } | null>(null);
const [recentLogs, setRecentLogs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const stats = [
useEffect(() => {
async function load() {
setLoading(true);
try {
const [s, logsRes] = await Promise.all([
statsAPI.get(),
auditLogsAPI.getAll({ pageSize: 10, page: 1 }),
]);
setStats(s);
setRecentLogs(logsRes.data || []);
} catch {
// ignore
} finally {
setLoading(false);
}
}
load();
}, []);
const getActionLabel = (a: string) => {
const labels: Record<string, string> = {
CREATE: 'قام بإنشاء',
UPDATE: 'قام بتحديث',
DELETE: 'قام بحذف',
ACTIVATE: 'قام بتفعيل',
DEACTIVATE: 'قام بتعطيل',
};
return labels[a] || a;
};
const formatTime = (d: string) => {
const diff = Date.now() - new Date(d).getTime();
const mins = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
if (mins < 60) return `منذ ${mins} دقيقة`;
if (hours < 24) return `منذ ${hours} ساعة`;
return new Date(d).toLocaleString('ar-SA', { dateStyle: 'short', timeStyle: 'short' });
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<LoadingSpinner size="lg" message="جاري تحميل لوحة التحكم..." />
</div>
);
}
const statCards = [
{
icon: Users,
label: 'إجمالي المستخدمين',
value: '24',
change: '+3 هذا الشهر',
color: 'bg-blue-500'
value: stats?.totalUsers ?? '0',
change: stats ? `+${stats.activeUsers} نشط` : '-',
color: 'bg-blue-500',
},
{
icon: Shield,
label: 'الأدوار النشطة',
value: '8',
change: '2 مخصص',
color: 'bg-purple-500'
value: '-',
change: 'من الأدوار',
color: 'bg-purple-500',
},
{
icon: Database,
label: 'آخر نسخة احتياطية',
value: 'منذ ساعتين',
value: 'قريباً',
change: 'تلقائي يومياً',
color: 'bg-green-500'
color: 'bg-green-500',
},
{
icon: Activity,
label: 'صحة النظام',
value: '99.9%',
change: 'ممتاز',
color: 'bg-teal-500'
}
]
const systemAlerts = [
{
type: 'warning',
message: 'يوجد 3 مستخدمين لم يسجلوا الدخول منذ 30 يوم',
time: 'منذ ساعة'
label: 'تسجيل الدخول اليوم',
value: String(stats?.loginsToday ?? 0),
change: ستخدم',
color: 'bg-teal-500',
},
];
const systemAlerts: { type: 'info' | 'warning'; message: string; time: string }[] = [
{
type: 'info',
message: 'تحديث النظام متاح - الإصدار 1.1.0',
time: 'منذ 3 ساعات'
}
]
const recentActivities = [
{
user: 'أحمد محمد',
action: 'قام بإنشاء مستخدم جديد',
time: 'منذ 10 دقائق'
message: 'النسخ الاحتياطي والتصدير ستكون متاحة قريباً',
time: '-',
},
{
user: 'فاطمة علي',
action: 'قام بتحديث صلاحيات الدور "مدير المبيعات"',
time: 'منذ 25 دقيقة'
},
{
user: 'النظام',
action: 'تم إنشاء نسخة احتياطية تلقائية',
time: 'منذ ساعتين'
},
{
user: 'محمد خالد',
action: 'قام بتغيير إعدادات البريد الإلكتروني',
time: 'منذ 3 ساعات'
}
]
];
return (
<div>
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">لوحة تحكم المدير</h1>
<p className="text-gray-600">مرحباً {user?.username}، إليك نظرة عامة على النظام</p>
@@ -92,8 +115,8 @@ export default function AdminDashboard() {
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{stats.map((stat, index) => {
const Icon = stat.icon
{statCards.map((stat, index) => {
const Icon = stat.icon;
return (
<div key={index} className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="flex items-center justify-between mb-4">
@@ -105,7 +128,7 @@ export default function AdminDashboard() {
<p className="text-sm text-gray-600 mb-1">{stat.label}</p>
<p className="text-xs text-gray-500">{stat.change}</p>
</div>
)
);
})}
</div>
@@ -149,17 +172,25 @@ export default function AdminDashboard() {
النشاطات الأخيرة
</h2>
<div className="space-y-4">
{recentActivities.map((activity, index) => (
<div key={index} className="flex items-start gap-3 p-3 hover:bg-gray-50 rounded-lg transition-colors">
<div className="w-2 h-2 bg-green-500 rounded-full mt-2"></div>
<div className="flex-1">
<p className="text-sm text-gray-900">
<span className="font-semibold">{activity.user}</span> {activity.action}
</p>
<p className="text-xs text-gray-600 mt-1">{activity.time}</p>
{recentLogs.length === 0 ? (
<p className="text-sm text-gray-500">لا توجد نشاطات حديثة</p>
) : (
recentLogs.map((log) => (
<div
key={log.id}
className="flex items-start gap-3 p-3 hover:bg-gray-50 rounded-lg transition-colors"
>
<div className="w-2 h-2 bg-green-500 rounded-full mt-2" />
<div className="flex-1">
<p className="text-sm text-gray-900">
<span className="font-semibold">{log.user?.username || 'نظام'}</span>{' '}
{getActionLabel(log.action)} {log.entityType}
</p>
<p className="text-xs text-gray-600 mt-1">{formatTime(log.createdAt)}</p>
</div>
</div>
</div>
))}
))
)}
</div>
</div>
</div>
@@ -174,7 +205,7 @@ export default function AdminDashboard() {
{[
{ name: 'قاعدة البيانات', status: 'operational', uptime: '99.99%' },
{ name: 'خادم التطبيق', status: 'operational', uptime: '99.95%' },
{ name: 'خدمة البريد', status: 'operational', uptime: '99.90%' }
{ name: 'خدمة البريد', status: 'قريباً', uptime: '-' },
].map((service, index) => (
<div key={index} className="p-4 border border-gray-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
@@ -182,9 +213,6 @@ export default function AdminDashboard() {
<CheckCircle className="h-5 w-5 text-green-500" />
</div>
<p className="text-sm text-gray-600">Uptime: {service.uptime}</p>
<div className="mt-2 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-green-500" style={{ width: service.uptime }}></div>
</div>
</div>
))}
</div>
@@ -211,15 +239,14 @@ export default function AdminDashboard() {
</a>
<a
href="/admin/backup"
href="/admin/audit-logs"
className="bg-gradient-to-br from-green-500 to-green-600 text-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-all transform hover:-translate-y-1"
>
<Database className="h-8 w-8 mb-3" />
<h3 className="text-lg font-bold mb-2">النسخ الاحتياطي</h3>
<p className="text-sm text-green-100">نسخ واستعادة قاعدة البيانات</p>
<Activity className="h-8 w-8 mb-3" />
<h3 className="text-lg font-bold mb-2">سجل العمليات</h3>
<p className="text-sm text-green-100">عرض وتتبع العمليات</p>
</a>
</div>
</div>
)
);
}

View File

@@ -1,219 +1,233 @@
'use client'
'use client';
import { useState } from 'react'
import {
Shield,
Plus,
Edit,
Trash2,
Users,
Check,
X
} from 'lucide-react'
import { useState, useEffect, useCallback } from 'react';
import { Shield, Edit, Users, Check, X } from 'lucide-react';
import { positionsAPI } from '@/lib/api/admin';
import type { PositionRole, PositionPermission } from '@/lib/api/admin';
import Modal from '@/components/Modal';
import LoadingSpinner from '@/components/LoadingSpinner';
const MODULES = [
{ id: 'contacts', name: 'إدارة جهات الاتصال', nameEn: 'Contact Management' },
{ id: 'crm', name: 'إدارة علاقات العملاء', nameEn: 'CRM' },
{ id: 'inventory', name: 'المخزون والأصول', nameEn: 'Inventory & Assets' },
{ id: 'projects', name: 'المهام والمشاريع', nameEn: 'Tasks & Projects' },
{ id: 'hr', name: 'الموارد البشرية', nameEn: 'HR Management' },
{ id: 'marketing', name: 'التسويق', nameEn: 'Marketing' },
{ id: 'admin', name: 'لوحة الإدارة', nameEn: 'Admin' },
];
const ACTIONS = [
{ id: 'read', name: 'عرض' },
{ id: 'create', name: 'إنشاء' },
{ id: 'update', name: 'تعديل' },
{ id: 'delete', name: 'حذف' },
{ id: 'export', name: 'تصدير' },
{ id: 'approve', name: 'اعتماد' },
{ id: 'merge', name: 'دمج' },
];
function hasAction(permission: PositionPermission | undefined, action: string): boolean {
if (!permission?.actions) return false;
const actions = Array.isArray(permission.actions) ? permission.actions : [];
return actions.includes('*') || actions.includes('all') || actions.includes(action);
}
function buildPermissionsFromMatrix(matrix: Record<string, Record<string, boolean>>) {
return MODULES.filter((m) => Object.values(matrix[m.id] || {}).some(Boolean)).map((m) => {
const actions = ACTIONS.filter((a) => matrix[m.id]?.[a.id]).map((a) => a.id);
return {
module: m.id,
resource: '*',
actions: actions.length === ACTIONS.length ? ['*'] : actions,
};
});
}
function buildMatrixFromPermissions(permissions: PositionPermission[]): Record<string, Record<string, boolean>> {
const matrix: Record<string, Record<string, boolean>> = {};
for (const m of MODULES) {
matrix[m.id] = {};
const perm = permissions.find((p) => p.module === m.id && (p.resource === '*' || p.resource === m.id));
const hasAll = perm && (Array.isArray(perm.actions)
? (perm.actions as string[]).includes('*') || (perm.actions as string[]).includes('all')
: false);
for (const a of ACTIONS) {
matrix[m.id][a.id] = hasAll || hasAction(perm, a.id);
}
}
return matrix;
}
export default function RolesManagement() {
const [selectedRole, setSelectedRole] = useState<string | null>(null)
const [showAddModal, setShowAddModal] = useState(false)
const [roles, setRoles] = useState<PositionRole[]>([]);
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 [permissionMatrix, setPermissionMatrix] = useState<Record<string, Record<string, boolean>>>({});
const [saving, setSaving] = useState(false);
// Modules and their permissions
const modules = [
{
id: 'contacts',
name: 'إدارة جهات الاتصال',
nameEn: 'Contact Management'
},
{
id: 'crm',
name: 'إدارة علاقات العملاء',
nameEn: 'CRM'
},
{
id: 'inventory',
name: 'المخزون والأصول',
nameEn: 'Inventory & Assets'
},
{
id: 'projects',
name: 'المهام والمشاريع',
nameEn: 'Tasks & Projects'
},
{
id: 'hr',
name: 'الموارد البشرية',
nameEn: 'HR Management'
},
{
id: 'marketing',
name: 'التسويق',
nameEn: 'Marketing'
const fetchRoles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const pos = await positionsAPI.getAll();
setRoles(pos);
if (selectedRoleId && !pos.find((p) => p.id === selectedRoleId)) {
setSelectedRoleId(null);
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'فشل تحميل الأدوار');
} finally {
setLoading(false);
}
]
}, [selectedRoleId]);
const permissions = [
{ id: 'canView', name: 'عرض', icon: '👁️' },
{ id: 'canCreate', name: 'إنشاء', icon: '' },
{ id: 'canEdit', name: 'تعديل', icon: '✏️' },
{ id: 'canDelete', name: 'حذف', icon: '🗑️' },
{ id: 'canExport', name: 'تصدير', icon: '📤' },
{ id: 'canApprove', name: 'اعتماد', icon: '✅' }
]
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
// Mock roles data
const roles = [
{
id: '1',
name: 'المدير العام',
nameEn: 'General Manager',
description: 'صلاحيات كاملة على النظام',
usersCount: 2,
permissions: {
contacts: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
crm: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
inventory: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
projects: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
hr: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true },
marketing: { canView: true, canCreate: true, canEdit: true, canDelete: true, canExport: true, canApprove: true }
}
},
{
id: '2',
name: 'مدير المبيعات',
nameEn: 'Sales Manager',
description: 'إدارة المبيعات والعملاء مع صلاحيات الاعتماد',
usersCount: 5,
permissions: {
contacts: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: true, canApprove: false },
crm: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: true, canApprove: true },
inventory: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
projects: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: false, canApprove: false },
hr: { canView: false, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
marketing: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false }
}
},
{
id: '3',
name: 'مندوب مبيعات',
nameEn: 'Sales Representative',
description: 'إدخال وتعديل بيانات المبيعات الأساسية',
usersCount: 12,
permissions: {
contacts: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: false, canApprove: false },
crm: { canView: true, canCreate: true, canEdit: true, canDelete: false, canExport: false, canApprove: false },
inventory: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
projects: { canView: true, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
hr: { canView: false, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false },
marketing: { canView: false, canCreate: false, canEdit: false, canDelete: false, canExport: false, canApprove: false }
}
const currentRole = roles.find((r) => r.id === selectedRoleId);
useEffect(() => {
if (currentRole) {
setPermissionMatrix(buildMatrixFromPermissions(currentRole.permissions || []));
}
]
}, [currentRole?.id, currentRole?.permissions]);
const currentRole = roles.find(r => r.id === selectedRole)
const handleTogglePermission = (moduleId: string, actionId: string) => {
setPermissionMatrix((prev) => ({
...prev,
[moduleId]: {
...(prev[moduleId] || {}),
[actionId]: !prev[moduleId]?.[actionId],
},
}));
};
const handleSavePermissions = async () => {
if (!selectedRoleId) return;
setSaving(true);
try {
const permissions = buildPermissionsFromMatrix(permissionMatrix);
await positionsAPI.updatePermissions(selectedRoleId, permissions);
setShowEditModal(false);
fetchRoles();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : 'فشل حفظ الصلاحيات');
} finally {
setSaving(false);
}
};
const handleSelectRole = (id: string) => {
setSelectedRoleId(id);
setShowEditModal(false);
};
return (
<div>
{/* Header */}
<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={() => setShowAddModal(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>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Roles List */}
<div className="lg:col-span-1 space-y-4">
<h2 className="text-xl font-bold text-gray-900 mb-4">الأدوار ({roles.length})</h2>
{loading ? (
<div className="flex justify-center p-12">
<LoadingSpinner />
</div>
) : error ? (
<div className="text-center text-red-600 p-12">{error}</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Roles List */}
<div className="lg:col-span-1 space-y-4">
<h2 className="text-xl font-bold text-gray-900 mb-4">الأدوار ({roles.length})</h2>
{roles.map((role) => (
<div
key={role.id}
onClick={() => setSelectedRole(role.id)}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selectedRole === role.id
? 'border-purple-600 bg-purple-50 shadow-md'
: 'border-gray-200 bg-white hover:border-purple-300 hover:shadow-sm'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${selectedRole === role.id ? 'bg-purple-600' : 'bg-purple-100'}`}>
<Shield className={`h-5 w-5 ${selectedRole === role.id ? 'text-white' : 'text-purple-600'}`} />
</div>
<div>
<h3 className="font-bold text-gray-900">{role.name}</h3>
<p className="text-xs text-gray-600">{role.nameEn}</p>
{roles.map((role) => (
<div
key={role.id}
onClick={() => handleSelectRole(role.id)}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selectedRoleId === role.id
? 'border-purple-600 bg-purple-50 shadow-md'
: 'border-gray-200 bg-white hover:border-purple-300 hover:shadow-sm'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<div
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'}`}
/>
</div>
<div>
<h3 className="font-bold text-gray-900">{role.titleAr || role.title}</h3>
<p className="text-xs text-gray-600">{role.title}</p>
</div>
</div>
</div>
</div>
<p className="text-sm text-gray-600 mb-3">{role.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-4 w-4" />
<span>{role.usersCount} مستخدم</span>
</div>
<div className="flex gap-1">
<button className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-4 w-4" />
<span>{role.usersCount ?? role._count?.employees ?? 0} مستخدم</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setSelectedRoleId(role.id);
setShowEditModal(true);
}}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
>
<Edit className="h-4 w-4" />
</button>
<button className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
))}
</div>
{/* Permission Matrix */}
<div className="lg:col-span-2">
{currentRole ? (
<div className="bg-white rounded-xl shadow-lg border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">{currentRole.name}</h2>
<p className="text-gray-600">{currentRole.description}</p>
{/* Permission Matrix */}
<div className="lg:col-span-2">
{currentRole ? (
<div className="bg-white rounded-xl shadow-lg border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">{currentRole.titleAr || currentRole.title}</h2>
<p className="text-gray-600">{currentRole.title}</p>
</div>
<button
onClick={() => setShowEditModal(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
>
تعديل الصلاحيات
</button>
</div>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium">
حفظ التغييرات
</button>
</div>
</div>
<div className="p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">مصفوفة الصلاحيات</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="px-4 py-3 text-right text-sm font-bold text-gray-900 min-w-[200px]">
الوحدة
</th>
{permissions.map((perm) => (
<th key={perm.id} className="px-4 py-3 text-center text-sm font-bold text-gray-900">
<div className="flex flex-col items-center gap-1">
<span className="text-xl">{perm.icon}</span>
<span>{perm.name}</span>
</div>
<div className="p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">مصفوفة الصلاحيات</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="px-4 py-3 text-right text-sm font-bold text-gray-900 min-w-[200px]">
الوحدة
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{modules.map((module) => {
const modulePerms = currentRole.permissions[module.id as keyof typeof currentRole.permissions]
return (
{ACTIONS.map((perm) => (
<th key={perm.id} className="px-4 py-3 text-center text-sm font-bold text-gray-900">
{perm.name}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{MODULES.map((module) => (
<tr key={module.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-4">
<div>
@@ -221,72 +235,106 @@ export default function RolesManagement() {
<p className="text-xs text-gray-600">{module.nameEn}</p>
</div>
</td>
{permissions.map((perm) => {
const hasPermission = modulePerms?.[perm.id as keyof typeof modulePerms]
{ACTIONS.map((action) => {
const hasPermission = permissionMatrix[module.id]?.[action.id];
return (
<td key={perm.id} className="px-4 py-4 text-center">
<label className="inline-flex items-center justify-center cursor-pointer">
<input
type="checkbox"
checked={!!hasPermission}
className="sr-only peer"
/>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all ${
<td key={action.id} className="px-4 py-4 text-center">
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all mx-auto ${
hasPermission
? 'bg-green-500 shadow-md'
: 'bg-gray-200 hover:bg-gray-300'
}`}>
{hasPermission ? (
<Check className="h-6 w-6 text-white" />
) : (
<X className="h-6 w-6 text-gray-500" />
)}
</div>
</label>
? '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" />}
</div>
</td>
)
);
})}
</tr>
)
})}
</tbody>
</table>
</div>
{/* Legend */}
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-bold text-blue-900 mb-2">💡 معلومات:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> انقر على المربعات لتفعيل أو إلغاء الصلاحيات</li>
<li> الصلاحيات تطبق فوراً على جميع مستخدمي هذا الدور</li>
<li> يجب أن يكون لديك صلاحية "عرض" على الأقل للوصول إلى الوحدة</li>
</ul>
</div>
{/* Quick Actions */}
<div className="mt-6 flex gap-3">
<button className="flex-1 px-4 py-3 border-2 border-green-600 text-green-600 rounded-lg hover:bg-green-50 transition-colors font-semibold">
منح جميع الصلاحيات
</button>
<button className="flex-1 px-4 py-3 border-2 border-red-600 text-red-600 rounded-lg hover:bg-red-50 transition-colors font-semibold">
إلغاء جميع الصلاحيات
</button>
<button className="flex-1 px-4 py-3 border-2 border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50 transition-colors font-semibold">
👁 صلاحيات العرض فقط
</button>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
) : (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12 text-center">
<Shield className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">اختر دوراً لعرض الصلاحيات</h3>
<p className="text-gray-600">اختر دور من القائمة لعرض وتعديل صلاحياته</p>
</div>
)}
) : (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12 text-center">
<Shield className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">اختر دوراً لعرض الصلاحيات</h3>
<p className="text-gray-600">اختر دور من القائمة لعرض وتعديل صلاحياته</p>
</div>
)}
</div>
</div>
</div>
</div>
)
}
)}
{/* Edit Permissions Modal */}
<Modal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
title={`تعديل صلاحيات: ${currentRole?.titleAr || currentRole?.title || ''}`}
size="2xl"
>
{currentRole && (
<div>
<div className="overflow-x-auto mb-6">
<table className="w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="px-4 py-3 text-right text-sm font-bold text-gray-900">الوحدة</th>
{ACTIONS.map((perm) => (
<th key={perm.id} className="px-4 py-3 text-center text-sm font-bold text-gray-900">
{perm.name}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{MODULES.map((module) => (
<tr key={module.id}>
<td className="px-4 py-4">
<p className="font-semibold text-gray-900">{module.name}</p>
</td>
{ACTIONS.map((action) => {
const hasPermission = permissionMatrix[module.id]?.[action.id];
return (
<td key={action.id} className="px-4 py-4 text-center">
<button
type="button"
onClick={() => handleTogglePermission(module.id, action.id)}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all mx-auto ${
hasPermission ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-500'
}`}
>
{hasPermission ? <Check className="h-6 w-6" /> : <X className="h-6 w-6" />}
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => setShowEditModal(false)}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium"
>
إلغاء
</button>
<button
onClick={handleSavePermissions}
disabled={saving}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-semibold disabled:opacity-50"
>
{saving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</div>
</div>
)}
</Modal>
</div>
);
}

View File

@@ -1,6 +1,6 @@
'use client'
'use client';
import { useState } from 'react'
import { useState, useEffect, useCallback } from 'react';
import {
Users,
Plus,
@@ -10,53 +10,132 @@ import {
Lock,
Unlock,
Mail,
Phone,
Shield,
Calendar,
Filter
} from 'lucide-react'
} from 'lucide-react';
import { usersAPI, statsAPI, positionsAPI } from '@/lib/api/admin';
import { employeesAPI } from '@/lib/api/employees';
import type { User, CreateUserData, UpdateUserData } from '@/lib/api/admin';
import type { Employee } from '@/lib/api/employees';
import Modal from '@/components/Modal';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function UsersManagement() {
const [searchTerm, setSearchTerm] = useState('')
const [showAddModal, setShowAddModal] = useState(false)
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(20);
const [searchTerm, setSearchTerm] = useState('');
const [selectedRole, setSelectedRole] = useState<string>('');
const [selectedStatus, setSelectedStatus] = useState<string>('');
const [stats, setStats] = useState({ totalUsers: 0, activeUsers: 0, inactiveUsers: 0, loginsToday: 0 });
const [positions, setPositions] = useState<{ id: string; title: string; titleAr?: string | null }[]>([]);
const [employees, setEmployees] = useState<Employee[]>([]);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [saving, setSaving] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
// Mock data - replace with actual API calls
const users = [
{
id: '1',
username: 'admin',
email: 'gm@atmata.com',
fullName: 'أحمد محمد السعيد',
role: 'المدير العام',
status: 'active',
lastLogin: '2024-01-06 14:30',
createdAt: '2024-01-01'
},
{
id: '2',
username: 'salesmanager',
email: 'sales.manager@atmata.com',
fullName: 'فاطمة الزهراني',
role: 'مدير المبيعات',
status: 'active',
lastLogin: '2024-01-06 09:15',
createdAt: '2024-01-01'
},
{
id: '3',
username: 'salesrep',
email: 'sales.rep@atmata.com',
fullName: 'محمد القحطاني',
role: 'مندوب مبيعات',
status: 'active',
lastLogin: '2024-01-05 16:45',
createdAt: '2024-01-01'
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await usersAPI.getAll({
search: searchTerm || undefined,
status: selectedStatus === 'active' ? 'active' : selectedStatus === 'inactive' ? 'inactive' : undefined,
positionId: selectedRole || undefined,
page,
pageSize,
});
setUsers(res.data);
setTotal(res.pagination.total);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'فشل تحميل المستخدمين');
} finally {
setLoading(false);
}
]
}, [searchTerm, selectedRole, selectedStatus, page, pageSize]);
const fetchStats = useCallback(async () => {
try {
const s = await statsAPI.get();
setStats(s);
} catch {
// ignore
}
}, []);
const fetchPositions = useCallback(async () => {
try {
const pos = await positionsAPI.getAll();
setPositions(pos.map((p) => ({ id: p.id, title: p.title, titleAr: p.titleAr })));
} catch {
// ignore
}
}, []);
const fetchEmployees = useCallback(async () => {
try {
const res = await employeesAPI.getAll({ status: 'ACTIVE', pageSize: 500 });
setEmployees(res.employees);
} catch {
setEmployees([]);
}
}, []);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
useEffect(() => {
fetchStats();
fetchPositions();
}, [fetchStats, fetchPositions]);
useEffect(() => {
if (showAddModal || showEditModal) fetchEmployees();
}, [showAddModal, showEditModal, fetchEmployees]);
const handleToggleActive = async (id: string) => {
try {
await usersAPI.toggleActive(id);
fetchUsers();
fetchStats();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : 'فشل تحديث الحالة');
}
};
const handleDelete = async (id: string) => {
if (deleteConfirm !== id) return;
try {
await usersAPI.delete(id);
setDeleteConfirm(null);
fetchUsers();
fetchStats();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : 'فشل حذف المستخدم');
}
};
const formatDate = (d: string | null | undefined) =>
d ? new Date(d).toLocaleString('ar-SA', { dateStyle: 'short', timeStyle: 'short' }) : '-';
const getFullName = (u: User) =>
u.employee
? `${u.employee.firstName} ${u.employee.lastName}`.trim() ||
`${u.employee.firstNameAr || ''} ${u.employee.lastNameAr || ''}`.trim() ||
u.username
: u.username;
const getRole = (u: User) =>
u.employee?.position?.titleAr || u.employee?.position?.title || '-';
return (
<div>
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">إدارة المستخدمين</h1>
@@ -74,10 +153,10 @@ export default function UsersManagement() {
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{[
{ label: 'إجمالي المستخدمين', value: '24', color: 'bg-blue-500' },
{ label: 'المستخدمون النشطون', value: '21', color: 'bg-green-500' },
{ label: 'المستخدمون المعطلون', value: '3', color: 'bg-red-500' },
{ label: 'تسجيل دخول اليوم', value: '18', color: 'bg-purple-500' }
{ label: 'إجمالي المستخدمين', value: String(stats.totalUsers), color: 'bg-blue-500' },
{ label: 'المستخدمون النشطون', value: String(stats.activeUsers), color: 'bg-green-500' },
{ label: 'المستخدمون المعطلون', value: String(stats.inactiveUsers), color: 'bg-red-500' },
{ label: 'تسجيل دخول اليوم', value: String(stats.loginsToday), color: 'bg-purple-500' },
].map((stat, index) => (
<div key={index} className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className={`${stat.color} w-12 h-12 rounded-lg flex items-center justify-center mb-3`}>
@@ -99,231 +178,510 @@ export default function UsersManagement() {
placeholder="بحث بالاسم أو البريد..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && fetchUsers()}
className="w-full pr-10 pl-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<select className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<select
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">جميع الأدوار</option>
<option value="admin">المدير العام</option>
<option value="manager">مدير المبيعات</option>
<option value="sales">مندوب مبيعات</option>
{positions.map((p) => (
<option key={p.id} value={p.id}>
{p.titleAr || p.title}
</option>
))}
</select>
<select className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">جميع الحالات</option>
<option value="active">نشط</option>
<option value="inactive">معطل</option>
</select>
</div>
<button
onClick={() => fetchUsers()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium"
>
بحث
</button>
</div>
{/* Users Table */}
<div className="bg-white rounded-xl shadow-md border border-gray-100 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المستخدم</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">البريد الإلكتروني</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الدور</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الحالة</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">آخر تسجيل دخول</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الإجراءات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-bold">
{user.fullName.charAt(0)}
</div>
<div>
<p className="font-semibold text-gray-900">{user.fullName}</p>
<p className="text-sm text-gray-600">@{user.username}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-gray-700">
<Mail className="h-4 w-4 text-gray-400" />
<span className="text-sm">{user.email}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-purple-500" />
<span className="text-sm font-medium text-gray-900">{user.role}</span>
</div>
</td>
<td className="px-6 py-4">
{user.status === 'active' ? (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
نشط
</span>
) : (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
معطل
</span>
)}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-gray-600">
<Calendar className="h-4 w-4 text-gray-400" />
<span className="text-sm">{user.lastLogin}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<Edit className="h-4 w-4" />
</button>
<button className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors">
{user.status === 'active' ? (
<Lock className="h-4 w-4" />
) : (
<Unlock className="h-4 w-4" />
)}
</button>
<button className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<p className="text-sm text-gray-600">
عرض <span className="font-semibold">1-3</span> من <span className="font-semibold">24</span> مستخدم
</p>
<div className="flex gap-2">
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
السابق
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
1
</button>
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
2
</button>
<button className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
التالي
</button>
{loading ? (
<div className="p-12 flex justify-center">
<LoadingSpinner />
</div>
</div>
) : error ? (
<div className="p-12 text-center text-red-600">{error}</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المستخدم</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">البريد الإلكتروني</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الدور</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الحالة</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">آخر تسجيل دخول</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الإجراءات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-bold">
{getFullName(user).charAt(0)}
</div>
<div>
<p className="font-semibold text-gray-900">{getFullName(user)}</p>
<p className="text-sm text-gray-600">@{user.username}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-gray-700">
<Mail className="h-4 w-4 text-gray-400" />
<span className="text-sm">{user.email}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-purple-500" />
<span className="text-sm font-medium text-gray-900">{getRole(user)}</span>
</div>
</td>
<td className="px-6 py-4">
{user.isActive ? (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
نشط
</span>
) : (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
معطل
</span>
)}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-gray-600">
<Calendar className="h-4 w-4 text-gray-400" />
<span className="text-sm">{formatDate(user.lastLogin)}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button
onClick={() => {
setEditingUser(user);
setShowEditModal(true);
}}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="تعديل"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleToggleActive(user.id)}
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
title={user.isActive ? 'تعطيل' : 'تفعيل'}
>
{user.isActive ? <Lock className="h-4 w-4" /> : <Unlock className="h-4 w-4" />}
</button>
{deleteConfirm === user.id ? (
<div className="flex gap-1">
<button
onClick={() => handleDelete(user.id)}
className="px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700"
>
تأكيد
</button>
<button
onClick={() => setDeleteConfirm(null)}
className="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50"
>
إلغاء
</button>
</div>
) : (
<button
onClick={() => setDeleteConfirm(user.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="حذف"
>
<Trash2 className="h-4 w-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<p className="text-sm text-gray-600">
عرض{' '}
<span className="font-semibold">
{users.length ? (page - 1) * pageSize + 1 : 0}-{Math.min(page * pageSize, total)}
</span>{' '}
من <span className="font-semibold">{total}</span> مستخدم
</p>
<div className="flex gap-2">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
السابق
</button>
{Array.from({ length: Math.min(5, Math.ceil(total / pageSize) || 1) }, (_, i) => i + 1).map(
(n) => (
<button
key={n}
onClick={() => setPage(n)}
className={`px-4 py-2 rounded-lg text-sm font-medium ${
page === n
? 'bg-blue-600 text-white'
: 'border border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
{n}
</button>
)
)}
<button
onClick={() => setPage((p) => Math.min(Math.ceil(total / pageSize) || 1, p + 1))}
disabled={page >= Math.ceil(total / pageSize)}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
التالي
</button>
</div>
</div>
</>
)}
</div>
{/* Add User Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900">إضافة مستخدم جديد</h2>
</div>
<AddUserModal
isOpen={showAddModal}
onClose={() => setShowAddModal(false)}
onSuccess={() => {
setShowAddModal(false);
fetchUsers();
fetchStats();
}}
employees={employees}
saving={saving}
setSaving={setSaving}
/>
<div className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">الاسم الأول</label>
<input
type="text"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="أحمد"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">الاسم الأخير</label>
<input
type="text"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="محمد"
/>
</div>
</div>
{/* Edit User Modal */}
<EditUserModal
isOpen={showEditModal}
user={editingUser}
onClose={() => {
setShowEditModal(false);
setEditingUser(null);
}}
onSuccess={() => {
setShowEditModal(false);
setEditingUser(null);
fetchUsers();
fetchStats();
}}
employees={employees}
saving={saving}
setSaving={setSaving}
/>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">اسم المستخدم</label>
<input
type="text"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="ahmed.mohamed"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">البريد الإلكتروني</label>
<input
type="email"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="ahmed@example.com"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">كلمة المرور</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="••••••••"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">الدور</label>
<select className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">اختر الدور</option>
<option value="admin">المدير العام</option>
<option value="manager">مدير المبيعات</option>
<option value="sales">مندوب مبيعات</option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">الموظف المرتبط</label>
<select className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">اختر الموظف</option>
<option value="1">أحمد محمد السعيد - EMP-2024-0001</option>
<option value="2">فاطمة الزهراني - EMP-2024-0002</option>
</select>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="active"
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
defaultChecked
/>
<label htmlFor="active" className="text-sm font-medium text-gray-700">
تفعيل الحساب فوراً
</label>
</div>
</div>
<div className="p-6 border-t border-gray-200 flex gap-3 justify-end">
<button
onClick={() => setShowAddModal(false)}
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
>
إلغاء
</button>
<button className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold">
إنشاء المستخدم
</button>
</div>
</div>
</div>
)}
</div>
)
);
}
function AddUserModal({
isOpen,
onClose,
onSuccess,
employees,
saving,
setSaving,
}: {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
employees: Employee[];
saving: boolean;
setSaving: (v: boolean) => void;
}) {
const [form, setForm] = useState<CreateUserData & { isActive: boolean }>({
email: '',
username: '',
password: '',
employeeId: '',
isActive: true,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.email || !form.username || !form.password || !form.employeeId) {
alert('يرجى ملء جميع الحقول المطلوبة');
return;
}
if (form.password.length < 8) {
alert('كلمة المرور يجب أن تكون 8 أحرف على الأقل');
return;
}
setSaving(true);
try {
await usersAPI.create(form);
onSuccess();
} catch (err: unknown) {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'فشل إنشاء المستخدم';
alert(msg);
} finally {
setSaving(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="إضافة مستخدم جديد" size="lg">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">الموظف المرتبط *</label>
<select
value={form.employeeId}
onChange={(e) => {
const emp = employees.find((em) => em.id === e.target.value);
setForm((f) => ({
...f,
employeeId: e.target.value,
email: emp?.email || f.email,
username: emp ? `${emp.firstName}.${emp.lastName}`.toLowerCase().replace(/\s/g, '.') : f.username,
}));
}}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">اختر الموظف</option>
{employees.map((emp) => (
<option key={emp.id} value={emp.id}>
{emp.firstName} {emp.lastName} - {emp.uniqueEmployeeId}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">اسم المستخدم *</label>
<input
type="text"
value={form.username}
onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="ahmed.mohamed"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">البريد الإلكتروني *</label>
<input
type="email"
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="ahmed@example.com"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">كلمة المرور *</label>
<input
type="password"
value={form.password}
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
required
minLength={8}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="••••••••"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="active"
checked={form.isActive}
onChange={(e) => setForm((f) => ({ ...f, isActive: e.target.checked }))}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="active" className="text-sm font-medium text-gray-700">
تفعيل الحساب فوراً
</label>
</div>
<div className="flex gap-3 justify-end pt-4">
<button type="button" onClick={onClose} className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium">
إلغاء
</button>
<button type="submit" disabled={saving} className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-semibold disabled:opacity-50">
{saving ? 'جاري الحفظ...' : 'إنشاء المستخدم'}
</button>
</div>
</form>
</Modal>
);
}
function EditUserModal({
isOpen,
user,
onClose,
onSuccess,
employees,
saving,
setSaving,
}: {
isOpen: boolean;
user: User | null;
onClose: () => void;
onSuccess: () => void;
employees: Employee[];
saving: boolean;
setSaving: (v: boolean) => void;
}) {
const [form, setForm] = useState<UpdateUserData & { isActive?: boolean }>({
email: '',
username: '',
password: '',
employeeId: null,
isActive: true,
});
useEffect(() => {
if (user) {
setForm({
email: user.email,
username: user.username,
password: '',
employeeId: user.employeeId ?? undefined,
isActive: user.isActive,
});
}
}, [user]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
if (!form.email || !form.username) {
alert('يرجى ملء الحقول المطلوبة');
return;
}
setSaving(true);
try {
const data: UpdateUserData = {
email: form.email,
username: form.username,
isActive: form.isActive,
employeeId: form.employeeId === '' ? null : form.employeeId || undefined,
};
if (form.password?.length && form.password.length >= 8) {
data.password = form.password;
}
await usersAPI.update(user.id, data);
onSuccess();
} catch (err: unknown) {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'فشل تحديث المستخدم';
alert(msg);
} finally {
setSaving(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="تعديل المستخدم" size="lg">
{user ? (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">الموظف المرتبط</label>
<select
value={form.employeeId || ''}
onChange={(e) => setForm((f) => ({ ...f, employeeId: e.target.value || null }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">-- بدون موظف --</option>
{employees.map((emp) => (
<option key={emp.id} value={emp.id}>
{emp.firstName} {emp.lastName} - {emp.uniqueEmployeeId}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">اسم المستخدم *</label>
<input
type="text"
value={form.username}
onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">البريد الإلكتروني *</label>
<input
type="email"
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">كلمة مرور جديدة (اختياري)</label>
<input
type="password"
value={form.password || ''}
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value || undefined }))}
minLength={8}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="اتركه فارغاً للإبقاء على كلمة المرور الحالية"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="edit-active"
checked={form.isActive ?? true}
onChange={(e) => setForm((f) => ({ ...f, isActive: e.target.checked }))}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="edit-active" className="text-sm font-medium text-gray-700">
الحساب نشط
</label>
</div>
<div className="flex gap-3 justify-end pt-4">
<button type="button" onClick={onClose} className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium">
إلغاء
</button>
<button type="submit" disabled={saving} className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-semibold disabled:opacity-50">
{saving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</div>
</form>
) : (
<LoadingSpinner />
)}
</Modal>
);
}

View File

@@ -1,140 +1,266 @@
import { api } from '../api'
import { api } from '../api';
// Users API
export interface User {
id: string
email: string
username: string
status: string
employeeId?: string
employee?: any
createdAt: string
updatedAt: string
id: string;
email: string;
username: string;
isActive: boolean;
lastLogin?: string | null;
employeeId?: string | null;
employee?: {
id: string;
firstName: string;
lastName: string;
firstNameAr?: string | null;
lastNameAr?: string | null;
position?: { id: string; title: string; titleAr?: string | null };
department?: { name: string; nameAr?: string | null };
};
createdAt: string;
updatedAt: string;
}
export interface CreateUserData {
email: string
username: string
password: string
employeeId?: string
email: string;
username: string;
password: string;
employeeId: string;
isActive?: boolean;
}
export interface UpdateUserData {
email?: string
username?: string
password?: string
status?: string
employeeId?: string
email?: string;
username?: string;
password?: string;
employeeId?: string | null;
isActive?: boolean;
}
export interface UserFilters {
search?: string;
status?: 'active' | 'inactive';
positionId?: string;
page?: number;
pageSize?: number;
}
export interface PaginatedResponse<T> {
success: boolean;
data: T[];
pagination: {
total: number;
page: number;
pageSize: number;
totalPages: number;
};
}
export const usersAPI = {
getAll: async (): Promise<User[]> => {
const response = await api.get('/auth/users')
return response.data.data || response.data
getAll: async (filters?: UserFilters): Promise<PaginatedResponse<User>> => {
const params = new URLSearchParams();
if (filters?.search) params.append('search', filters.search);
if (filters?.status) params.append('status', filters.status);
if (filters?.positionId) params.append('positionId', filters.positionId);
if (filters?.page) params.append('page', String(filters.page));
if (filters?.pageSize) params.append('pageSize', String(filters.pageSize));
const response = await api.get(`/admin/users?${params.toString()}`);
return {
success: true,
data: response.data.data || [],
pagination: response.data.pagination || { total: 0, page: 1, pageSize: 20, totalPages: 0 },
};
},
getById: async (id: string): Promise<User> => {
const response = await api.get(`/auth/users/${id}`)
return response.data.data
const response = await api.get(`/admin/users/${id}`);
return response.data.data;
},
create: async (data: CreateUserData): Promise<User> => {
const response = await api.post('/auth/register', data)
return response.data.data
const response = await api.post('/admin/users', data);
return response.data.data;
},
update: async (id: string, data: UpdateUserData): Promise<User> => {
const response = await api.put(`/auth/users/${id}`, data)
return response.data.data
const response = await api.put(`/admin/users/${id}`, data);
return response.data.data;
},
toggleActive: async (id: string): Promise<User> => {
const response = await api.patch(`/admin/users/${id}/toggle-active`);
return response.data.data;
},
delete: async (id: string): Promise<void> => {
await api.delete(`/auth/users/${id}`)
}
await api.delete(`/admin/users/${id}`);
},
};
// Stats API
export interface AdminStats {
totalUsers: number;
activeUsers: number;
inactiveUsers: number;
loginsToday: number;
}
// Roles & Permissions API
export const statsAPI = {
get: async (): Promise<AdminStats> => {
const response = await api.get('/admin/stats');
return response.data.data || response.data;
},
};
// Positions (Roles) API - maps to HR positions with permissions
export interface PositionPermission {
id: string;
module: string;
resource: string;
actions: string[];
}
export interface PositionRole {
id: string;
title: string;
titleAr?: string | null;
code: string;
department?: { name: string; nameAr?: string | null };
permissions: PositionPermission[];
usersCount: number;
_count?: { employees: number };
}
export const positionsAPI = {
getAll: async (): Promise<PositionRole[]> => {
const response = await api.get('/admin/positions');
return response.data.data || [];
},
updatePermissions: async (
positionId: string,
permissions: Array<{ module: string; resource: string; actions: string[] }>
): Promise<PositionRole> => {
const response = await api.put(`/admin/positions/${positionId}/permissions`, {
permissions,
});
return response.data.data;
},
};
// Roles API - alias for positions (for compatibility with existing frontend)
export interface Role {
id: string
name: string
nameAr?: string
permissions: Permission[]
}
export interface Permission {
id: string
module: string
resource: string
action: string
id: string;
name: string;
nameAr?: string;
title?: string;
titleAr?: string;
permissions: { id?: string; module: string; resource: string; actions: string[] }[];
usersCount?: number;
}
export const rolesAPI = {
getAll: async (): Promise<Role[]> => {
const response = await api.get('/admin/roles')
return response.data.data || []
const positions = await positionsAPI.getAll();
return positions.map((p) => ({
id: p.id,
name: p.title,
nameAr: p.titleAr || undefined,
title: p.title,
titleAr: p.titleAr || undefined,
permissions: p.permissions,
usersCount: p.usersCount,
}));
},
update: async (id: string, permissions: Permission[]): Promise<Role> => {
const response = await api.put(`/admin/roles/${id}/permissions`, { permissions })
return response.data.data
}
}
update: async (
id: string,
permissions: Array<{ module: string; resource: string; actions: string[] }>
): Promise<Role> => {
const position = await positionsAPI.updatePermissions(id, permissions);
return {
id: position.id,
name: position.title,
nameAr: position.titleAr || undefined,
permissions: position.permissions,
usersCount: position.usersCount,
};
},
};
// Audit Logs API
export interface AuditLog {
id: string
entityType: string
entityId: string
action: string
userId: string
user?: any
changes?: any
createdAt: string
id: string;
entityType: string;
entityId: string;
action: string;
userId: string;
user?: { id: string; username: string; email: string };
changes?: unknown;
createdAt: string;
}
export interface AuditLogFilters {
entityType?: string;
action?: string;
userId?: string;
startDate?: string;
endDate?: string;
page?: number;
pageSize?: number;
}
export const auditLogsAPI = {
getAll: async (filters?: any): Promise<AuditLog[]> => {
const params = new URLSearchParams()
if (filters?.entityType) params.append('entityType', filters.entityType)
if (filters?.action) params.append('action', filters.action)
if (filters?.startDate) params.append('startDate', filters.startDate)
if (filters?.endDate) params.append('endDate', filters.endDate)
getAll: async (filters?: AuditLogFilters): Promise<PaginatedResponse<AuditLog>> => {
const params = new URLSearchParams();
if (filters?.entityType) params.append('entityType', filters.entityType);
if (filters?.action) params.append('action', filters.action);
if (filters?.userId) params.append('userId', filters.userId);
if (filters?.startDate) params.append('startDate', filters.startDate);
if (filters?.endDate) params.append('endDate', filters.endDate);
if (filters?.page) params.append('page', String(filters.page));
if (filters?.pageSize) params.append('pageSize', String(filters.pageSize));
const response = await api.get(`/admin/audit-logs?${params.toString()}`)
return response.data.data || []
}
}
const response = await api.get(`/admin/audit-logs?${params.toString()}`);
return {
success: true,
data: response.data.data || [],
pagination: response.data.pagination || { total: 0, page: 1, pageSize: 20, totalPages: 0 },
};
},
};
// System Settings API
// System Settings API (placeholder - out of scope)
export interface SystemSetting {
key: string
value: any
description?: string
key: string;
value: unknown;
description?: string;
}
export const settingsAPI = {
getAll: async (): Promise<SystemSetting[]> => {
const response = await api.get('/admin/settings')
return response.data.data || []
const response = await api.get('/admin/settings').catch(() => ({ data: { data: [] } }));
return response.data?.data || [];
},
update: async (key: string, value: any): Promise<SystemSetting> => {
const response = await api.put(`/admin/settings/${key}`, { value })
return response.data.data
}
}
update: async (key: string, value: unknown): Promise<SystemSetting> => {
const response = await api.put(`/admin/settings/${key}`, { value });
return response.data.data;
},
};
// System Health API
// System Health API (placeholder - optional)
export interface SystemHealth {
status: string
database: string
memory: any
uptime: number
status: string;
database: string;
memory?: unknown;
uptime?: number;
}
export const healthAPI = {
check: async (): Promise<SystemHealth> => {
const response = await api.get('/admin/health')
return response.data.data || response.data
}
}
const response = await api.get('/admin/health').catch(() => ({ data: { data: {} } }));
return response.data?.data || response.data || {};
},
};