Commit f50a8d54 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: multi-tenant role system with workflows & user management

Complete overhaul from single-admin to multi-role multi-org system:

- Auth: multi-user login from admin_users table with 9 roles
  (superadmin, admin, moderator, org_admin, org_manager,
  tournament_organizer, sponsor, charity, viewer)
- Permissions: granular permission matrix with org-scoping
- RuleEngine: event-driven workflow automation
- Users module: full CRUD for admin user management
- Workflows module: approval queue, automation rules, org requests
- Dashboard: role-aware with scoped data per role
- Sidebar: permission-filtered navigation per role
- Topbar: shows role badge next to username
- Login: supports all user types
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 02d81493
<?php
/**
* Role-based permission matrix.
*
* Permission format:
* '*' — full access (superadmin only)
* 'module' — full access to the module (all actions)
* 'module.*' — same as above, explicit wildcard
* 'module.action'— specific action within a module
* 'module.own' — scoped to own resources only
*
* Granular actions:
* list, show, create, update, delete, ban, unban, start, complete,
* approve, reject, verify, export, own
*/
return [
// ─── Full access ─────────────────────────────────────────────────────
'superadmin' => ['*'],
// ─── Everything except settings & branding ───────────────────────────
'admin' => [
'dashboard', 'players', 'games', 'chess-bots', 'tournaments',
'organizations', 'economy', 'ads', 'moderation', 'feature-flags',
'notifications', 'analytics', 'audit-log'
'dashboard',
'players.*',
'games.*',
'chess-bots.*',
'tournaments.*',
'organizations.*',
'economy.*',
'ads.*',
'moderation.*',
'feature-flags.*',
'notifications.*',
'analytics.*',
'audit-log.*',
'reports.*',
],
// ─── Moderation focused ──────────────────────────────────────────────
'moderator' => [
'dashboard', 'players.list', 'players.show', 'players.ban', 'players.unban',
'moderation'
'dashboard',
'players.list',
'players.show',
'players.ban',
'players.unban',
'moderation.*',
'audit-log.list',
'audit-log.show',
'reports.list',
'reports.show',
'reports.update',
],
// ─── Org admin: full control over own organization ───────────────────
'org_admin' => [
'dashboard',
'tournaments.list',
'tournaments.show',
'tournaments.create',
'tournaments.update',
'tournaments.start',
'tournaments.complete',
'tournaments.delete',
'tournaments.own',
'organizations.own',
'organizations.show',
'organizations.update',
'organizations.members',
'players.list',
'players.show',
'analytics.own',
'notifications.list',
'notifications.create',
'notifications.own',
'economy.list',
'economy.show',
],
// ─── Org manager: like org_admin without delete & member write ───────
'org_manager' => [
'dashboard',
'tournaments.list',
'tournaments.show',
'tournaments.create',
'tournaments.update',
'tournaments.start',
'tournaments.complete',
'tournaments.own',
'organizations.own',
'organizations.show',
'players.list',
'players.show',
'analytics.own',
'notifications.list',
'notifications.create',
'notifications.own',
'economy.list',
'economy.show',
],
// ─── Tournament organizer: manages own tournaments ───────────────────
'tournament_organizer' => [
'dashboard',
'tournaments.list',
'tournaments.show',
'tournaments.create',
'tournaments.update',
'tournaments.start',
'tournaments.complete',
'tournaments.own',
'players.list',
'players.show',
'analytics.own',
],
// ─── Sponsor: manages own ads & campaigns ────────────────────────────
'sponsor' => [
'dashboard',
'ads.list',
'ads.show',
'ads.create',
'ads.update',
'ads.own',
'analytics.own',
],
// ─── Charity: charity tournaments & economy view ─────────────────────
'charity' => [
'dashboard',
'tournaments.list',
'tournaments.show',
'tournaments.create',
'tournaments.update',
'tournaments.own',
'economy.list',
'economy.show',
'notifications.list',
'notifications.own',
],
// ─── Viewer: read-only access to most listing/detail views ───────────
'viewer' => [
'dashboard', 'players.list', 'players.show', 'games.list',
'tournaments.list', 'tournaments.show', 'organizations.list',
'analytics', 'audit-log'
'dashboard',
'players.list',
'players.show',
'games.list',
'games.show',
'tournaments.list',
'tournaments.show',
'organizations.list',
'organizations.show',
'economy.list',
'economy.show',
'ads.list',
'ads.show',
'analytics.list',
'analytics.show',
'audit-log.list',
'audit-log.show',
],
];
......@@ -6,6 +6,32 @@ return [
'login' => ['module' => 'auth', 'action' => 'login'],
'logout' => ['module' => 'auth', 'action' => 'logout'],
// Users Management
'users' => ['module' => 'users', 'action' => 'list'],
'users/create' => ['module' => 'users', 'action' => 'create'],
'users/store' => ['module' => 'users', 'action' => 'store'],
'users/bulk-action' => ['module' => 'users', 'action' => 'bulkAction'],
'users/{id}' => ['module' => 'users', 'action' => 'show'],
'users/{id}/edit' => ['module' => 'users', 'action' => 'edit'],
'users/{id}/update' => ['module' => 'users', 'action' => 'update'],
'users/{id}/toggle-status' => ['module' => 'users', 'action' => 'toggleStatus'],
'users/{id}/delete' => ['module' => 'users', 'action' => 'delete'],
'users/{id}/reset-password' => ['module' => 'users', 'action' => 'resetPassword'],
// Workflows & Approvals
'workflows' => ['module' => 'workflows', 'action' => 'list'],
'workflows/my-requests' => ['module' => 'workflows', 'action' => 'myRequests'],
'workflows/rules' => ['module' => 'workflows', 'action' => 'rules'],
'workflows/rules/create' => ['module' => 'workflows', 'action' => 'createRule'],
'workflows/rules/store' => ['module' => 'workflows', 'action' => 'storeRule'],
'workflows/rules/{id}/toggle' => ['module' => 'workflows', 'action' => 'toggleRule'],
'workflows/rules/{id}/delete' => ['module' => 'workflows', 'action' => 'deleteRule'],
'workflows/{id}' => ['module' => 'workflows', 'action' => 'show'],
'workflows/{id}/approve' => ['module' => 'workflows', 'action' => 'approve'],
'workflows/{id}/reject' => ['module' => 'workflows', 'action' => 'reject'],
'workflows/bulk-approve' => ['module' => 'workflows', 'action' => 'bulkApprove'],
// Players
'players' => ['module' => 'players', 'action' => 'list'],
'players/create' => ['module' => 'players', 'action' => 'create'],
'players/store' => ['module' => 'players', 'action' => 'store'],
......@@ -18,6 +44,7 @@ return [
'players/{id}/grant' => ['module' => 'players', 'action' => 'grant'],
'players/{id}/revoke' => ['module' => 'players', 'action' => 'revoke'],
// Games
'games' => ['module' => 'games', 'action' => 'list'],
'games/create' => ['module' => 'games', 'action' => 'create'],
'games/store' => ['module' => 'games', 'action' => 'store'],
......@@ -26,6 +53,7 @@ return [
'games/{id}/toggle' => ['module' => 'games', 'action' => 'toggle'],
'games/{id}/delete' => ['module' => 'games', 'action' => 'delete'],
// Chess Bots
'chess-bots' => ['module' => 'chess-bots', 'action' => 'list'],
'chess-bots/create' => ['module' => 'chess-bots', 'action' => 'create'],
'chess-bots/store' => ['module' => 'chess-bots', 'action' => 'store'],
......@@ -37,6 +65,7 @@ return [
'chess-bots/test-move' => ['module' => 'chess-bots', 'action' => 'testMove'],
'chess-bots/pool' => ['module' => 'chess-bots', 'action' => 'pool'],
// Tournaments
'tournaments' => ['module' => 'tournaments', 'action' => 'list'],
'tournaments/create' => ['module' => 'tournaments', 'action' => 'create'],
'tournaments/store' => ['module' => 'tournaments', 'action' => 'store'],
......@@ -50,6 +79,7 @@ return [
'tournaments/{id}/rounds/{roundId}/results' => ['module' => 'tournaments', 'action' => 'submitResults'],
'tournaments/{id}/standings' => ['module' => 'tournaments', 'action' => 'standings'],
// Organizations
'organizations' => ['module' => 'organizations', 'action' => 'list'],
'organizations/create' => ['module' => 'organizations', 'action' => 'create'],
'organizations/store' => ['module' => 'organizations', 'action' => 'store'],
......@@ -62,12 +92,14 @@ return [
'organizations/{id}/members/add' => ['module' => 'organizations', 'action' => 'addMember'],
'organizations/{id}/members/{memberId}/remove' => ['module' => 'organizations', 'action' => 'removeMember'],
// Economy
'economy' => ['module' => 'economy', 'action' => 'index'],
'economy/transactions' => ['module' => 'economy', 'action' => 'transactions'],
'economy/grant' => ['module' => 'economy', 'action' => 'grant'],
'economy/revoke' => ['module' => 'economy', 'action' => 'revoke'],
'economy/bulk-grant' => ['module' => 'economy', 'action' => 'bulkGrant'],
// Ads
'ads' => ['module' => 'ads', 'action' => 'list'],
'ads/create' => ['module' => 'ads', 'action' => 'create'],
'ads/store' => ['module' => 'ads', 'action' => 'store'],
......@@ -77,12 +109,14 @@ return [
'ads/{id}/toggle' => ['module' => 'ads', 'action' => 'toggle'],
'ads/{id}/delete' => ['module' => 'ads', 'action' => 'delete'],
// Moderation
'moderation' => ['module' => 'moderation', 'action' => 'list'],
'moderation/{id}' => ['module' => 'moderation', 'action' => 'show'],
'moderation/{id}/resolve' => ['module' => 'moderation', 'action' => 'resolve'],
'moderation/{id}/dismiss' => ['module' => 'moderation', 'action' => 'dismiss'],
'moderation/bulk-dismiss' => ['module' => 'moderation', 'action' => 'bulkDismiss'],
// Feature Flags
'feature-flags' => ['module' => 'feature-flags', 'action' => 'list'],
'feature-flags/create' => ['module' => 'feature-flags', 'action' => 'create'],
'feature-flags/store' => ['module' => 'feature-flags', 'action' => 'store'],
......@@ -91,24 +125,30 @@ return [
'feature-flags/{id}/toggle' => ['module' => 'feature-flags', 'action' => 'toggle'],
'feature-flags/{id}/delete' => ['module' => 'feature-flags', 'action' => 'delete'],
// Settings
'settings' => ['module' => 'settings', 'action' => 'index'],
'settings/update' => ['module' => 'settings', 'action' => 'update'],
// Branding
'branding' => ['module' => 'branding', 'action' => 'index'],
'branding/colors' => ['module' => 'branding', 'action' => 'colors'],
'branding/assets' => ['module' => 'branding', 'action' => 'assets'],
'branding/update-color' => ['module' => 'branding', 'action' => 'updateColor'],
'branding/upload-asset' => ['module' => 'branding', 'action' => 'uploadAsset'],
// Analytics
'analytics' => ['module' => 'analytics', 'action' => 'index'],
// Notifications
'notifications' => ['module' => 'notifications', 'action' => 'list'],
'notifications/send' => ['module' => 'notifications', 'action' => 'send'],
'notifications/broadcast' => ['module' => 'notifications', 'action' => 'broadcast'],
'notifications/{id}/delete' => ['module' => 'notifications', 'action' => 'delete'],
// Audit Log
'audit-log' => ['module' => 'audit-log', 'action' => 'index'],
'audit-log/export' => ['module' => 'audit-log', 'action' => 'export'],
// API
'api/health' => ['module' => 'api', 'action' => 'health'],
];
......@@ -2,6 +2,9 @@
class Auth
{
/**
* Check if the user is authenticated and session is still valid.
*/
public static function check(): bool
{
if (!isset($_SESSION['user'])) {
......@@ -15,21 +18,57 @@ class Auth
return true;
}
/**
* Attempt login: checks admin_users table first, falls back to hardcoded superadmin.
*/
public static function login(string $username, string $password): bool
{
// Try database user first
$dbUser = self::findDbUser($username);
if ($dbUser !== null) {
if (!$dbUser['is_active']) {
return false;
}
if (!password_verify($password, $dbUser['password_hash'])) {
return false;
}
$_SESSION['user'] = [
'id' => $dbUser['id'],
'username' => $dbUser['username'],
'display_name' => $dbUser['display_name'],
'role' => $dbUser['role'],
'org_id' => $dbUser['org_id'],
'email' => $dbUser['email'],
'source' => 'database',
];
$_SESSION['last_activity'] = time();
self::regenerateCsrf();
self::updateLastLogin($dbUser['id']);
return true;
}
// Fallback: hardcoded superadmin
if ($username === ADMIN_USERNAME && password_verify($password, ADMIN_PASSWORD_HASH)) {
$_SESSION['user'] = [
'id' => null,
'username' => $username,
'role' => 'superadmin',
'display_name' => 'المسؤول',
'role' => 'superadmin',
'org_id' => null,
'email' => null,
'source' => 'hardcoded',
];
$_SESSION['last_activity'] = time();
self::regenerateCsrf();
return true;
}
return false;
}
/**
* Destroy the current session.
*/
public static function logout(): void
{
$_SESSION = [];
......@@ -40,16 +79,59 @@ class Auth
session_destroy();
}
/**
* Get the current authenticated user array.
*/
public static function user(): ?array
{
return $_SESSION['user'] ?? null;
}
/**
* Get the current user's role.
*/
public static function role(): string
{
return $_SESSION['user']['role'] ?? 'viewer';
}
/**
* Get the current user's org_id (null for global admins).
*/
public static function orgId(): ?string
{
return $_SESSION['user']['org_id'] ?? null;
}
/**
* Returns true if the current user is scoped to a specific organization.
*/
public static function isOrgScoped(): bool
{
return self::orgId() !== null;
}
/**
* Check if the current user can access a given organization's data.
* superadmin/admin roles can access any org. Org-scoped users can only access their own.
*/
public static function canAccessOrg(?string $orgId): bool
{
$role = self::role();
// Global roles can access everything
if (in_array($role, ['superadmin', 'admin'], true)) {
return true;
}
// Org-scoped users can only access their own org
if ($orgId === null) {
return false;
}
return self::orgId() === $orgId;
}
/**
* Require authentication or redirect to login.
*/
public static function requireAuth(): void
{
if (!self::check()) {
......@@ -58,10 +140,23 @@ class Auth
}
}
/**
* Require a minimum role level.
*/
public static function requireRole(string $minRole): void
{
self::requireAuth();
$levels = ['viewer' => 10, 'moderator' => 50, 'admin' => 80, 'superadmin' => 100];
$levels = [
'viewer' => 10,
'charity' => 20,
'sponsor' => 25,
'tournament_organizer' => 30,
'org_manager' => 40,
'org_admin' => 50,
'moderator' => 60,
'admin' => 80,
'superadmin' => 100,
];
$userLevel = $levels[self::role()] ?? 0;
$requiredLevel = $levels[$minRole] ?? 100;
......@@ -72,18 +167,56 @@ class Auth
}
}
/**
* Check if the current user has a specific permission.
* Evaluates wildcard, module-level, module.action, and module.* patterns.
*/
public static function hasPermission(string $permission): bool
{
$permissions = require BASE_PATH . '/config/permissions.php';
$rolePerms = $permissions[self::role()] ?? [];
if (in_array('*', $rolePerms)) return true;
if (in_array($permission, $rolePerms)) return true;
// Wildcard: full access
if (in_array('*', $rolePerms, true)) {
return true;
}
// Exact match
if (in_array($permission, $rolePerms, true)) {
return true;
}
// Module-level match: if user has "players" they get all players.* actions
$module = explode('.', $permission)[0];
return in_array($module, $rolePerms);
if (in_array($module, $rolePerms, true)) {
return true;
}
// Wildcard module match: "players.*" grants any players.X
if (in_array($module . '.*', $rolePerms, true)) {
return true;
}
return false;
}
/**
* Require a specific permission. Halts execution with 403 if not granted.
*/
public static function requirePermission(string $permission): void
{
self::requireAuth();
if (!self::hasPermission($permission)) {
http_response_code(403);
View::render('errors/403');
exit;
}
}
/**
* Get the CSRF token for the current session.
*/
public static function csrfToken(): string
{
if (!isset($_SESSION['csrf_token'])) {
......@@ -92,12 +225,18 @@ class Auth
return $_SESSION['csrf_token'];
}
/**
* Validate the submitted CSRF token.
*/
public static function validateCsrf(): bool
{
$token = $_POST['_csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
return hash_equals(self::csrfToken(), $token);
}
/**
* Require a valid CSRF token or halt with 403.
*/
public static function requireCsrf(): void
{
if (!self::validateCsrf()) {
......@@ -107,6 +246,43 @@ class Auth
}
}
// ─── Private Helpers ─────────────────────────────────────────────────
/**
* Find a user in the admin_users table by username.
*/
private static function findDbUser(string $username): ?array
{
try {
$db = Database::getInstance();
$result = $db->selectOne('admin_users', [
'username' => 'eq.' . $username,
]);
return $result;
} catch (\Throwable $e) {
// If the table doesn't exist or DB is unreachable, silently fall through
return null;
}
}
/**
* Update the last_login timestamp for a database user.
*/
private static function updateLastLogin(string $userId): void
{
try {
$db = Database::getInstance();
$db->update('admin_users', ['id' => 'eq.' . $userId], [
'last_login' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical — don't break login if this fails
}
}
/**
* Regenerate the CSRF token.
*/
private static function regenerateCsrf(): void
{
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
......
This diff is collapsed.
This diff is collapsed.
<?php
$roleLabels = [
'superadmin' => 'مسؤول أعلى',
'admin' => 'مسؤول',
'moderator' => 'مشرف',
'org_admin' => 'مدير منظمة',
'org_manager' => 'مدير عمليات',
'tournament_organizer' => 'منظم بطولات',
'sponsor' => 'راعي',
'charity' => 'جمعية خيرية',
'viewer' => 'مشاهد',
];
$roleBadgeColors = [
'superadmin' => 'purple',
'admin' => 'info',
'moderator' => 'warning',
'org_admin' => 'success',
'org_manager' => 'info',
'tournament_organizer' => 'warning',
'sponsor' => 'purple',
'charity' => 'success',
'viewer' => 'secondary',
];
$currentRole = Auth::role();
?>
<header class="topbar">
<div class="topbar-right">
<button class="mobile-toggle" onclick="toggleSidebar()">
......@@ -14,7 +39,10 @@
<div class="topbar-avatar">
<?= mb_substr(Auth::user()['display_name'] ?? 'م', 0, 1) ?>
</div>
<span class="hide-mobile"><?= View::e(Auth::user()['display_name'] ?? 'المسؤول') ?></span>
<div class="hide-mobile" style="display: flex; flex-direction: column; align-items: flex-start; gap: 2px;">
<span style="font-size: var(--font-size-sm); font-weight: var(--font-weight-medium);"><?= View::e(Auth::user()['display_name'] ?? 'المسؤول') ?></span>
<span class="badge badge-<?= $roleBadgeColors[$currentRole] ?? 'secondary' ?>" style="font-size: 10px; padding: 1px 6px;"><?= $roleLabels[$currentRole] ?? $currentRole ?></span>
</div>
</div>
</div>
</header>
<div class="auth-logo">E</div>
<h1 class="auth-title">مرحباً بك</h1>
<p class="auth-subtitle">سجل دخولك للوصول إلى لوحة التحكم</p>
<p class="auth-subtitle">سجل دخولك للوصول إلى لوحة الإدارة</p>
<?php if (!empty($error)): ?>
<div class="auth-error"><?= View::e($error) ?></div>
......@@ -8,8 +8,8 @@
<form method="POST" action="/login" data-validate>
<div class="form-group">
<label class="form-label">اسم المستخدم</label>
<input type="text" name="username" class="form-input" placeholder="admin" required autofocus>
<label class="form-label">اسم المستخدم أو البريد الإلكتروني</label>
<input type="text" name="username" class="form-input" placeholder="admin" required autofocus value="<?= View::e($_POST['username'] ?? '') ?>">
<span class="form-error"></span>
</div>
......@@ -19,8 +19,17 @@
<span class="form-error"></span>
</div>
<div class="form-group" style="display: flex; align-items: center; gap: var(--space-2);">
<input type="checkbox" name="remember" id="remember" style="width: auto;">
<label for="remember" class="form-label" style="margin: 0; font-size: var(--font-size-sm);">تذكرني</label>
</div>
<button type="submit" class="btn btn-primary btn-lg w-full" style="margin-top: var(--space-4);">
<span class="btn-text">تسجيل الدخول</span>
<span class="btn-spinner"></span>
</button>
</form>
<div style="margin-top: var(--space-5); text-align: center;">
<p class="text-xs text-muted">نظام إدارة منصة El3ab — جميع الأدوار مدعومة</p>
</div>
......@@ -5,7 +5,53 @@ class DashboardController
public function index(array $params, string $method): void
{
$db = Database::getInstance();
$role = Auth::role();
$orgId = Auth::orgId();
$pageTitle = 'لوحة التحكم';
$moduleCSS = 'dashboard';
$moduleJS = 'dashboard';
$data = compact('pageTitle', 'moduleCSS', 'moduleJS', 'role');
switch ($role) {
case 'superadmin':
case 'admin':
$data = array_merge($data, $this->getAdminData($db));
break;
case 'moderator':
$data = array_merge($data, $this->getModeratorData($db));
break;
case 'org_admin':
case 'org_manager':
$data = array_merge($data, $this->getOrgData($db, $orgId));
break;
case 'tournament_organizer':
$data = array_merge($data, $this->getTournamentOrganizerData($db));
break;
case 'sponsor':
$data = array_merge($data, $this->getSponsorData($db));
break;
case 'charity':
$data = array_merge($data, $this->getCharityData($db));
break;
case 'viewer':
default:
$data = array_merge($data, $this->getViewerData($db));
break;
}
View::render('dashboard/index', $data);
}
private function getAdminData(Database $db): array
{
$totalPlayers = $db->count('profiles', []);
$onlinePlayers = $db->count('profiles', ['is_online' => 'eq.true']);
......@@ -15,6 +61,9 @@ class DashboardController
$activeTournaments = $db->count('el3ab_tournaments', ['status' => 'eq.in_progress']);
$pendingReports = $db->count('cheat_reports', ['status' => 'eq.pending']);
$totalAdminUsers = $db->count('admin_users', []);
$pendingApprovals = $db->count('admin_users', ['status' => 'eq.pending']);
$recentActivity = $db->select('audit_log', [
'select' => '*',
'order' => 'created_at.desc',
......@@ -29,15 +78,167 @@ class DashboardController
$stockfishHealth = ApiProxy::healthCheck(STOCKFISH_API_URL . '/health');
$swissHealth = ApiProxy::healthCheck(SWISS_API_URL . '/health');
$pageTitle = 'لوحة التحكم';
$moduleCSS = 'dashboard';
$moduleJS = 'dashboard';
View::render('dashboard/index', compact(
return compact(
'totalPlayers', 'onlinePlayers', 'totalMatches', 'activeMatches',
'activeTournaments', 'pendingReports', 'recentActivity',
'supabaseHealth', 'stockfishHealth', 'swissHealth',
'pageTitle', 'moduleCSS', 'moduleJS'
));
'activeTournaments', 'pendingReports', 'totalAdminUsers', 'pendingApprovals',
'recentActivity', 'supabaseHealth', 'stockfishHealth', 'swissHealth'
);
}
private function getModeratorData(Database $db): array
{
$pendingReports = $db->count('cheat_reports', ['status' => 'eq.pending']);
$resolvedReports = $db->count('cheat_reports', ['status' => 'eq.resolved']);
$totalBans = $db->count('profiles', ['is_banned' => 'eq.true']);
$recentReports = $db->select('cheat_reports', [
'select' => '*',
'order' => 'created_at.desc',
'limit' => 10,
]);
$recentActions = $db->select('audit_log', [
'select' => '*',
'actor' => 'eq.' . Auth::user()['username'],
'order' => 'created_at.desc',
'limit' => 10,
]);
return compact(
'pendingReports', 'resolvedReports', 'totalBans',
'recentReports', 'recentActions'
);
}
private function getOrgData(Database $db, ?string $orgId): array
{
$orgFilters = ['org_id' => 'eq.' . $orgId];
$orgInfo = $db->selectOne('organizations', ['id' => 'eq.' . $orgId]);
$membersCount = $db->count('org_members', $orgFilters);
$tournamentsCount = $db->count('el3ab_tournaments', $orgFilters);
$activeMatches = $db->count('matches', array_merge($orgFilters, ['status' => 'eq.in_progress']));
$recentActivity = $db->select('audit_log', [
'select' => '*',
'org_id' => 'eq.' . $orgId,
'order' => 'created_at.desc',
'limit' => 10,
]);
return compact(
'orgInfo', 'membersCount', 'tournamentsCount', 'activeMatches', 'recentActivity'
);
}
private function getTournamentOrganizerData(Database $db): array
{
$userId = Auth::user()['username'];
$activeTournaments = $db->count('el3ab_tournaments', [
'organizer_id' => 'eq.' . $userId,
'status' => 'eq.in_progress',
]);
$completedTournaments = $db->count('el3ab_tournaments', [
'organizer_id' => 'eq.' . $userId,
'status' => 'eq.completed',
]);
$totalTournaments = $db->count('el3ab_tournaments', [
'organizer_id' => 'eq.' . $userId,
]);
$myTournaments = $db->select('el3ab_tournaments', [
'select' => '*',
'organizer_id' => 'eq.' . $userId,
'order' => 'created_at.desc',
'limit' => 5,
]);
$totalParticipants = 0;
foreach ($myTournaments as $t) {
$totalParticipants += (int) ($t['participants_count'] ?? 0);
}
return compact(
'activeTournaments', 'completedTournaments', 'totalTournaments',
'totalParticipants', 'myTournaments'
);
}
private function getSponsorData(Database $db): array
{
$userId = Auth::user()['username'];
$activeCampaigns = $db->count('ad_campaigns', [
'sponsor_id' => 'eq.' . $userId,
'status' => 'eq.active',
]);
$totalCampaigns = $db->count('ad_campaigns', [
'sponsor_id' => 'eq.' . $userId,
]);
$campaigns = $db->select('ad_campaigns', [
'select' => '*',
'sponsor_id' => 'eq.' . $userId,
'order' => 'created_at.desc',
'limit' => 10,
]);
$totalImpressions = 0;
$totalSpend = 0;
foreach ($campaigns as $c) {
$totalImpressions += (int) ($c['impressions'] ?? 0);
$totalSpend += (float) ($c['spend'] ?? 0);
}
return compact(
'activeCampaigns', 'totalCampaigns', 'totalImpressions', 'totalSpend', 'campaigns'
);
}
private function getCharityData(Database $db): array
{
$userId = Auth::user()['username'];
$charityTournaments = $db->count('el3ab_tournaments', [
'charity_id' => 'eq.' . $userId,
]);
$activeTournaments = $db->count('el3ab_tournaments', [
'charity_id' => 'eq.' . $userId,
'status' => 'eq.in_progress',
]);
$tournaments = $db->select('el3ab_tournaments', [
'select' => '*',
'charity_id' => 'eq.' . $userId,
'order' => 'created_at.desc',
'limit' => 10,
]);
$totalDonations = 0;
$totalParticipants = 0;
foreach ($tournaments as $t) {
$totalDonations += (float) ($t['donation_amount'] ?? 0);
$totalParticipants += (int) ($t['participants_count'] ?? 0);
}
return compact(
'charityTournaments', 'activeTournaments', 'totalDonations',
'totalParticipants', 'tournaments'
);
}
private function getViewerData(Database $db): array
{
$totalPlayers = $db->count('profiles', []);
$onlinePlayers = $db->count('profiles', ['is_online' => 'eq.true']);
$totalMatches = $db->count('matches', []);
$activeTournaments = $db->count('el3ab_tournaments', ['status' => 'eq.in_progress']);
return compact('totalPlayers', 'onlinePlayers', 'totalMatches', 'activeTournaments');
}
}
This diff is collapsed.
/* Users module specific styles */
/* Role badge colors */
.badge-purple {
background: rgba(139, 92, 246, 0.15);
color: #a78bfa;
}
.badge-blue {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
}
.badge-orange {
background: rgba(249, 115, 22, 0.15);
color: #fb923c;
}
.badge-green {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.badge-cyan {
background: rgba(6, 182, 212, 0.15);
color: #22d3ee;
}
.badge-gold {
background: rgba(234, 179, 8, 0.15);
color: #facc15;
}
.badge-pink {
background: rgba(236, 72, 153, 0.15);
color: #f472b6;
}
.badge-teal {
background: rgba(20, 184, 166, 0.15);
color: #2dd4bf;
}
/* Input group for password field */
.input-group {
display: flex;
align-items: center;
gap: var(--space-2);
}
.input-group .form-input {
flex: 1;
}
/* User profile card avatar */
.avatar-xl {
width: 80px;
height: 80px;
font-size: 2rem;
}
/* Organization link */
.text-link {
color: var(--color-primary);
text-decoration: none;
transition: opacity 0.2s;
}
.text-link:hover {
opacity: 0.8;
text-decoration: underline;
}
/* Filter pills scrollable on mobile */
.filter-pills {
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: var(--space-2);
}
.filter-pills::-webkit-scrollbar {
display: none;
}
/* Password generated display */
.generated-password {
font-family: 'IBM Plex Mono', monospace;
background: var(--color-surface-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
font-size: 0.875rem;
letter-spacing: 0.05em;
direction: ltr;
text-align: left;
user-select: all;
}
/* Change role modal */
.role-option {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.role-option:hover {
border-color: var(--color-primary);
background: var(--color-surface-elevated);
}
.role-option input[type="radio"] {
accent-color: var(--color-primary);
}
.role-option.selected {
border-color: var(--color-primary);
background: rgba(var(--color-primary-rgb), 0.05);
}
// Users module JavaScript
// Role-dependent organization field visibility
const roleSelect = document.getElementById('roleSelect');
const orgField = document.getElementById('orgField');
const orgRoles = ['org_admin', 'org_manager', 'charity', 'sponsor'];
if (roleSelect && orgField) {
roleSelect.addEventListener('change', function () {
if (orgRoles.includes(this.value)) {
orgField.classList.remove('hidden');
} else {
orgField.classList.add('hidden');
// Clear org selection when not needed
const orgSelect = document.getElementById('orgSelect');
if (orgSelect) orgSelect.value = '';
}
});
}
// Toggle password visibility
function togglePasswordVisibility() {
const input = document.getElementById('passwordInput');
const icon = document.getElementById('eyeIcon');
if (input.type === 'password') {
input.type = 'text';
icon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/>';
} else {
input.type = 'password';
icon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';
}
}
// Generate random password
function generatePassword() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%';
let password = '';
for (let i = 0; i < 12; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
const input = document.getElementById('passwordInput');
if (input) {
input.type = 'text';
input.value = password;
}
}
// Toggle user status from list
function toggleUserStatus(id, name, isActive) {
const action = isActive ? 'تعطيل' : 'تفعيل';
const message = `هل تريد ${action} المستخدم "${name}"؟`;
showConfirm(message, '', () => {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/users/${id}/toggle-status`;
form.innerHTML = `<input type="hidden" name="_csrf" value="${CSRF_TOKEN}">`;
document.body.appendChild(form);
form.submit();
}, action);
}
// Reset password from list
function resetUserPassword(id, name) {
showConfirm(
`إعادة تعيين كلمة مرور "${name}"`,
'سيتم إنشاء كلمة مرور مؤقتة جديدة',
() => {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/users/${id}/reset-password`;
form.innerHTML = `<input type="hidden" name="_csrf" value="${CSRF_TOKEN}">`;
document.body.appendChild(form);
form.submit();
},
'إعادة تعيين'
);
}
// Change role modal
function changeRoleModal() {
const userId = window.location.pathname.split('/')[2];
const roles = {
'superadmin': 'مسؤول أعلى',
'admin': 'مسؤول',
'moderator': 'مشرف',
'org_admin': 'مسؤول منظمة',
'org_manager': 'مدير منظمة',
'tournament_organizer': 'منظم بطولات',
'sponsor': 'راعي',
'charity': 'جمعية خيرية',
'viewer': 'مشاهد',
};
let rolesHtml = '<form method="POST" action="/users/' + userId + '/update" id="changeRoleForm">';
rolesHtml += `<input type="hidden" name="_csrf" value="${CSRF_TOKEN}">`;
// Keep existing data
rolesHtml += '<input type="hidden" name="display_name" value="">';
rolesHtml += '<input type="hidden" name="email" value="">';
rolesHtml += '<div class="flex flex-col gap-2">';
for (const [key, label] of Object.entries(roles)) {
rolesHtml += `
<label class="role-option">
<input type="radio" name="role" value="${key}">
<span>${label}</span>
</label>
`;
}
rolesHtml += '</div></form>';
openModal('تغيير الدور', rolesHtml, `
<button class="btn btn-ghost" onclick="closeModal()">إلغاء</button>
<button class="btn btn-primary" onclick="document.getElementById('changeRoleForm').submit()">حفظ</button>
`);
}
// Bulk activate
function bulkActivate() {
const ids = getSelectedIds();
if (!ids.length) return;
showConfirm(`هل تريد تفعيل ${ids.length} مستخدم؟`, '', () => {
submitBulkAction('activate', ids);
}, 'تفعيل');
}
// Bulk deactivate
function bulkDeactivate() {
const ids = getSelectedIds();
if (!ids.length) return;
showConfirm(`هل تريد تعطيل ${ids.length} مستخدم؟`, '', () => {
submitBulkAction('deactivate', ids);
}, 'تعطيل');
}
function submitBulkAction(action, ids) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/users/bulk-action';
form.innerHTML = `
<input type="hidden" name="_csrf" value="${CSRF_TOKEN}">
<input type="hidden" name="action" value="${action}">
<input type="hidden" name="ids" value="${ids.join(',')}">
`;
document.body.appendChild(form);
form.submit();
}
function getSelectedIds() {
return Array.from(document.querySelectorAll('.row-check:checked')).map(cb => cb.value);
}
// Checkbox bulk selection logic
const checkAll = document.querySelector('.check-all');
const bulkActionsBar = document.getElementById('bulkActions');
if (checkAll) {
checkAll.addEventListener('change', function () {
document.querySelectorAll('.row-check').forEach(cb => {
cb.checked = this.checked;
});
updateBulkBar();
});
document.querySelectorAll('.row-check').forEach(cb => {
cb.addEventListener('change', updateBulkBar);
});
}
function updateBulkBar() {
const checked = document.querySelectorAll('.row-check:checked').length;
if (bulkActionsBar) {
if (checked > 0) {
bulkActionsBar.classList.remove('hidden');
bulkActionsBar.querySelector('.bulk-count').textContent = checked;
} else {
bulkActionsBar.classList.add('hidden');
}
}
}
// Filter by status
function filterByStatus(value) {
const url = new URL(window.location);
if (value) {
url.searchParams.set('status', value);
} else {
url.searchParams.delete('status');
}
url.searchParams.set('page', '1');
window.location.href = url.toString();
}
// Filter by organization
function filterByOrg(value) {
const url = new URL(window.location);
if (value) {
url.searchParams.set('org', value);
} else {
url.searchParams.delete('org');
}
url.searchParams.set('page', '1');
window.location.href = url.toString();
}
// Search with debounce
const searchInput = document.getElementById('userSearch');
if (searchInput) {
let timeout;
searchInput.addEventListener('input', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL(window.location);
if (searchInput.value) {
url.searchParams.set('search', searchInput.value);
} else {
url.searchParams.delete('search');
}
url.searchParams.set('page', '1');
window.location.href = url.toString();
}, 300);
});
}
This diff is collapsed.
<?php
$isEdit = !empty($user['id']);
$roleLabels = [
'superadmin' => 'مسؤول أعلى',
'admin' => 'مسؤول',
'moderator' => 'مشرف',
'org_admin' => 'مسؤول منظمة',
'org_manager' => 'مدير منظمة',
'tournament_organizer' => 'منظم بطولات',
'sponsor' => 'راعي',
'charity' => 'جمعية خيرية',
'viewer' => 'مشاهد',
];
// Roles that require organization selection
$orgRoles = ['org_admin', 'org_manager', 'charity', 'sponsor'];
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/users" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= $isEdit ? 'تعديل المستخدم' : 'إضافة مستخدم' ?></h1>
</div>
</div>
<div class="card max-w-lg">
<form method="POST" action="<?= $isEdit ? "/users/{$user['id']}/update" : '/users/store' ?>" data-validate enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<?php if (!$isEdit): ?>
<div class="form-group">
<label class="form-label">اسم المستخدم *</label>
<input type="text" name="username" class="form-input" value="<?= View::e($user['username'] ?? '') ?>" required minlength="3" maxlength="30" placeholder="username" dir="ltr">
<span class="form-hint">يستخدم لتسجيل الدخول - أحرف إنجليزية وأرقام فقط</span>
<span class="form-error"></span>
</div>
<?php endif; ?>
<div class="form-group">
<label class="form-label">الاسم المعروض *</label>
<input type="text" name="display_name" class="form-input" value="<?= View::e($user['display_name'] ?? '') ?>" required>
<span class="form-error"></span>
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني *</label>
<input type="email" name="email" class="form-input" value="<?= View::e($user['email'] ?? '') ?>" required dir="ltr">
<span class="form-error"></span>
</div>
<?php if (!$isEdit): ?>
<div class="form-group">
<label class="form-label">كلمة المرور *</label>
<div class="input-group">
<input type="password" name="password" class="form-input" required minlength="8" id="passwordInput" dir="ltr">
<button type="button" class="btn btn-ghost btn-sm" onclick="togglePasswordVisibility()" title="إظهار/إخفاء">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="eyeIcon"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
<span class="form-hint">8 أحرف على الأقل</span>
<span class="form-error"></span>
<button type="button" class="btn btn-ghost btn-sm mt-2" onclick="generatePassword()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
توليد كلمة مرور
</button>
</div>
<?php endif; ?>
<div class="form-group">
<label class="form-label">الدور *</label>
<select name="role" class="form-select" id="roleSelect" required>
<option value="">اختر الدور...</option>
<?php foreach ($roleLabels as $roleKey => $roleLabel): ?>
<option value="<?= $roleKey ?>" <?= ($user['role'] ?? '') === $roleKey ? 'selected' : '' ?>><?= $roleLabel ?></option>
<?php endforeach; ?>
</select>
<span class="form-error"></span>
</div>
<div class="form-group <?= !in_array($user['role'] ?? '', $orgRoles) ? 'hidden' : '' ?>" id="orgField">
<label class="form-label">المنظمة</label>
<select name="org_id" class="form-select" id="orgSelect">
<option value="">بدون منظمة</option>
<?php foreach ($organizations as $orgItem): ?>
<option value="<?= $orgItem['id'] ?>" <?= ($user['org_id'] ?? '') === $orgItem['id'] ? 'selected' : '' ?>><?= View::e($orgItem['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">رابط الصورة الشخصية</label>
<input type="url" name="avatar_url" class="form-input" value="<?= View::e($user['avatar_url'] ?? '') ?>" placeholder="https://..." dir="ltr">
<span class="form-hint">رابط مباشر لصورة (PNG, JPG)</span>
</div>
<div class="form-group">
<label class="form-label flex items-center gap-3">
<input type="checkbox" name="is_active" class="form-checkbox" <?= ($user['is_active'] ?? true) ? 'checked' : '' ?>>
<span>الحساب نشط</span>
</label>
<span class="form-hint">المستخدم المعطل لا يمكنه تسجيل الدخول</span>
</div>
<div class="flex gap-3 mt-6">
<button type="submit" class="btn btn-primary">
<span class="btn-text"><?= $isEdit ? 'حفظ التعديلات' : 'إنشاء المستخدم' ?></span>
<span class="btn-spinner"></span>
</button>
<a href="/users" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
This diff is collapsed.
<?php
$roleLabels = [
'superadmin' => 'مسؤول أعلى',
'admin' => 'مسؤول',
'moderator' => 'مشرف',
'org_admin' => 'مسؤول منظمة',
'org_manager' => 'مدير منظمة',
'tournament_organizer' => 'منظم بطولات',
'sponsor' => 'راعي',
'charity' => 'جمعية خيرية',
'viewer' => 'مشاهد',
];
$roleBadgeColors = [
'superadmin' => 'purple',
'admin' => 'blue',
'moderator' => 'orange',
'org_admin' => 'green',
'org_manager' => 'cyan',
'tournament_organizer' => 'gold',
'sponsor' => 'pink',
'charity' => 'teal',
'viewer' => 'default',
];
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/users" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= View::e($user['display_name'] ?? $user['username']) ?></h1>
<span class="badge badge-<?= $roleBadgeColors[$user['role']] ?? 'default' ?>">
<?= $roleLabels[$user['role']] ?? $user['role'] ?>
</span>
<?php if (!($user['is_active'] ?? false)): ?>
<span class="badge badge-danger">معطل</span>
<?php endif; ?>
</div>
<div class="flex gap-3">
<a href="/users/<?= $user['id'] ?>/edit" class="btn btn-ghost">تعديل</a>
</div>
</div>
<!-- User Profile Card & Stats -->
<div class="grid grid-3 mb-6">
<div class="card" style="grid-column: span 1;">
<div class="flex flex-col items-center text-center p-5">
<div class="avatar avatar-xl mb-4">
<?php if (!empty($user['avatar_url'])): ?>
<img src="<?= View::e($user['avatar_url']) ?>" alt="">
<?php else: ?>
<?= mb_substr($user['display_name'] ?? $user['username'] ?? '?', 0, 1) ?>
<?php endif; ?>
</div>
<h3 class="text-lg font-semibold"><?= View::e($user['display_name'] ?? $user['username']) ?></h3>
<p class="text-secondary text-sm">@<?= View::e($user['username']) ?></p>
<span class="badge badge-<?= $roleBadgeColors[$user['role']] ?? 'default' ?> mt-2">
<?= $roleLabels[$user['role']] ?? $user['role'] ?>
</span>
<?php if ($organization): ?>
<a href="/organizations/<?= $organization['id'] ?>" class="text-sm text-link mt-2">
<?= View::e($organization['name']) ?>
</a>
<?php endif; ?>
</div>
</div>
<div class="card" style="grid-column: span 2;">
<div class="card-header">
<h3 class="card-title">معلومات المستخدم</h3>
</div>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between"><span class="text-secondary">البريد الإلكتروني</span><span dir="ltr"><?= View::e($user['email'] ?? '-') ?></span></div>
<div class="flex justify-between"><span class="text-secondary">الحالة</span>
<?php if ($user['is_active'] ?? false): ?>
<span class="badge badge-success badge-dot">نشط</span>
<?php else: ?>
<span class="badge badge-danger badge-dot">معطل</span>
<?php endif; ?>
</div>
<div class="flex justify-between"><span class="text-secondary">تاريخ الإنشاء</span><span class="tabular-nums"><?= !empty($user['created_at']) ? date('Y-m-d H:i', strtotime($user['created_at'])) : '-' ?></span></div>
<div class="flex justify-between"><span class="text-secondary">أنشئ بواسطة</span><span><?= View::e($user['created_by'] ?? '-') ?></span></div>
<div class="flex justify-between"><span class="text-secondary">آخر دخول</span><span class="tabular-nums"><?= !empty($user['last_login']) ? date('Y-m-d H:i', strtotime($user['last_login'])) : 'لم يسجل الدخول بعد' ?></span></div>
</div>
<!-- Stats -->
<div class="grid grid-3 gap-4 mt-5">
<div class="p-3 rounded bg-primary text-center">
<div class="text-xl font-bold tabular-nums"><?= number_format($actionsCount) ?></div>
<div class="text-xs text-muted">إجراء</div>
</div>
<div class="p-3 rounded bg-primary text-center">
<div class="text-xl font-bold tabular-nums"><?= !empty($user['last_login']) ? date('m/d', strtotime($user['last_login'])) : '-' ?></div>
<div class="text-xs text-muted">آخر نشاط</div>
</div>
<div class="p-3 rounded bg-primary text-center">
<div class="text-xl font-bold tabular-nums">
<?php if (!empty($user['created_at'])): ?>
<?= (int)((time() - strtotime($user['created_at'])) / 86400) ?>
<?php else: ?>
-
<?php endif; ?>
</div>
<div class="text-xs text-muted">يوم منذ الانضمام</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card mb-6">
<div class="card-header">
<h3 class="card-title">إجراءات سريعة</h3>
</div>
<div class="flex gap-3 flex-wrap">
<form method="POST" action="/users/<?= $user['id'] ?>/reset-password" style="display:inline;" onsubmit="return confirm('هل تريد إعادة تعيين كلمة المرور؟')">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-ghost btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
إعادة تعيين كلمة المرور
</button>
</form>
<form method="POST" action="/users/<?= $user['id'] ?>/toggle-status" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<?php if ($user['is_active'] ?? false): ?>
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('هل تريد تعطيل هذا المستخدم؟')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
تعطيل الحساب
</button>
<?php else: ?>
<button type="submit" class="btn btn-success btn-sm" onclick="return confirm('هل تريد تفعيل هذا المستخدم؟')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
تفعيل الحساب
</button>
<?php endif; ?>
</form>
<button class="btn btn-ghost btn-sm" onclick="changeRoleModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
تغيير الدور
</button>
</div>
</div>
<!-- Activity Log Tab -->
<div class="card">
<div class="card-header">
<h3 class="card-title">سجل النشاط</h3>
<span class="text-sm text-secondary"><?= number_format($actionsCount) ?> إجراء</span>
</div>
<?php if (empty($activities)): ?>
<p class="text-secondary text-center p-5">لا يوجد نشاط مسجل</p>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>الإجراء</th>
<th>النوع</th>
<th>المعرف</th>
<th>العنوان IP</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($activities as $activity): ?>
<tr>
<td><span class="badge badge-info"><?= View::e($activity['action']) ?></span></td>
<td class="text-sm"><?= View::e($activity['entity_type'] ?? '-') ?></td>
<td class="text-xs text-muted tabular-nums"><?= View::e(substr($activity['entity_id'] ?? '-', 0, 8)) ?></td>
<td class="text-xs text-muted tabular-nums" dir="ltr" style="text-align: right;"><?= View::e($activity['ip_address'] ?? '-') ?></td>
<td class="text-xs text-muted tabular-nums"><?= !empty($activity['created_at']) ? date('Y-m-d H:i', strtotime($activity['created_at'])) : '-' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
This diff is collapsed.
// Workflows module JS
// ===== Modal Management =====
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.add('active');
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.remove('active');
}
// Close modals on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal.active').forEach(function(modal) {
modal.classList.remove('active');
});
}
});
// ===== Approve/Reject from list =====
function quickApprove(requestId) {
const form = document.getElementById('approveForm');
if (form) {
form.action = '/workflows/' + requestId + '/approve';
showModal('approveModal');
}
}
function showRejectModal(requestId) {
const form = document.getElementById('rejectForm');
if (form) {
form.action = '/workflows/' + requestId + '/reject';
showModal('rejectModal');
}
}
// ===== Bulk Selection =====
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.row-checkbox');
checkboxes.forEach(function(cb) {
cb.checked = checkbox.checked;
});
updateBulkBtn();
}
function updateBulkBtn() {
const checked = document.querySelectorAll('.row-checkbox:checked');
const bulkBtn = document.getElementById('bulkApproveBtn');
if (bulkBtn) {
bulkBtn.style.display = checked.length > 0 ? 'inline-flex' : 'none';
}
}
function bulkApprove() {
const checked = document.querySelectorAll('.row-checkbox:checked');
const ids = Array.from(checked).map(function(cb) { return cb.value; });
const bulkIds = document.getElementById('bulkIds');
const bulkCount = document.getElementById('bulkCount');
if (bulkIds) bulkIds.value = ids.join(',');
if (bulkCount) bulkCount.textContent = ids.length;
showModal('bulkApproveModal');
}
// ===== Conditions Builder =====
let conditionIndex = document.querySelectorAll('.condition-row').length;
function addCondition() {
const container = document.getElementById('conditionsContainer');
if (!container) return;
const row = document.createElement('div');
row.className = 'condition-row';
row.dataset.index = conditionIndex;
row.innerHTML = `
<select name="condition_field[]" class="form-select condition-field">
<option value="">الحقل...</option>
<option value="type">النوع</option>
<option value="requester_role">دور مقدم الطلب</option>
<option value="amount">المبلغ</option>
<option value="org_id">المنظمة</option>
<option value="max_players">أقصى لاعبين</option>
<option value="budget">الميزانية</option>
</select>
<select name="condition_operator[]" class="form-select condition-operator">
<option value="eq">يساوي</option>
<option value="neq">لا يساوي</option>
<option value="gt">أكبر من</option>
<option value="gte">أكبر من أو يساوي</option>
<option value="lt">أقل من</option>
<option value="lte">أقل من أو يساوي</option>
<option value="contains">يحتوي على</option>
<option value="starts_with">يبدأ بـ</option>
</select>
<input type="text" name="condition_value[]" class="form-input condition-value" placeholder="القيمة">
<button type="button" class="btn btn-icon btn-ghost btn-danger-hover" onclick="removeCondition(this)" title="حذف الشرط">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
`;
container.appendChild(row);
conditionIndex++;
}
function removeCondition(btn) {
const row = btn.closest('.condition-row');
if (row) {
row.style.opacity = '0';
row.style.transform = 'translateX(20px)';
row.style.transition = 'opacity 0.2s, transform 0.2s';
setTimeout(function() {
row.remove();
}, 200);
}
}
// ===== Search with debounce =====
(function() {
const searchInput = document.getElementById('workflowSearch');
if (!searchInput) return;
let timeout;
searchInput.addEventListener('input', function() {
clearTimeout(timeout);
const value = this.value;
timeout = setTimeout(function() {
const url = new URL(window.location.href);
if (value) {
url.searchParams.set('search', value);
} else {
url.searchParams.delete('search');
}
url.searchParams.set('page', '1');
window.location.href = url.toString();
}, 500);
});
})();
This diff is collapsed.
This diff is collapsed.
<?php
$typeLabels = [
'tournament_create' => 'إنشاء بطولة',
'ad_create' => 'إنشاء إعلان',
'org_verify' => 'توثيق منظمة',
'economy_grant' => 'منح اقتصادي',
'member_add' => 'إضافة عضو',
];
$statusLabels = [
'pending' => 'قيد الانتظار',
'approved' => 'موافق عليه',
'rejected' => 'مرفوض',
];
$statusBadges = [
'pending' => 'badge-warning',
'approved' => 'badge-success',
'rejected' => 'badge-danger',
];
$statusIcons = [
'pending' => '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
'approved' => '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
'rejected' => '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
];
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/workflows" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1>طلباتي</h1>
</div>
</div>
<?php if (empty($requests)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<h3 class="empty-state-title">لا توجد طلبات</h3>
<p class="empty-state-text">لم تقدم أي طلبات موافقة بعد</p>
</div>
<?php else: ?>
<div class="my-requests-timeline">
<?php foreach ($requests as $req): ?>
<?php
$data = is_string($req['data'] ?? null) ? json_decode($req['data'], true) : ($req['data'] ?? []);
$reqStatus = $req['status'] ?? 'pending';
$reqType = $req['type'] ?? '';
$summary = '';
if ($reqType === 'tournament_create') {
$summary = ($data['name'] ?? '') . ' - ' . ($data['game'] ?? '');
} elseif ($reqType === 'ad_create') {
$summary = ($data['campaign_name'] ?? '') . ' (' . ($data['budget'] ?? '0') . ')';
} elseif ($reqType === 'org_verify') {
$summary = ($data['org_name'] ?? '') . ' - ' . ($data['verification_type'] ?? '');
} elseif ($reqType === 'economy_grant') {
$summary = ($data['amount'] ?? '0') . ' ' . ($data['currency'] ?? 'coins');
} elseif ($reqType === 'member_add') {
$summary = ($data['role'] ?? 'member');
}
?>
<div class="my-request-card">
<div class="my-request-status">
<div class="my-request-dot my-request-dot-<?= $reqStatus ?>"></div>
<div class="my-request-line"></div>
</div>
<div class="my-request-content">
<div class="my-request-header">
<div class="flex items-center gap-3">
<span class="badge <?= $statusBadges[$reqStatus] ?? 'badge-default' ?>">
<?= $statusIcons[$reqStatus] ?? '' ?>
<?= $statusLabels[$reqStatus] ?? $reqStatus ?>
</span>
<span class="badge badge-default"><?= $typeLabels[$reqType] ?? $reqType ?></span>
</div>
<span class="text-xs text-muted tabular-nums"><?= !empty($req['created_at']) ? date('Y-m-d H:i', strtotime($req['created_at'])) : '-' ?></span>
</div>
<div class="my-request-body">
<p class="text-sm font-medium"><?= View::e($summary) ?></p>
<?php if (!empty($req['org_id'])): ?>
<p class="text-xs text-muted">المنظمة: <?= View::e($req['org_id']) ?></p>
<?php endif; ?>
</div>
<?php if ($reqStatus === 'rejected' && !empty($req['review_note'])): ?>
<div class="my-request-note my-request-note-danger">
<div class="text-xs font-medium mb-1">سبب الرفض:</div>
<p class="text-sm"><?= View::e($req['review_note']) ?></p>
</div>
<?php elseif ($reqStatus === 'approved' && !empty($req['review_note'])): ?>
<div class="my-request-note my-request-note-success">
<div class="text-xs font-medium mb-1">ملاحظة المراجع:</div>
<p class="text-sm"><?= View::e($req['review_note']) ?></p>
</div>
<?php endif; ?>
<?php if (!empty($req['reviewed_at'])): ?>
<div class="text-xs text-muted mt-2">
تمت المراجعة: <?= date('Y-m-d H:i', strtotime($req['reviewed_at'])) ?>
<?php if (!empty($req['reviewer_id'])): ?>
بواسطة <?= View::e($req['reviewer_id']) ?>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mt-3">
<a href="/workflows/<?= $req['id'] ?>" class="btn btn-ghost btn-sm">عرض التفاصيل</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($pagination->totalPages > 1): ?>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&per_page=<?= $pagination->perPage ?>" class="pagination-btn <?= !$pagination->hasPrev() ? 'disabled' : '' ?>" <?= !$pagination->hasPrev() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&per_page=<?= $pagination->perPage ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&per_page=<?= $pagination->perPage ?>" class="pagination-btn <?= !$pagination->hasNext() ? 'disabled' : '' ?>" <?= !$pagination->hasNext() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php
$eventLabels = [
'user.login' => 'تسجيل دخول',
'tournament.created' => 'إنشاء بطولة',
'tournament.completed' => 'اكتمال بطولة',
'player.banned' => 'حظر لاعب',
'report.submitted' => 'تقديم بلاغ',
'org.verified' => 'توثيق منظمة',
'ad.created' => 'إنشاء إعلان',
'economy.grant' => 'منح اقتصادي',
'member.added' => 'إضافة عضو',
];
$actionOptions = [
'auto_approve' => 'موافقة تلقائية',
'notify_admins' => 'إشعار المسؤولين',
'send_webhook' => 'إرسال Webhook',
'flag_review' => 'تحديد للمراجعة',
'update_status' => 'تحديث الحالة',
];
$operatorLabels = [
'eq' => 'يساوي',
'neq' => 'لا يساوي',
'gt' => 'أكبر من',
'gte' => 'أكبر من أو يساوي',
'lt' => 'أقل من',
'lte' => 'أقل من أو يساوي',
'contains' => 'يحتوي على',
'starts_with' => 'يبدأ بـ',
];
$existingConditions = [];
$existingActions = [];
if (!empty($rule)) {
$existingConditions = is_string($rule['conditions'] ?? null) ? json_decode($rule['conditions'], true) : ($rule['conditions'] ?? []);
$existingActions = is_string($rule['actions'] ?? null) ? json_decode($rule['actions'], true) : ($rule['actions'] ?? []);
}
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/workflows/rules" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= View::e($pageTitle) ?></h1>
</div>
</div>
<form method="POST" action="/workflows/rules/store" class="card">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="card-header">
<h3 class="card-title">معلومات القاعدة</h3>
</div>
<div class="p-5">
<div class="grid grid-2 gap-5">
<!-- Name -->
<div class="form-group">
<label class="form-label" for="ruleName">اسم القاعدة <span class="text-danger">*</span></label>
<input type="text" id="ruleName" name="name" class="form-input" value="<?= View::e($rule['name'] ?? '') ?>" placeholder="مثال: موافقة تلقائية للبطولات الصغيرة" required>
</div>
<!-- Event -->
<div class="form-group">
<label class="form-label" for="ruleEvent">الحدث <span class="text-danger">*</span></label>
<select id="ruleEvent" name="event" class="form-select" required>
<option value="">اختر الحدث...</option>
<?php foreach ($eventLabels as $value => $label): ?>
<option value="<?= $value ?>" <?= ($rule['event'] ?? '') === $value ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Priority -->
<div class="form-group">
<label class="form-label" for="rulePriority">الأولوية</label>
<input type="number" id="rulePriority" name="priority" class="form-input" value="<?= (int)($rule['priority'] ?? 0) ?>" min="0" max="100" placeholder="0">
<span class="form-hint">الأرقام الأصغر تُنفذ أولاً</span>
</div>
<!-- Active Toggle -->
<div class="form-group">
<label class="form-label">الحالة</label>
<label class="toggle-label">
<input type="checkbox" name="is_active" value="1" <?= ($rule['is_active'] ?? true) ? 'checked' : '' ?>>
<span class="toggle-switch-inline"></span>
<span>مفعّلة</span>
</label>
</div>
</div>
<!-- Conditions Builder -->
<div class="form-group mt-6">
<div class="flex justify-between items-center mb-3">
<label class="form-label mb-0">الشروط</label>
<button type="button" class="btn btn-ghost btn-sm" onclick="addCondition()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
إضافة شرط
</button>
</div>
<div id="conditionsContainer" class="conditions-builder">
<?php if (!empty($existingConditions)): ?>
<?php foreach ($existingConditions as $i => $cond): ?>
<div class="condition-row" data-index="<?= $i ?>">
<select name="condition_field[]" class="form-select condition-field">
<option value="">الحقل...</option>
<option value="type" <?= ($cond['field'] ?? '') === 'type' ? 'selected' : '' ?>>النوع</option>
<option value="requester_role" <?= ($cond['field'] ?? '') === 'requester_role' ? 'selected' : '' ?>>دور مقدم الطلب</option>
<option value="amount" <?= ($cond['field'] ?? '') === 'amount' ? 'selected' : '' ?>>المبلغ</option>
<option value="org_id" <?= ($cond['field'] ?? '') === 'org_id' ? 'selected' : '' ?>>المنظمة</option>
<option value="max_players" <?= ($cond['field'] ?? '') === 'max_players' ? 'selected' : '' ?>>أقصى لاعبين</option>
<option value="budget" <?= ($cond['field'] ?? '') === 'budget' ? 'selected' : '' ?>>الميزانية</option>
</select>
<select name="condition_operator[]" class="form-select condition-operator">
<?php foreach ($operatorLabels as $op => $opLabel): ?>
<option value="<?= $op ?>" <?= ($cond['operator'] ?? '') === $op ? 'selected' : '' ?>><?= $opLabel ?></option>
<?php endforeach; ?>
</select>
<input type="text" name="condition_value[]" class="form-input condition-value" value="<?= View::e($cond['value'] ?? '') ?>" placeholder="القيمة">
<button type="button" class="btn btn-icon btn-ghost btn-danger-hover" onclick="removeCondition(this)" title="حذف الشرط">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<p class="form-hint mt-2">أضف شروطاً لتحديد متى تُطبق هذه القاعدة</p>
</div>
<!-- Actions -->
<div class="form-group mt-6">
<label class="form-label">الإجراءات</label>
<div class="actions-checkboxes">
<?php foreach ($actionOptions as $value => $label): ?>
<label class="action-checkbox-label">
<input type="checkbox" name="actions[]" value="<?= $value ?>" <?= in_array($value, $existingActions) ? 'checked' : '' ?>>
<span class="action-checkbox-box">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</span>
<span class="action-checkbox-text"><?= $label ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="card-footer flex justify-between">
<a href="/workflows/rules" class="btn btn-ghost">إلغاء</a>
<button type="submit" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
حفظ القاعدة
</button>
</div>
</form>
<?php
$eventLabels = [
'user.login' => 'تسجيل دخول',
'tournament.created' => 'إنشاء بطولة',
'tournament.completed' => 'اكتمال بطولة',
'player.banned' => 'حظر لاعب',
'report.submitted' => 'تقديم بلاغ',
'org.verified' => 'توثيق منظمة',
'ad.created' => 'إنشاء إعلان',
'economy.grant' => 'منح اقتصادي',
'member.added' => 'إضافة عضو',
];
$actionLabels = [
'auto_approve' => 'موافقة تلقائية',
'notify_admins' => 'إشعار المسؤولين',
'send_webhook' => 'إرسال Webhook',
'flag_review' => 'تحديد للمراجعة',
'update_status' => 'تحديث الحالة',
];
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/workflows" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1>قواعد سير العمل</h1>
</div>
<a href="/workflows/rules/create" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
إنشاء قاعدة
</a>
</div>
<?php if (empty($rules)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<h3 class="empty-state-title">لا توجد قواعد</h3>
<p class="empty-state-text">لم يتم إنشاء أي قواعد لسير العمل بعد</p>
<a href="/workflows/rules/create" class="btn btn-primary mt-4">إنشاء قاعدة جديدة</a>
</div>
<?php else: ?>
<div class="rules-grid">
<?php foreach ($rules as $rule): ?>
<?php
$conditions = is_string($rule['conditions'] ?? null) ? json_decode($rule['conditions'], true) : ($rule['conditions'] ?? []);
$actions = is_string($rule['actions'] ?? null) ? json_decode($rule['actions'], true) : ($rule['actions'] ?? []);
$isActive = $rule['is_active'] ?? false;
?>
<div class="rule-card <?= $isActive ? '' : 'rule-card-inactive' ?>">
<div class="rule-card-header">
<div class="flex items-center gap-3">
<div class="rule-icon <?= $isActive ? 'rule-icon-active' : 'rule-icon-inactive' ?>">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<div>
<h4 class="rule-card-title"><?= View::e($rule['name'] ?? '-') ?></h4>
<span class="text-xs text-muted">الأولوية: <?= (int)($rule['priority'] ?? 0) ?></span>
</div>
</div>
<form method="POST" action="/workflows/rules/<?= $rule['id'] ?>/toggle" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<label class="toggle-switch">
<input type="checkbox" <?= $isActive ? 'checked' : '' ?> onchange="this.form.submit()">
<span class="toggle-slider"></span>
</label>
</form>
</div>
<div class="rule-card-body">
<!-- Event -->
<div class="rule-section">
<div class="rule-section-label">الحدث</div>
<span class="badge badge-info"><?= $eventLabels[$rule['event'] ?? ''] ?? ($rule['event'] ?? '-') ?></span>
</div>
<!-- Conditions -->
<div class="rule-section">
<div class="rule-section-label">الشروط</div>
<?php if (!empty($conditions)): ?>
<div class="rule-conditions">
<?php foreach ($conditions as $cond): ?>
<div class="rule-condition-item">
<code><?= View::e($cond['field'] ?? '') ?></code>
<span class="rule-condition-op"><?= View::e($cond['operator'] ?? '') ?></span>
<code><?= View::e($cond['value'] ?? '') ?></code>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<span class="text-muted text-xs">بدون شروط</span>
<?php endif; ?>
</div>
<!-- Actions -->
<div class="rule-section">
<div class="rule-section-label">الإجراءات</div>
<div class="flex flex-wrap gap-2">
<?php foreach ($actions as $action): ?>
<span class="badge badge-default"><?= $actionLabels[$action] ?? View::e($action) ?></span>
<?php endforeach; ?>
<?php if (empty($actions)): ?>
<span class="text-muted text-xs">بدون إجراءات</span>
<?php endif; ?>
</div>
</div>
</div>
<div class="rule-card-footer">
<span class="text-xs text-muted">
بواسطة: <?= View::e($rule['created_by'] ?? '-') ?>
<?php if (!empty($rule['created_at'])): ?>
- <?= date('Y-m-d', strtotime($rule['created_at'])) ?>
<?php endif; ?>
</span>
<div class="flex gap-2">
<form method="POST" action="/workflows/rules/<?= $rule['id'] ?>/delete" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-icon btn-ghost btn-danger-hover" onclick="return confirm('هل أنت متأكد من حذف هذه القاعدة؟')" title="حذف">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment