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));
......
<?php
/**
* Simple rule engine that evaluates conditions and triggers actions
* for workflow automation.
*
* Rules are stored in the `workflow_rules` Supabase table:
* id, event, conditions (JSON), actions (JSON), is_active, priority, created_by
*
* Conditions format (array of comparisons, all must pass — AND logic):
* [{"field": "amount", "op": ">", "value": 1000}, ...]
*
* Supported operators: =, !=, >, <, >=, <=, in, not_in, contains, starts_with, ends_with
*
* Actions format (array of action objects):
* [{"type": "notify", "params": {"to": "admin", "message": "..."}}, ...]
*
* Supported action types: notify, auto_approve, flag_review, send_webhook, update_status
*/
class RuleEngine
{
/**
* Available events that can trigger rules.
*/
private const EVENTS = [
'user.login',
'tournament.created',
'tournament.completed',
'player.banned',
'report.submitted',
'org.verified',
'ad.created',
'economy.grant',
];
/**
* Evaluate all active rules for a given event and execute matching actions.
*
* @param string $event One of the supported event names
* @param array $context Contextual data available for condition evaluation
*/
public static function trigger(string $event, array $context = []): void
{
if (!in_array($event, self::EVENTS, true)) {
return;
}
$rules = self::loadRules($event);
foreach ($rules as $rule) {
$conditions = json_decode($rule['conditions'] ?? '[]', true);
$actions = json_decode($rule['actions'] ?? '[]', true);
if (!is_array($conditions) || !is_array($actions)) {
continue;
}
if (self::evaluateConditions($conditions, $context)) {
self::executeActions($actions, $event, $context, $rule);
}
}
}
/**
* Load active rules for a specific event, ordered by priority (desc).
*/
private static function loadRules(string $event): array
{
try {
$db = Database::getInstance();
return $db->select('workflow_rules', [
'event' => 'eq.' . $event,
'is_active' => 'eq.true',
'order' => 'priority.desc',
]);
} catch (\Throwable $e) {
// If table doesn't exist or DB error, fail silently
return [];
}
}
/**
* Evaluate all conditions against the context (AND logic — all must pass).
*/
private static function evaluateConditions(array $conditions, array $context): bool
{
if (empty($conditions)) {
return true; // No conditions means always match
}
foreach ($conditions as $condition) {
if (!self::evaluateSingleCondition($condition, $context)) {
return false;
}
}
return true;
}
/**
* Evaluate a single condition against the context.
*
* @param array $condition ['field' => string, 'op' => string, 'value' => mixed]
* @param array $context The event context data
*/
private static function evaluateSingleCondition(array $condition, array $context): bool
{
$field = $condition['field'] ?? null;
$op = $condition['op'] ?? '=';
$expected = $condition['value'] ?? null;
if ($field === null) {
return false;
}
// Support nested field access via dot notation
$actual = self::resolveField($field, $context);
return match ($op) {
'=', '==' => $actual == $expected,
'!=' => $actual != $expected,
'>' => $actual > $expected,
'<' => $actual < $expected,
'>=' => $actual >= $expected,
'<=' => $actual <= $expected,
'in' => is_array($expected) && in_array($actual, $expected, false),
'not_in' => is_array($expected) && !in_array($actual, $expected, false),
'contains' => is_string($actual) && str_contains($actual, (string)$expected),
'starts_with' => is_string($actual) && str_starts_with($actual, (string)$expected),
'ends_with' => is_string($actual) && str_ends_with($actual, (string)$expected),
default => false,
};
}
/**
* Resolve a dot-notation field path from the context array.
* E.g., "player.level" resolves $context['player']['level']
*/
private static function resolveField(string $field, array $context): mixed
{
$parts = explode('.', $field);
$value = $context;
foreach ($parts as $part) {
if (!is_array($value) || !array_key_exists($part, $value)) {
return null;
}
$value = $value[$part];
}
return $value;
}
/**
* Execute all actions for a matched rule.
*/
private static function executeActions(array $actions, string $event, array $context, array $rule): void
{
foreach ($actions as $action) {
$type = $action['type'] ?? null;
$params = $action['params'] ?? [];
match ($type) {
'notify' => self::actionNotify($params, $event, $context),
'auto_approve' => self::actionAutoApprove($params, $context),
'flag_review' => self::actionFlagReview($params, $event, $context),
'send_webhook' => self::actionSendWebhook($params, $event, $context),
'update_status' => self::actionUpdateStatus($params, $context),
default => null, // Unknown action type — skip
};
}
}
// ─── Action Implementations ──────────────────────────────────────────
/**
* Action: Create a notification record.
* Params: to (user_id or 'admin'), message, title (optional)
*/
private static function actionNotify(array $params, string $event, array $context): void
{
try {
$db = Database::getInstance();
$db->insert('notifications', [
'recipient' => $params['to'] ?? 'admin',
'title' => $params['title'] ?? 'Rule triggered: ' . $event,
'message' => self::interpolate($params['message'] ?? '', $context),
'type' => 'rule_engine',
'event' => $event,
'is_read' => false,
'created_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical
}
}
/**
* Action: Auto-approve a pending item.
* Params: table, id_field, status_field, approved_value
*/
private static function actionAutoApprove(array $params, array $context): void
{
try {
$table = $params['table'] ?? null;
$idField = $params['id_field'] ?? 'id';
$statusField = $params['status_field'] ?? 'status';
$approvedValue = $params['approved_value'] ?? 'approved';
$id = $context[$idField] ?? null;
if (!$table || !$id) {
return;
}
$db = Database::getInstance();
$db->update($table, [$idField => 'eq.' . $id], [
$statusField => $approvedValue,
'approved_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical
}
}
/**
* Action: Flag an item for manual review.
* Params: table, id_field, flag_field, reason (optional)
*/
private static function actionFlagReview(array $params, string $event, array $context): void
{
try {
$table = $params['table'] ?? null;
$idField = $params['id_field'] ?? 'id';
$flagField = $params['flag_field'] ?? 'needs_review';
$id = $context[$idField] ?? null;
if (!$table || !$id) {
return;
}
$db = Database::getInstance();
$db->update($table, [$idField => 'eq.' . $id], [
$flagField => true,
'review_reason' => $params['reason'] ?? 'Flagged by rule engine on ' . $event,
'flagged_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical
}
}
/**
* Action: Send an HTTP webhook.
* Params: url, method (default POST), headers (optional)
*/
private static function actionSendWebhook(array $params, string $event, array $context): void
{
try {
$url = $params['url'] ?? null;
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) {
return;
}
$method = strtoupper($params['method'] ?? 'POST');
$headers = $params['headers'] ?? [];
$payload = json_encode([
'event' => $event,
'context' => $context,
'triggered_at' => date('c'),
]);
$curlHeaders = ['Content-Type: application/json'];
foreach ($headers as $key => $value) {
$curlHeaders[] = "{$key}: {$value}";
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => $curlHeaders,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $payload,
]);
curl_exec($ch);
curl_close($ch);
} catch (\Throwable $e) {
// Non-critical
}
}
/**
* Action: Update the status of a record.
* Params: table, id_field, status_field, status_value
*/
private static function actionUpdateStatus(array $params, array $context): void
{
try {
$table = $params['table'] ?? null;
$idField = $params['id_field'] ?? 'id';
$statusField = $params['status_field'] ?? 'status';
$statusValue = $params['status_value'] ?? null;
$id = $context[$idField] ?? null;
if (!$table || !$id || !$statusValue) {
return;
}
$db = Database::getInstance();
$db->update($table, [$idField => 'eq.' . $id], [
$statusField => $statusValue,
'updated_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical
}
}
// ─── Utility ─────────────────────────────────────────────────────────
/**
* Simple string interpolation: replaces {{field}} with context values.
*/
private static function interpolate(string $template, array $context): string
{
return preg_replace_callback('/\{\{(\w+(?:\.\w+)*)\}\}/', function ($matches) use ($context) {
$value = self::resolveField($matches[1], $context);
return is_scalar($value) ? (string)$value : json_encode($value);
}, $template);
}
}
......@@ -5,6 +5,19 @@ $currentModule = explode('/', $currentRoute)[0] ?: 'dashboard';
function navActive(string $module, string $current): string {
return $module === $current ? 'active' : '';
}
$role = Auth::role();
$isAdmin = in_array($role, ['superadmin', 'admin']);
$isModerator = ($role === 'moderator');
$isOrg = in_array($role, ['org_admin', 'org_manager']);
$isTournamentOrganizer = ($role === 'tournament_organizer');
$isSponsor = ($role === 'sponsor');
$isCharity = ($role === 'charity');
$isViewer = ($role === 'viewer');
$db = Database::getInstance();
$pendingReports = $db->count('cheat_reports', ['status' => 'eq.pending']);
$pendingWorkflows = $isAdmin ? $db->count('admin_users', ['status' => 'eq.pending']) : 0;
?>
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
......@@ -15,6 +28,7 @@ function navActive(string $module, string $current): string {
<div class="favorites-bar" id="favoritesBar" style="display:none;"></div>
<nav class="sidebar-nav">
<!-- Main Section - Always visible -->
<div class="nav-section" data-section="main">
<div class="nav-section-header">
<span class="nav-section-title nav-text">الرئيسية</span>
......@@ -28,6 +42,8 @@ function navActive(string $module, string $current): string {
</div>
</div>
<?php if ($isAdmin): ?>
<!-- Admin: Full Management Section -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">إدارة</span>
......@@ -54,9 +70,14 @@ function navActive(string $module, string $current): string {
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/><path d="M9 9h.01"/><path d="M9 13h.01"/><path d="M9 17h.01"/></svg>
<span class="nav-text">المنظمات</span>
</a>
<a href="/users" class="nav-item <?= navActive('users', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>
<span class="nav-text">المستخدمون</span>
</a>
</div>
</div>
<!-- Admin: Operations Section -->
<div class="nav-section" data-section="ops">
<div class="nav-section-header">
<span class="nav-section-title nav-text">العمليات</span>
......@@ -74,14 +95,17 @@ function navActive(string $module, string $current): string {
<a href="/moderation" class="nav-item <?= navActive('moderation', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<span class="nav-text">الإشراف</span>
<?php
$db = Database::getInstance();
$pendingReports = $db->count('cheat_reports', ['status' => 'eq.pending']);
if ($pendingReports > 0):
?>
<?php if ($pendingReports > 0): ?>
<span class="nav-badge"><?= $pendingReports ?></span>
<?php endif; ?>
</a>
<a href="/workflows" class="nav-item <?= navActive('workflows', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
<span class="nav-text">سير العمل</span>
<?php if ($pendingWorkflows > 0): ?>
<span class="nav-badge"><?= $pendingWorkflows ?></span>
<?php endif; ?>
</a>
<a href="/notifications" class="nav-item <?= navActive('notifications', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="nav-text">الإشعارات</span>
......@@ -89,6 +113,7 @@ function navActive(string $module, string $current): string {
</div>
</div>
<!-- Admin: System Section -->
<div class="nav-section" data-section="system">
<div class="nav-section-header">
<span class="nav-section-title nav-text">النظام</span>
......@@ -117,6 +142,160 @@ function navActive(string $module, string $current): string {
</a>
</div>
</div>
<?php elseif ($isModerator): ?>
<!-- Moderator Navigation -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">الإشراف</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="nav-section-items">
<a href="/players" class="nav-item <?= navActive('players', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span class="nav-text">اللاعبون</span>
</a>
<a href="/moderation" class="nav-item <?= navActive('moderation', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<span class="nav-text">الإشراف</span>
<?php if ($pendingReports > 0): ?>
<span class="nav-badge"><?= $pendingReports ?></span>
<?php endif; ?>
</a>
<a href="/audit-log" class="nav-item <?= navActive('audit-log', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<span class="nav-text">سجل العمليات</span>
</a>
</div>
</div>
<?php elseif ($isOrg): ?>
<!-- Organization Navigation -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">المنظمة</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="nav-section-items">
<a href="/organizations/<?= Auth::orgId() ?>" class="nav-item <?= navActive('organizations', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/><path d="M9 9h.01"/><path d="M9 13h.01"/><path d="M9 17h.01"/></svg>
<span class="nav-text">منظمتي</span>
</a>
<a href="/tournaments" class="nav-item <?= navActive('tournaments', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
<span class="nav-text">البطولات</span>
</a>
<a href="/players" class="nav-item <?= navActive('players', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span class="nav-text">الأعضاء</span>
</a>
<a href="/analytics" class="nav-item <?= navActive('analytics', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
<span class="nav-text">التحليلات</span>
</a>
<a href="/notifications" class="nav-item <?= navActive('notifications', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="nav-text">الإشعارات</span>
</a>
</div>
</div>
<?php elseif ($isTournamentOrganizer): ?>
<!-- Tournament Organizer Navigation -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">البطولات</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="nav-section-items">
<a href="/tournaments" class="nav-item <?= navActive('tournaments', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
<span class="nav-text">بطولاتي</span>
</a>
<a href="/players" class="nav-item <?= navActive('players', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span class="nav-text">اللاعبون</span>
</a>
<a href="/analytics" class="nav-item <?= navActive('analytics', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
<span class="nav-text">التحليلات</span>
</a>
</div>
</div>
<?php elseif ($isSponsor): ?>
<!-- Sponsor Navigation -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">الرعاية</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="nav-section-items">
<a href="/ads" class="nav-item <?= navActive('ads', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
<span class="nav-text">حملاتي</span>
</a>
<a href="/analytics" class="nav-item <?= navActive('analytics', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
<span class="nav-text">التحليلات</span>
</a>
<a href="/notifications" class="nav-item <?= navActive('notifications', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="nav-text">الإشعارات</span>
</a>
</div>
</div>
<?php elseif ($isCharity): ?>
<!-- Charity Navigation -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">الأعمال الخيرية</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="nav-section-items">
<a href="/tournaments?type=charity" class="nav-item <?= navActive('tournaments', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
<span class="nav-text">الفعاليات الخيرية</span>
</a>
<a href="/economy" class="nav-item <?= navActive('economy', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
<span class="nav-text">الاقتصاد</span>
</a>
<a href="/notifications" class="nav-item <?= navActive('notifications', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="nav-text">الإشعارات</span>
</a>
</div>
</div>
<?php elseif ($isViewer): ?>
<!-- Viewer Navigation (read-only) -->
<div class="nav-section" data-section="manage">
<div class="nav-section-header">
<span class="nav-section-title nav-text">التصفح</span>
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="nav-section-items">
<a href="/players" class="nav-item <?= navActive('players', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span class="nav-text">اللاعبون</span>
</a>
<a href="/games" class="nav-item <?= navActive('games', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M12 12h.01"/><path d="M17 12h.01"/><path d="M7 12h.01"/></svg>
<span class="nav-text">الألعاب</span>
</a>
<a href="/tournaments" class="nav-item <?= navActive('tournaments', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
<span class="nav-text">البطولات</span>
</a>
<a href="/analytics" class="nav-item <?= navActive('analytics', $currentModule) ?>">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
<span class="nav-text">التحليلات</span>
</a>
</div>
</div>
<?php endif; ?>
</nav>
<div class="sidebar-footer" style="padding: var(--space-4); border-top: 1px solid var(--border);">
......
<?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');
}
}
<?php $role = $role ?? Auth::role(); ?>
<?php if (in_array($role, ['superadmin', 'admin'])): ?>
<!-- Admin/Superadmin Dashboard -->
<div class="content-header">
<h1>لوحة التحكم</h1>
<div class="flex gap-3">
<a href="/tournaments/create" class="btn btn-primary btn-sm">إنشاء بطولة</a>
<a href="/moderation" class="btn btn-ghost btn-sm">مراجعة البلاغات</a>
<?php if ($pendingApprovals > 0): ?>
<a href="/workflows?status=pending" class="btn btn-warning btn-sm">الموافقات المعلقة (<?= $pendingApprovals ?>)</a>
<?php endif; ?>
</div>
</div>
......@@ -49,6 +56,29 @@
</div>
</div>
<!-- Admin Users & Approvals -->
<div class="grid grid-2 mb-6">
<div class="stat-card">
<div class="stat-icon purple">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي مستخدمي الإدارة</div>
<div class="stat-value" data-count="<?= $totalAdminUsers ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon warning">
<svg width="24" height="24" 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>
</div>
<div class="stat-content">
<div class="stat-label">الموافقات المعلقة</div>
<div class="stat-value" data-count="<?= $pendingApprovals ?>">0</div>
</div>
</div>
</div>
<!-- Service Health -->
<div class="grid grid-2 mb-6">
<div class="card">
......@@ -83,6 +113,8 @@
<a href="/moderation" class="btn btn-ghost w-full">مراجعة البلاغات</a>
<a href="/games" class="btn btn-ghost w-full">إدارة الألعاب</a>
<a href="/analytics" class="btn btn-ghost w-full">التحليلات</a>
<a href="/workflows" class="btn btn-ghost w-full">سير العمل</a>
<a href="/users" class="btn btn-ghost w-full">إدارة المستخدمين</a>
</div>
</div>
</div>
......@@ -124,3 +156,537 @@
</div>
<?php endif; ?>
</div>
<?php elseif ($role === 'moderator'): ?>
<!-- Moderator Dashboard -->
<div class="content-header">
<h1>لوحة التحكم - الإشراف</h1>
<div class="flex gap-3">
<a href="/moderation" class="btn btn-primary btn-sm">قائمة البلاغات</a>
</div>
</div>
<!-- Moderator Stat Cards -->
<div class="grid grid-3 stagger mb-6">
<div class="stat-card">
<div class="stat-icon red">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البلاغات المعلقة</div>
<div class="stat-value" data-count="<?= $pendingReports ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<svg width="24" height="24" 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>
</div>
<div class="stat-content">
<div class="stat-label">البلاغات المحلولة</div>
<div class="stat-value" data-count="<?= $resolvedReports ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg width="24" height="24" 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>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي الحظر</div>
<div class="stat-value" data-count="<?= $totalBans ?>">0</div>
</div>
</div>
</div>
<!-- Recent Reports -->
<div class="card mb-6">
<div class="card-header">
<h3 class="card-title">آخر البلاغات</h3>
<a href="/moderation" class="btn btn-ghost btn-sm">عرض الكل</a>
</div>
<?php if (empty($recentReports)): ?>
<div class="empty-state" style="padding: var(--space-6);">
<p class="text-secondary">لا توجد بلاغات معلقة</p>
</div>
<?php else: ?>
<div class="data-table-wrapper" style="border: none;">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>المُبلِّغ</th>
<th>المُبلَّغ عنه</th>
<th>السبب</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentReports as $report): ?>
<tr>
<td class="text-muted text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($report['created_at'])) ?></td>
<td><?= View::e($report['reporter_id'] ?? '-') ?></td>
<td><?= View::e($report['reported_id'] ?? '-') ?></td>
<td><?= View::e($report['reason'] ?? '-') ?></td>
<td><span class="badge badge-<?= $report['status'] === 'pending' ? 'warning' : 'success' ?>"><?= View::e($report['status']) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Recent Actions -->
<div class="card">
<div class="card-header">
<h3 class="card-title">إجراءاتي الأخيرة</h3>
</div>
<?php if (empty($recentActions)): ?>
<div class="empty-state" style="padding: var(--space-6);">
<p class="text-secondary">لا توجد إجراءات بعد</p>
</div>
<?php else: ?>
<div class="data-table-wrapper" style="border: none;">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>الإجراء</th>
<th>النوع</th>
<th>المعرف</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentActions as $action): ?>
<tr>
<td class="text-muted text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($action['created_at'])) ?></td>
<td><span class="badge badge-info"><?= View::e($action['action']) ?></span></td>
<td><?= View::e($action['entity_type']) ?></td>
<td class="text-xs text-muted truncate" style="max-width: 120px;"><?= View::e($action['entity_id'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif (in_array($role, ['org_admin', 'org_manager'])): ?>
<!-- Organization Dashboard -->
<div class="content-header">
<h1>لوحة التحكم - <?= View::e($orgInfo['name'] ?? 'المنظمة') ?></h1>
<div class="flex gap-3">
<a href="/organizations/<?= View::e($orgInfo['id'] ?? '') ?>" class="btn btn-primary btn-sm">إدارة المنظمة</a>
<a href="/workflows?type=my_requests" class="btn btn-ghost btn-sm">طلباتي</a>
</div>
</div>
<!-- Org Stat Cards -->
<div class="grid grid-3 stagger mb-6">
<div class="stat-card">
<div class="stat-icon blue">
<svg width="24" height="24" 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"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">الأعضاء</div>
<div class="stat-value" data-count="<?= $membersCount ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon gold">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البطولات</div>
<div class="stat-value" data-count="<?= $tournamentsCount ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M12 12h.01"/><path d="M17 12h.01"/><path d="M7 12h.01"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">المباريات النشطة</div>
<div class="stat-value" data-count="<?= $activeMatches ?>">0</div>
</div>
</div>
</div>
<!-- Org Recent Activity -->
<div class="card">
<div class="card-header">
<h3 class="card-title">آخر نشاطات المنظمة</h3>
</div>
<?php if (empty($recentActivity)): ?>
<div class="empty-state" style="padding: var(--space-6);">
<p class="text-secondary">لا توجد نشاطات بعد</p>
</div>
<?php else: ?>
<div class="data-table-wrapper" style="border: none;">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>المستخدم</th>
<th>الإجراء</th>
<th>النوع</th>
<th>المعرف</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentActivity as $activity): ?>
<tr>
<td class="text-muted text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($activity['created_at'])) ?></td>
<td><?= View::e($activity['actor']) ?></td>
<td><span class="badge badge-info"><?= View::e($activity['action']) ?></span></td>
<td><?= View::e($activity['entity_type']) ?></td>
<td class="text-xs text-muted truncate" style="max-width: 120px;"><?= View::e($activity['entity_id'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($role === 'tournament_organizer'): ?>
<!-- Tournament Organizer Dashboard -->
<div class="content-header">
<h1>لوحة التحكم - منظم البطولات</h1>
<div class="flex gap-3">
<a href="/tournaments/create" class="btn btn-primary btn-sm">إنشاء بطولة</a>
<a href="/workflows?type=my_requests" class="btn btn-ghost btn-sm">طلباتي</a>
</div>
</div>
<!-- Tournament Organizer Stat Cards -->
<div class="grid grid-4 stagger mb-6">
<div class="stat-card">
<div class="stat-icon green">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البطولات الجارية</div>
<div class="stat-value" data-count="<?= $activeTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg width="24" height="24" 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>
</div>
<div class="stat-content">
<div class="stat-label">البطولات المكتملة</div>
<div class="stat-value" data-count="<?= $completedTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon gold">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي البطولات</div>
<div class="stat-value" data-count="<?= $totalTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon purple">
<svg width="24" height="24" 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"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي المشاركين</div>
<div class="stat-value" data-count="<?= $totalParticipants ?>">0</div>
</div>
</div>
</div>
<!-- My Tournaments -->
<div class="card">
<div class="card-header">
<h3 class="card-title">بطولاتي الأخيرة</h3>
<a href="/tournaments?organizer=me" class="btn btn-ghost btn-sm">عرض الكل</a>
</div>
<?php if (empty($myTournaments)): ?>
<div class="empty-state" style="padding: var(--space-6);">
<p class="text-secondary">لا توجد بطولات بعد</p>
</div>
<?php else: ?>
<div class="data-table-wrapper" style="border: none;">
<table class="data-table">
<thead>
<tr>
<th>الاسم</th>
<th>الحالة</th>
<th>المشاركون</th>
<th>تاريخ الإنشاء</th>
</tr>
</thead>
<tbody>
<?php foreach ($myTournaments as $tournament): ?>
<tr>
<td><?= View::e($tournament['name'] ?? '-') ?></td>
<td><span class="badge badge-<?= $tournament['status'] === 'in_progress' ? 'success' : ($tournament['status'] === 'completed' ? 'info' : 'warning') ?>"><?= View::e($tournament['status']) ?></span></td>
<td><?= number_format($tournament['participants_count'] ?? 0) ?></td>
<td class="text-muted text-xs tabular-nums"><?= date('Y-m-d', strtotime($tournament['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($role === 'sponsor'): ?>
<!-- Sponsor Dashboard -->
<div class="content-header">
<h1>لوحة التحكم - الراعي</h1>
<div class="flex gap-3">
<a href="/ads/create" class="btn btn-primary btn-sm">إنشاء حملة</a>
<a href="/workflows?type=my_requests" class="btn btn-ghost btn-sm">طلباتي</a>
</div>
</div>
<!-- Sponsor Stat Cards -->
<div class="grid grid-4 stagger mb-6">
<div class="stat-card">
<div class="stat-icon green">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">الحملات النشطة</div>
<div class="stat-value" data-count="<?= $activeCampaigns ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي الحملات</div>
<div class="stat-value" data-count="<?= $totalCampaigns ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon purple">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي المشاهدات</div>
<div class="stat-value" data-count="<?= $totalImpressions ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon gold">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي الإنفاق</div>
<div class="stat-value"><?= number_format($totalSpend, 2) ?></div>
</div>
</div>
</div>
<!-- Campaign Performance Placeholder -->
<div class="card mb-6">
<div class="card-header">
<h3 class="card-title">أداء الحملات</h3>
</div>
<div class="flex items-center justify-center" style="padding: var(--space-8); min-height: 200px;">
<p class="text-secondary">رسم بياني لأداء الحملات - قريبا</p>
</div>
</div>
<!-- Campaigns List -->
<div class="card">
<div class="card-header">
<h3 class="card-title">حملاتي</h3>
<a href="/ads" class="btn btn-ghost btn-sm">عرض الكل</a>
</div>
<?php if (empty($campaigns)): ?>
<div class="empty-state" style="padding: var(--space-6);">
<p class="text-secondary">لا توجد حملات بعد</p>
</div>
<?php else: ?>
<div class="data-table-wrapper" style="border: none;">
<table class="data-table">
<thead>
<tr>
<th>الاسم</th>
<th>الحالة</th>
<th>المشاهدات</th>
<th>الإنفاق</th>
</tr>
</thead>
<tbody>
<?php foreach ($campaigns as $campaign): ?>
<tr>
<td><?= View::e($campaign['name'] ?? '-') ?></td>
<td><span class="badge badge-<?= $campaign['status'] === 'active' ? 'success' : 'secondary' ?>"><?= View::e($campaign['status']) ?></span></td>
<td><?= number_format($campaign['impressions'] ?? 0) ?></td>
<td><?= number_format($campaign['spend'] ?? 0, 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($role === 'charity'): ?>
<!-- Charity Dashboard -->
<div class="content-header">
<h1>لوحة التحكم - الجمعية الخيرية</h1>
<div class="flex gap-3">
<a href="/tournaments?type=charity" class="btn btn-primary btn-sm">فعالياتي الخيرية</a>
<a href="/workflows?type=my_requests" class="btn btn-ghost btn-sm">طلباتي</a>
</div>
</div>
<!-- Charity Stat Cards -->
<div class="grid grid-4 stagger mb-6">
<div class="stat-card">
<div class="stat-icon green">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البطولات الجارية</div>
<div class="stat-value" data-count="<?= $activeTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي البطولات الخيرية</div>
<div class="stat-value" data-count="<?= $charityTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon gold">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي التبرعات</div>
<div class="stat-value"><?= number_format($totalDonations, 2) ?></div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon purple">
<svg width="24" height="24" 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"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي المشاركين</div>
<div class="stat-value" data-count="<?= $totalParticipants ?>">0</div>
</div>
</div>
</div>
<!-- Charity Tournaments -->
<div class="card">
<div class="card-header">
<h3 class="card-title">فعالياتي الخيرية</h3>
<a href="/tournaments?type=charity" class="btn btn-ghost btn-sm">عرض الكل</a>
</div>
<?php if (empty($tournaments)): ?>
<div class="empty-state" style="padding: var(--space-6);">
<p class="text-secondary">لا توجد فعاليات خيرية بعد</p>
</div>
<?php else: ?>
<div class="data-table-wrapper" style="border: none;">
<table class="data-table">
<thead>
<tr>
<th>الاسم</th>
<th>الحالة</th>
<th>المشاركون</th>
<th>التبرعات</th>
</tr>
</thead>
<tbody>
<?php foreach ($tournaments as $tournament): ?>
<tr>
<td><?= View::e($tournament['name'] ?? '-') ?></td>
<td><span class="badge badge-<?= $tournament['status'] === 'in_progress' ? 'success' : ($tournament['status'] === 'completed' ? 'info' : 'warning') ?>"><?= View::e($tournament['status']) ?></span></td>
<td><?= number_format($tournament['participants_count'] ?? 0) ?></td>
<td><?= number_format($tournament['donation_amount'] ?? 0, 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Viewer Dashboard (default/read-only) -->
<div class="content-header">
<h1>لوحة التحكم</h1>
</div>
<!-- Viewer Stat Cards -->
<div class="grid grid-4 stagger mb-6">
<div class="stat-card">
<div class="stat-icon blue">
<svg width="24" height="24" 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"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">اللاعبون المتصلون / الإجمالي</div>
<div class="stat-value"><span data-count="<?= $onlinePlayers ?>">0</span> / <?= number_format($totalPlayers) ?></div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M12 12h.01"/><path d="M17 12h.01"/><path d="M7 12h.01"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي المباريات</div>
<div class="stat-value" data-count="<?= $totalMatches ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon gold">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البطولات الجارية</div>
<div class="stat-value" data-count="<?= $activeTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<svg width="24" height="24" 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"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي اللاعبين</div>
<div class="stat-value" data-count="<?= $totalPlayers ?>">0</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">اختصارات سريعة</h3>
</div>
<div class="grid grid-2 gap-3">
<a href="/players" class="btn btn-ghost w-full">قائمة اللاعبين</a>
<a href="/games" class="btn btn-ghost w-full">الألعاب</a>
<a href="/tournaments" class="btn btn-ghost w-full">البطولات</a>
<a href="/analytics" class="btn btn-ghost w-full">التحليلات</a>
</div>
</div>
<?php endif; ?>
/* 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);
});
}
<?php
class UsersController
{
private Database $db;
private array $allRoles = [
'superadmin', 'admin', 'moderator', 'org_admin',
'org_manager', 'tournament_organizer', 'sponsor', 'charity', 'viewer'
];
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
Auth::requireRole('admin');
$search = $_GET['search'] ?? '';
$role = $_GET['role'] ?? '';
$org = $_GET['org'] ?? '';
$status = $_GET['status'] ?? '';
$sort = $_GET['sort'] ?? 'created_at';
$dir = $_GET['dir'] ?? 'desc';
$queryParams = ['select' => '*', 'order' => "{$sort}.{$dir}"];
if ($search) {
$queryParams['or'] = "(username.ilike.*{$search}*,display_name.ilike.*{$search}*,email.ilike.*{$search}*)";
}
if ($role && in_array($role, $this->allRoles)) {
$queryParams['role'] = "eq.{$role}";
}
if ($org) {
$queryParams['org_id'] = "eq.{$org}";
}
if ($status === 'active') {
$queryParams['is_active'] = 'eq.true';
} elseif ($status === 'inactive') {
$queryParams['is_active'] = 'eq.false';
}
// Exclude soft-deleted
$queryParams['deleted_at'] = 'is.null';
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('admin_users', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$users = $this->db->select('admin_users', $queryParams);
// Fetch organizations for filter dropdown
$organizations = $this->db->select('organizations', ['select' => 'id,name', 'order' => 'name.asc']);
$pageTitle = 'المستخدمون';
$moduleCSS = 'users';
$moduleJS = 'users';
View::render('users/list', compact(
'users', 'pagination', 'search', 'role', 'org', 'status',
'sort', 'dir', 'organizations', 'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function create(array $params, string $method): void
{
Auth::requireRole('admin');
$organizations = $this->db->select('organizations', ['select' => 'id,name', 'order' => 'name.asc']);
$user = [];
$pageTitle = 'إضافة مستخدم';
$moduleCSS = 'users';
$moduleJS = 'users';
View::render('users/form', compact('user', 'organizations', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
public function store(array $params, string $method): void
{
Auth::requireRole('admin');
Auth::requireCsrf();
$validator = Validator::make($_POST)
->required('username', 'اسم المستخدم')
->minLength('username', 3, 'اسم المستخدم')
->maxLength('username', 30, 'اسم المستخدم')
->required('display_name', 'الاسم المعروض')
->required('email', 'البريد الإلكتروني')
->email('email', 'البريد الإلكتروني')
->required('password', 'كلمة المرور')
->minLength('password', 8, 'كلمة المرور')
->required('role', 'الدور')
->in('role', $this->allRoles, 'الدور');
if ($validator->fails()) {
Response::error($validator->firstError(), '/users/create');
return;
}
// Check username uniqueness
$existing = $this->db->selectOne('admin_users', ['username' => "eq.{$_POST['username']}"]);
if ($existing) {
Response::error('اسم المستخدم مستخدم بالفعل', '/users/create');
return;
}
// Check email uniqueness
$existingEmail = $this->db->selectOne('admin_users', ['email' => "eq.{$_POST['email']}"]);
if ($existingEmail) {
Response::error('البريد الإلكتروني مستخدم بالفعل', '/users/create');
return;
}
$data = [
'username' => trim($_POST['username']),
'display_name' => trim($_POST['display_name']),
'email' => trim($_POST['email']),
'password_hash' => password_hash($_POST['password'], PASSWORD_BCRYPT),
'role' => $_POST['role'],
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'is_active' => isset($_POST['is_active']) ? true : false,
'avatar_url' => trim($_POST['avatar_url'] ?? ''),
'created_by' => Auth::user()['username'],
];
$result = $this->db->insert('admin_users', $data);
$logData = $data;
unset($logData['password_hash']);
AuditLog::log('create', 'admin_user', $result['id'] ?? null, null, $logData);
Response::success('تم إنشاء المستخدم بنجاح', '/users');
}
public function show(array $params, string $method): void
{
Auth::requireRole('admin');
$id = $params['id'];
$user = $this->db->selectOne('admin_users', ['id' => "eq.{$id}", 'deleted_at' => 'is.null']);
if (!$user) {
http_response_code(404);
View::render('errors/404');
return;
}
// Fetch organization info if user has one
$organization = null;
if (!empty($user['org_id'])) {
$organization = $this->db->selectOne('organizations', ['id' => "eq.{$user['org_id']}"]);
}
// Activity log for this user
$activities = $this->db->select('audit_log', [
'select' => '*',
'actor' => "eq.{$user['username']}",
'order' => 'created_at.desc',
'limit' => 50,
]);
// Count stats
$actionsCount = $this->db->count('audit_log', ['actor' => "eq.{$user['username']}"]);
$pageTitle = $user['display_name'] ?? $user['username'];
$moduleCSS = 'users';
$moduleJS = 'users';
View::render('users/show', compact(
'user', 'organization', 'activities', 'actionsCount',
'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function edit(array $params, string $method): void
{
Auth::requireRole('admin');
$id = $params['id'];
$user = $this->db->selectOne('admin_users', ['id' => "eq.{$id}", 'deleted_at' => 'is.null']);
if (!$user) {
http_response_code(404);
View::render('errors/404');
return;
}
$organizations = $this->db->select('organizations', ['select' => 'id,name', 'order' => 'name.asc']);
$pageTitle = 'تعديل المستخدم';
$moduleCSS = 'users';
$moduleJS = 'users';
View::render('users/form', compact('user', 'organizations', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
public function update(array $params, string $method): void
{
Auth::requireRole('admin');
Auth::requireCsrf();
$id = $params['id'];
$old = $this->db->selectOne('admin_users', ['id' => "eq.{$id}", 'deleted_at' => 'is.null']);
if (!$old) {
Response::error('المستخدم غير موجود', '/users');
return;
}
$validator = Validator::make($_POST)
->required('display_name', 'الاسم المعروض')
->required('email', 'البريد الإلكتروني')
->email('email', 'البريد الإلكتروني')
->required('role', 'الدور')
->in('role', $this->allRoles, 'الدور');
if ($validator->fails()) {
Response::error($validator->firstError(), "/users/{$id}/edit");
return;
}
// Check email uniqueness (exclude current user)
$existingEmail = $this->db->selectOne('admin_users', [
'email' => "eq." . trim($_POST['email']),
'id' => "neq.{$id}",
]);
if ($existingEmail) {
Response::error('البريد الإلكتروني مستخدم بالفعل', "/users/{$id}/edit");
return;
}
$data = [
'display_name' => trim($_POST['display_name']),
'email' => trim($_POST['email']),
'role' => $_POST['role'],
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'is_active' => isset($_POST['is_active']) ? true : false,
'avatar_url' => trim($_POST['avatar_url'] ?? ''),
];
$this->db->update('admin_users', ['id' => "eq.{$id}"], $data);
$logOld = ['display_name' => $old['display_name'], 'email' => $old['email'], 'role' => $old['role'], 'org_id' => $old['org_id']];
AuditLog::log('update', 'admin_user', $id, $logOld, $data);
Response::success('تم تحديث بيانات المستخدم', "/users/{$id}");
}
public function toggleStatus(array $params, string $method): void
{
Auth::requireRole('admin');
Auth::requireCsrf();
$id = $params['id'];
$user = $this->db->selectOne('admin_users', ['id' => "eq.{$id}", 'deleted_at' => 'is.null']);
if (!$user) {
Response::error('المستخدم غير موجود', '/users');
return;
}
// Prevent deactivating own account
if ($user['username'] === Auth::user()['username']) {
Response::error('لا يمكنك تعطيل حسابك', '/users');
return;
}
$newStatus = !($user['is_active'] ?? false);
$this->db->update('admin_users', ['id' => "eq.{$id}"], ['is_active' => $newStatus]);
$action = $newStatus ? 'activate' : 'deactivate';
AuditLog::log($action, 'admin_user', $id, ['is_active' => $user['is_active']], ['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل المستخدم' : 'تم تعطيل المستخدم';
Response::success($message, '/users');
}
public function delete(array $params, string $method): void
{
Auth::requireRole('admin');
Auth::requireCsrf();
$id = $params['id'];
$user = $this->db->selectOne('admin_users', ['id' => "eq.{$id}", 'deleted_at' => 'is.null']);
if (!$user) {
Response::error('المستخدم غير موجود', '/users');
return;
}
// Prevent deleting own account
if ($user['username'] === Auth::user()['username']) {
Response::error('لا يمكنك حذف حسابك', '/users');
return;
}
// Soft delete
$this->db->update('admin_users', ['id' => "eq.{$id}"], [
'deleted_at' => date('c'),
'is_active' => false,
]);
AuditLog::log('delete', 'admin_user', $id, null, ['username' => $user['username']]);
Response::success('تم حذف المستخدم', '/users');
}
public function resetPassword(array $params, string $method): void
{
Auth::requireRole('admin');
Auth::requireCsrf();
$id = $params['id'];
$user = $this->db->selectOne('admin_users', ['id' => "eq.{$id}", 'deleted_at' => 'is.null']);
if (!$user) {
Response::error('المستخدم غير موجود', '/users');
return;
}
// Generate temporary password
$tempPassword = bin2hex(random_bytes(4)); // 8 character hex string
$hash = password_hash($tempPassword, PASSWORD_BCRYPT);
$this->db->update('admin_users', ['id' => "eq.{$id}"], ['password_hash' => $hash]);
AuditLog::log('reset_password', 'admin_user', $id);
// Flash the temp password so admin can share it
$_SESSION['flash'] = [
'type' => 'success',
'message' => "تم إعادة تعيين كلمة المرور. كلمة المرور المؤقتة: {$tempPassword}",
];
header("Location: /users/{$id}");
exit;
}
public function bulkAction(array $params, string $method): void
{
Auth::requireRole('admin');
Auth::requireCsrf();
$action = $_POST['action'] ?? '';
$ids = array_filter(explode(',', $_POST['ids'] ?? ''));
if (empty($ids) || !in_array($action, ['activate', 'deactivate'])) {
Response::error('بيانات غير صحيحة', '/users');
return;
}
$newStatus = $action === 'activate';
foreach ($ids as $id) {
$this->db->update('admin_users', ['id' => "eq.{$id}"], ['is_active' => $newStatus]);
}
AuditLog::log("bulk_{$action}", 'admin_user', null, null, ['ids' => $ids]);
$message = $newStatus ? 'تم تفعيل المستخدمين المحددين' : 'تم تعطيل المستخدمين المحددين';
Response::success($message, '/users');
}
}
<?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>
<?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">
<h1>المستخدمون</h1>
<a href="/users/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>
<!-- Filter Pills by Role -->
<div class="filter-pills mb-5">
<a href="/users?status=<?= urlencode($status) ?>&org=<?= urlencode($org) ?>" class="filter-pill <?= empty($role) ? 'active' : '' ?>">الكل</a>
<?php foreach ($roleLabels as $roleKey => $roleLabel): ?>
<a href="/users?role=<?= $roleKey ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($org) ?>" class="filter-pill <?= $role === $roleKey ? 'active' : '' ?>"><?= $roleLabel ?></a>
<?php endforeach; ?>
</div>
<div class="data-table-wrapper">
<div class="table-toolbar">
<div class="table-search">
<svg class="table-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" placeholder="بحث بالاسم أو البريد الإلكتروني..." value="<?= View::e($search) ?>" id="userSearch">
</div>
<div class="table-actions flex gap-2">
<select class="form-select" style="width: auto; padding: var(--space-2) var(--space-4);" onchange="filterByStatus(this.value)">
<option value="" <?= empty($status) ? 'selected' : '' ?>>كل الحالات</option>
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="inactive" <?= $status === 'inactive' ? 'selected' : '' ?>>معطل</option>
</select>
<?php if (!empty($organizations)): ?>
<select class="form-select" style="width: auto; padding: var(--space-2) var(--space-4);" onchange="filterByOrg(this.value)">
<option value="">كل المنظمات</option>
<?php foreach ($organizations as $orgItem): ?>
<option value="<?= $orgItem['id'] ?>" <?= $org === $orgItem['id'] ? 'selected' : '' ?>><?= View::e($orgItem['name']) ?></option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<select class="form-select" style="width: auto; padding: var(--space-2) var(--space-4);" onchange="location.href='/users?per_page='+this.value">
<option value="25" <?= ($pagination->perPage == 25) ? 'selected' : '' ?>>25</option>
<option value="50" <?= ($pagination->perPage == 50) ? 'selected' : '' ?>>50</option>
<option value="100" <?= ($pagination->perPage == 100) ? 'selected' : '' ?>>100</option>
</select>
</div>
</div>
<!-- Bulk Actions Bar -->
<div class="hidden flex items-center gap-3 p-3 bg-elevated border-b" id="bulkActions">
<span class="text-sm text-secondary">تم تحديد <strong class="bulk-count">0</strong> مستخدم</span>
<button class="btn btn-success btn-sm" onclick="bulkActivate()">تفعيل المحدد</button>
<button class="btn btn-danger btn-sm" onclick="bulkDeactivate()">تعطيل المحدد</button>
</div>
<?php if (empty($users)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<h3 class="empty-state-title">لا يوجد مستخدمون</h3>
<p class="empty-state-text">لم يتم العثور على أي مستخدمين<?= $search ? ' لبحثك "' . View::e($search) . '"' : '' ?></p>
</div>
<?php else: ?>
<table class="data-table" id="usersTable">
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" class="check-all"></th>
<th>المستخدم</th>
<th>البريد الإلكتروني</th>
<th>الدور</th>
<th>المنظمة</th>
<th>الحالة</th>
<th data-sort="last_login">آخر دخول</th>
<th style="width: 60px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $u): ?>
<tr>
<td><input type="checkbox" class="row-check" value="<?= $u['id'] ?>"></td>
<td>
<div class="flex items-center gap-3">
<div class="avatar">
<?php if (!empty($u['avatar_url'])): ?>
<img src="<?= View::e($u['avatar_url']) ?>" alt="">
<?php else: ?>
<?= mb_substr($u['display_name'] ?? $u['username'] ?? '?', 0, 1) ?>
<?php endif; ?>
</div>
<div>
<div class="font-medium"><?= View::e($u['display_name'] ?? $u['username']) ?></div>
<div class="text-xs text-muted">@<?= View::e($u['username']) ?></div>
</div>
</div>
</td>
<td class="text-sm" dir="ltr" style="text-align: right;"><?= View::e($u['email'] ?? '-') ?></td>
<td>
<span class="badge badge-<?= $roleBadgeColors[$u['role']] ?? 'default' ?>">
<?= $roleLabels[$u['role']] ?? $u['role'] ?>
</span>
</td>
<td class="text-sm text-secondary"><?= View::e($u['org_name'] ?? '-') ?></td>
<td>
<?php if ($u['is_active'] ?? false): ?>
<span class="badge badge-success badge-dot">نشط</span>
<?php else: ?>
<span class="badge badge-danger badge-dot">معطل</span>
<?php endif; ?>
</td>
<td class="text-xs text-muted tabular-nums">
<?= !empty($u['last_login']) ? date('m/d H:i', strtotime($u['last_login'])) : '-' ?>
</td>
<td>
<div class="dropdown">
<button class="btn btn-icon btn-ghost" onclick="toggleDropdown(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
</button>
<div class="dropdown-menu">
<a href="/users/<?= $u['id'] ?>" class="dropdown-item">عرض</a>
<a href="/users/<?= $u['id'] ?>/edit" class="dropdown-item">تعديل</a>
<button class="dropdown-item" onclick="toggleUserStatus('<?= $u['id'] ?>', '<?= View::e($u['display_name'] ?? $u['username']) ?>', <?= ($u['is_active'] ?? false) ? 'true' : 'false' ?>)">
<?= ($u['is_active'] ?? false) ? 'تعطيل' : 'تفعيل' ?>
</button>
<button class="dropdown-item" onclick="resetUserPassword('<?= $u['id'] ?>', '<?= View::e($u['display_name'] ?? $u['username']) ?>')">إعادة تعيين كلمة المرور</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger" onclick="confirmDelete('/users/<?= $u['id'] ?>/delete', '<?= View::e($u['username']) ?>')">حذف</button>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&per_page=<?= $pagination->perPage ?>&search=<?= urlencode($search) ?>&role=<?= urlencode($role) ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($org) ?>" 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 ?>&search=<?= urlencode($search) ?>&role=<?= urlencode($role) ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($org) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&per_page=<?= $pagination->perPage ?>&search=<?= urlencode($search) ?>&role=<?= urlencode($role) ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($org) ?>" 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; ?>
</div>
<?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>
/* Workflows module styles */
/* ===== Timeline ===== */
.workflow-timeline {
position: relative;
padding-right: var(--space-6);
}
.workflow-timeline::before {
content: '';
position: absolute;
right: 7px;
top: 8px;
bottom: 8px;
width: 2px;
background: var(--color-border);
}
.timeline-item {
position: relative;
padding-right: var(--space-8);
padding-bottom: var(--space-6);
}
.timeline-item:last-child {
padding-bottom: 0;
}
.timeline-dot {
position: absolute;
right: -var(--space-6);
right: calc(-1 * var(--space-6) + 1px);
top: 4px;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--color-border);
background: var(--color-bg-card);
z-index: 1;
}
.timeline-dot-info {
border-color: var(--color-info);
background: var(--color-info);
}
.timeline-dot-success {
border-color: var(--color-success);
background: var(--color-success);
}
.timeline-dot-danger {
border-color: var(--color-danger);
background: var(--color-danger);
}
.timeline-dot-warning {
border-color: var(--color-warning);
background: var(--color-warning);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.timeline-content {
padding-top: 0;
}
.timeline-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--color-text-primary);
margin-bottom: var(--space-1);
}
.timeline-meta {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: 0.8rem;
color: var(--color-text-secondary);
}
.timeline-date {
color: var(--color-text-muted);
}
.timeline-note {
margin-top: var(--space-2);
padding: var(--space-3);
border-radius: var(--radius-md);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.timeline-note-danger {
background: rgba(var(--color-danger-rgb, 239, 68, 68), 0.05);
border-color: rgba(var(--color-danger-rgb, 239, 68, 68), 0.2);
}
.timeline-item-pending {
opacity: 0.6;
}
/* ===== Request Data Grid ===== */
.request-data-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-4);
}
.request-data-item {
padding: var(--space-3);
border-radius: var(--radius-md);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
}
.request-data-label {
font-size: 0.75rem;
font-weight: 500;
color: var(--color-text-muted);
margin-bottom: var(--space-1);
text-transform: uppercase;
}
.request-data-value {
font-size: 0.9rem;
font-weight: 500;
color: var(--color-text-primary);
word-break: break-word;
}
.request-data-list {
list-style: disc;
padding-right: var(--space-4);
margin: 0;
}
.request-data-list li {
font-size: 0.85rem;
margin-bottom: var(--space-1);
}
/* ===== Rules Grid ===== */
.rules-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: var(--space-5);
}
.rule-card {
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
background: var(--color-bg-card);
overflow: hidden;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.rule-card:hover {
border-color: var(--color-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.rule-card-inactive {
opacity: 0.65;
}
.rule-card-inactive:hover {
opacity: 0.85;
}
.rule-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.rule-card-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--color-text-primary);
margin: 0;
}
.rule-icon {
width: 36px;
height: 36px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.rule-icon-active {
background: rgba(var(--color-success-rgb, 34, 197, 94), 0.1);
color: var(--color-success);
}
.rule-icon-inactive {
background: var(--color-bg-elevated);
color: var(--color-text-muted);
}
.rule-card-body {
padding: var(--space-4);
}
.rule-section {
margin-bottom: var(--space-3);
}
.rule-section:last-child {
margin-bottom: 0;
}
.rule-section-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
color: var(--color-text-muted);
margin-bottom: var(--space-2);
letter-spacing: 0.5px;
}
.rule-conditions {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.rule-condition-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.8rem;
padding: var(--space-1) var(--space-2);
background: var(--color-bg-elevated);
border-radius: var(--radius-sm);
}
.rule-condition-item code {
font-size: 0.75rem;
padding: 1px 4px;
border-radius: 3px;
background: var(--color-bg-card);
border: 1px solid var(--color-border);
}
.rule-condition-op {
color: var(--color-primary);
font-weight: 600;
font-size: 0.7rem;
}
.rule-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--color-border);
background: var(--color-bg-elevated);
}
/* ===== Toggle Switch ===== */
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
inset: 0;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: 22px;
transition: 0.3s;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 16px;
width: 16px;
right: 2px;
bottom: 2px;
background: var(--color-text-muted);
border-radius: 50%;
transition: 0.3s;
}
.toggle-switch input:checked + .toggle-slider {
background: var(--color-primary);
border-color: var(--color-primary);
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(-18px);
background: white;
}
/* Toggle label inline */
.toggle-label {
display: flex;
align-items: center;
gap: var(--space-3);
cursor: pointer;
}
.toggle-switch-inline {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: 22px;
transition: 0.3s;
flex-shrink: 0;
}
.toggle-switch-inline::before {
content: '';
position: absolute;
height: 16px;
width: 16px;
right: 2px;
bottom: 2px;
background: var(--color-text-muted);
border-radius: 50%;
transition: 0.3s;
}
.toggle-label input:checked + .toggle-switch-inline {
background: var(--color-primary);
border-color: var(--color-primary);
}
.toggle-label input:checked + .toggle-switch-inline::before {
transform: translateX(-18px);
background: white;
}
.toggle-label input {
display: none;
}
/* ===== Conditions Builder ===== */
.conditions-builder {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.condition-row {
display: grid;
grid-template-columns: 1fr 140px 1fr auto;
gap: var(--space-3);
align-items: center;
padding: var(--space-3);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.condition-row .form-select,
.condition-row .form-input {
font-size: 0.85rem;
padding: var(--space-2) var(--space-3);
}
/* ===== Actions Checkboxes ===== */
.actions-checkboxes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: var(--space-3);
}
.action-checkbox-label {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.action-checkbox-label:hover {
border-color: var(--color-primary);
background: var(--color-bg-elevated);
}
.action-checkbox-label input {
display: none;
}
.action-checkbox-box {
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: 0.2s;
}
.action-checkbox-box svg {
opacity: 0;
transition: 0.2s;
}
.action-checkbox-label input:checked + .action-checkbox-box {
background: var(--color-primary);
border-color: var(--color-primary);
}
.action-checkbox-label input:checked + .action-checkbox-box svg {
opacity: 1;
color: white;
}
.action-checkbox-text {
font-size: 0.85rem;
font-weight: 500;
color: var(--color-text-primary);
}
/* ===== My Requests Timeline ===== */
.my-requests-timeline {
display: flex;
flex-direction: column;
gap: 0;
}
.my-request-card {
display: flex;
gap: var(--space-4);
}
.my-request-status {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 20px;
}
.my-request-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
margin-top: var(--space-4);
}
.my-request-dot-pending {
background: var(--color-warning);
animation: pulse 2s infinite;
}
.my-request-dot-approved {
background: var(--color-success);
}
.my-request-dot-rejected {
background: var(--color-danger);
}
.my-request-line {
width: 2px;
flex: 1;
background: var(--color-border);
margin-top: var(--space-2);
}
.my-request-card:last-child .my-request-line {
display: none;
}
.my-request-content {
flex: 1;
padding: var(--space-4);
padding-bottom: var(--space-6);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
background: var(--color-bg-card);
margin-bottom: var(--space-3);
}
.my-request-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
.my-request-body {
margin-bottom: var(--space-2);
}
.my-request-note {
margin-top: var(--space-3);
padding: var(--space-3);
border-radius: var(--radius-md);
font-size: 0.85rem;
}
.my-request-note-danger {
background: rgba(var(--color-danger-rgb, 239, 68, 68), 0.05);
border: 1px solid rgba(var(--color-danger-rgb, 239, 68, 68), 0.15);
color: var(--color-danger);
}
.my-request-note-success {
background: rgba(var(--color-success-rgb, 34, 197, 94), 0.05);
border: 1px solid rgba(var(--color-success-rgb, 34, 197, 94), 0.15);
color: var(--color-success);
}
/* ===== Status Colors ===== */
.text-success { color: var(--color-success); }
.text-danger { color: var(--color-danger); }
.text-warning { color: var(--color-warning); }
/* ===== Modal Overrides ===== */
.modal {
display: none;
position: fixed;
inset: 0;
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
.modal-content {
position: relative;
background: var(--color-bg-card);
border-radius: var(--radius-lg);
width: 100%;
max-width: 480px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid var(--color-border);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-border);
}
.modal-header h3 {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.modal-body {
padding: var(--space-5);
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-top: 1px solid var(--color-border);
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.request-data-grid {
grid-template-columns: 1fr;
}
.rules-grid {
grid-template-columns: 1fr;
}
.condition-row {
grid-template-columns: 1fr;
}
.actions-checkboxes {
grid-template-columns: 1fr;
}
}
// 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);
});
})();
<?php
class WorkflowsController
{
private Database $db;
private array $typeLabels = [
'tournament_create' => 'إنشاء بطولة',
'ad_create' => 'إنشاء إعلان',
'org_verify' => 'توثيق منظمة',
'economy_grant' => 'منح اقتصادي',
'member_add' => 'إضافة عضو',
];
private array $statusLabels = [
'pending' => 'قيد الانتظار',
'approved' => 'موافق عليه',
'rejected' => 'مرفوض',
];
private array $eventLabels = [
'user.login' => 'تسجيل دخول',
'tournament.created' => 'إنشاء بطولة',
'tournament.completed' => 'اكتمال بطولة',
'player.banned' => 'حظر لاعب',
'report.submitted' => 'تقديم بلاغ',
'org.verified' => 'توثيق منظمة',
'ad.created' => 'إنشاء إعلان',
'economy.grant' => 'منح اقتصادي',
'member.added' => 'إضافة عضو',
];
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$status = $_GET['status'] ?? '';
$type = $_GET['type'] ?? '';
$search = $_GET['search'] ?? '';
$queryParams = ['select' => '*', 'order' => 'created_at.desc'];
if ($status) {
$queryParams['status'] = "eq.{$status}";
}
if ($type) {
$queryParams['type'] = "eq.{$type}";
}
if ($search) {
$queryParams['or'] = "(data->>name.ilike.*{$search}*,requester_id.ilike.*{$search}*)";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('approval_requests', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$requests = $this->db->select('approval_requests', $queryParams);
$pendingCount = $this->db->count('approval_requests', ['status' => 'eq.pending']);
$pageTitle = 'الموافقات';
$moduleCSS = 'workflows';
$moduleJS = 'workflows';
View::render('workflows/list', compact(
'requests', 'pagination', 'status', 'type', 'search',
'pendingCount', 'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function show(array $params, string $method): void
{
$id = $params['id'];
$request = $this->db->selectOne('approval_requests', ['id' => "eq.{$id}"]);
if (!$request) {
http_response_code(404);
View::render('errors/404');
return;
}
$pageTitle = 'تفاصيل الطلب';
$moduleCSS = 'workflows';
$moduleJS = 'workflows';
View::render('workflows/show', compact('request', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
public function approve(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('moderator');
$id = $params['id'];
$request = $this->db->selectOne('approval_requests', ['id' => "eq.{$id}"]);
if (!$request) {
Response::error('الطلب غير موجود', '/workflows');
return;
}
if ($request['status'] !== 'pending') {
Response::error('هذا الطلب تمت معالجته مسبقاً', '/workflows');
return;
}
$note = trim($_POST['review_note'] ?? '');
$data = [
'status' => 'approved',
'reviewer_id' => Auth::user()['username'],
'review_note' => $note,
'reviewed_at' => date('c'),
];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
AuditLog::log('approve', 'approval_request', $id, $request, $data);
Response::success('تمت الموافقة على الطلب بنجاح', '/workflows');
}
public function reject(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('moderator');
$id = $params['id'];
$request = $this->db->selectOne('approval_requests', ['id' => "eq.{$id}"]);
if (!$request) {
Response::error('الطلب غير موجود', '/workflows');
return;
}
if ($request['status'] !== 'pending') {
Response::error('هذا الطلب تمت معالجته مسبقاً', '/workflows');
return;
}
$note = trim($_POST['review_note'] ?? '');
if (empty($note)) {
Response::error('يجب إدخال سبب الرفض', "/workflows/{$id}");
return;
}
$data = [
'status' => 'rejected',
'reviewer_id' => Auth::user()['username'],
'review_note' => $note,
'reviewed_at' => date('c'),
];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
AuditLog::log('reject', 'approval_request', $id, $request, $data);
Response::success('تم رفض الطلب', '/workflows');
}
public function bulkApprove(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('admin');
$ids = array_filter(explode(',', $_POST['ids'] ?? ''));
if (empty($ids)) {
Response::error('لم يتم اختيار أي طلبات', '/workflows');
return;
}
$count = 0;
$note = trim($_POST['review_note'] ?? 'موافقة جماعية');
foreach ($ids as $id) {
$request = $this->db->selectOne('approval_requests', [
'id' => "eq.{$id}",
'status' => 'eq.pending',
]);
if (!$request) continue;
$data = [
'status' => 'approved',
'reviewer_id' => Auth::user()['username'],
'review_note' => $note,
'reviewed_at' => date('c'),
];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
AuditLog::log('approve', 'approval_request', $id, $request, $data);
$count++;
}
Response::success("تمت الموافقة على {$count} طلب بنجاح", '/workflows');
}
public function rules(array $params, string $method): void
{
Auth::requireRole('admin');
$rules = $this->db->select('workflow_rules', [
'select' => '*',
'order' => 'priority.asc,created_at.desc',
]);
$pageTitle = 'قواعد سير العمل';
$moduleCSS = 'workflows';
$moduleJS = 'workflows';
View::render('workflows/rules', compact('rules', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
public function createRule(array $params, string $method): void
{
Auth::requireRole('admin');
$rule = [];
$pageTitle = 'إنشاء قاعدة جديدة';
$moduleCSS = 'workflows';
$moduleJS = 'workflows';
View::render('workflows/rule-form', compact('rule', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
public function storeRule(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('admin');
$validator = Validator::make($_POST)
->required('name', 'اسم القاعدة')
->minLength('name', 2, 'اسم القاعدة')
->required('event', 'الحدث');
if ($validator->fails()) {
Response::error($validator->firstError(), '/workflows/rules/create');
return;
}
$conditions = [];
if (!empty($_POST['condition_field'])) {
foreach ($_POST['condition_field'] as $i => $field) {
if (empty($field)) continue;
$conditions[] = [
'field' => $field,
'operator' => $_POST['condition_operator'][$i] ?? 'eq',
'value' => $_POST['condition_value'][$i] ?? '',
];
}
}
$actions = $_POST['actions'] ?? [];
$data = [
'name' => trim($_POST['name']),
'event' => trim($_POST['event']),
'conditions' => json_encode($conditions, JSON_UNESCAPED_UNICODE),
'actions' => json_encode($actions, JSON_UNESCAPED_UNICODE),
'is_active' => isset($_POST['is_active']),
'priority' => (int)($_POST['priority'] ?? 0),
'created_by' => Auth::user()['username'],
];
$result = $this->db->insert('workflow_rules', $data);
AuditLog::log('create', 'workflow_rule', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء القاعدة بنجاح', '/workflows/rules');
}
public function toggleRule(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('admin');
$id = $params['id'];
$rule = $this->db->selectOne('workflow_rules', ['id' => "eq.{$id}"]);
if (!$rule) {
Response::error('القاعدة غير موجودة', '/workflows/rules');
return;
}
$newState = !($rule['is_active'] ?? false);
$this->db->update('workflow_rules', ['id' => "eq.{$id}"], ['is_active' => $newState]);
AuditLog::log('toggle', 'workflow_rule', $id, $rule, ['is_active' => $newState]);
$msg = $newState ? 'تم تفعيل القاعدة' : 'تم تعطيل القاعدة';
Response::success($msg, '/workflows/rules');
}
public function deleteRule(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('admin');
$id = $params['id'];
$rule = $this->db->selectOne('workflow_rules', ['id' => "eq.{$id}"]);
if (!$rule) {
Response::error('القاعدة غير موجودة', '/workflows/rules');
return;
}
$this->db->delete('workflow_rules', ['id' => "eq.{$id}"]);
AuditLog::log('delete', 'workflow_rule', $id, $rule);
Response::success('تم حذف القاعدة', '/workflows/rules');
}
public function myRequests(array $params, string $method): void
{
$userId = Auth::user()['username'];
$queryParams = [
'select' => '*',
'requester_id' => "eq.{$userId}",
'order' => 'created_at.desc',
];
$total = $this->db->count('approval_requests', ['requester_id' => "eq.{$userId}"]);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$requests = $this->db->select('approval_requests', $queryParams);
$pageTitle = 'طلباتي';
$moduleCSS = 'workflows';
$moduleJS = 'workflows';
View::render('workflows/my-requests', compact('requests', 'pagination', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<h1>الموافقات</h1>
<?php if ($pendingCount > 0): ?>
<span class="badge badge-warning"><?= $pendingCount ?> قيد الانتظار</span>
<?php endif; ?>
</div>
<div class="flex gap-3">
<a href="/workflows/my-requests" class="btn btn-ghost">طلباتي</a>
<a href="/workflows/rules" class="btn btn-ghost">قواعد سير العمل</a>
</div>
</div>
<!-- Filter Pills -->
<div class="filter-pills mb-5">
<a href="/workflows" class="filter-pill <?= empty($status) ? 'active' : '' ?>">الكل</a>
<a href="/workflows?status=pending&type=<?= urlencode($type) ?>" class="filter-pill <?= $status === 'pending' ? 'active' : '' ?>">قيد الانتظار</a>
<a href="/workflows?status=approved&type=<?= urlencode($type) ?>" class="filter-pill <?= $status === 'approved' ? 'active' : '' ?>">موافق عليها</a>
<a href="/workflows?status=rejected&type=<?= urlencode($type) ?>" class="filter-pill <?= $status === 'rejected' ? 'active' : '' ?>">مرفوضة</a>
</div>
<div class="data-table-wrapper">
<div class="table-toolbar">
<div class="table-search">
<svg class="table-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" placeholder="بحث..." value="<?= View::e($search) ?>" id="workflowSearch">
</div>
<div class="table-actions flex gap-3">
<select class="form-select" style="width: auto; padding: var(--space-2) var(--space-4);" onchange="location.href='/workflows?status=<?= urlencode($status) ?>&type='+this.value">
<option value="">كل الأنواع</option>
<option value="tournament_create" <?= $type === 'tournament_create' ? 'selected' : '' ?>>إنشاء بطولة</option>
<option value="ad_create" <?= $type === 'ad_create' ? 'selected' : '' ?>>إنشاء إعلان</option>
<option value="org_verify" <?= $type === 'org_verify' ? 'selected' : '' ?>>توثيق منظمة</option>
<option value="economy_grant" <?= $type === 'economy_grant' ? 'selected' : '' ?>>منح اقتصادي</option>
<option value="member_add" <?= $type === 'member_add' ? 'selected' : '' ?>>إضافة عضو</option>
</select>
<button class="btn btn-success btn-sm" id="bulkApproveBtn" style="display:none;" onclick="bulkApprove()">
<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>
موافقة جماعية
</button>
</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="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2"/></svg>
<h3 class="empty-state-title">لا توجد طلبات</h3>
<p class="empty-state-text">لم يتم العثور على أي طلبات موافقة<?= $status ? ' بهذا الفلتر' : '' ?></p>
</div>
<?php else: ?>
<table class="data-table" id="workflowsTable">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" id="selectAll" onchange="toggleSelectAll(this)">
</th>
<th>مقدم الطلب</th>
<th>النوع</th>
<th>المنظمة</th>
<th>الملخص</th>
<th>الحالة</th>
<th>التاريخ</th>
<th style="width: 120px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $req): ?>
<?php
$data = is_string($req['data'] ?? null) ? json_decode($req['data'], true) : ($req['data'] ?? []);
$typeLabels = [
'tournament_create' => 'إنشاء بطولة',
'ad_create' => 'إنشاء إعلان',
'org_verify' => 'توثيق منظمة',
'economy_grant' => 'منح اقتصادي',
'member_add' => 'إضافة عضو',
];
$typeBadges = [
'tournament_create' => 'badge-info',
'ad_create' => 'badge-purple',
'org_verify' => 'badge-warning',
'economy_grant' => 'badge-success',
'member_add' => 'badge-default',
];
$statusLabels = [
'pending' => 'قيد الانتظار',
'approved' => 'موافق عليه',
'rejected' => 'مرفوض',
];
$statusBadges = [
'pending' => 'badge-warning',
'approved' => 'badge-success',
'rejected' => 'badge-danger',
];
$roleLabels = [
'superadmin' => 'مسؤول أعلى',
'admin' => 'مدير',
'moderator' => 'مشرف',
'org_admin' => 'مدير منظمة',
'org_manager' => 'مدير عمليات',
'tournament_organizer' => 'منظم بطولات',
'sponsor' => 'راعي',
'charity' => 'جمعية خيرية',
'viewer' => 'مشاهد',
];
$reqType = $req['type'] ?? '';
$reqStatus = $req['status'] ?? 'pending';
// Build summary from data
$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');
}
?>
<tr>
<td>
<?php if ($reqStatus === 'pending'): ?>
<input type="checkbox" class="row-checkbox" value="<?= $req['id'] ?>" onchange="updateBulkBtn()">
<?php endif; ?>
</td>
<td>
<div class="flex items-center gap-2">
<div class="avatar avatar-sm"><?= mb_substr($req['requester_id'] ?? '?', 0, 1) ?></div>
<div>
<div class="font-medium text-sm"><?= View::e($req['requester_id'] ?? '-') ?></div>
<span class="badge badge-default text-xs"><?= $roleLabels[$req['requester_role'] ?? ''] ?? ($req['requester_role'] ?? '-') ?></span>
</div>
</div>
</td>
<td>
<span class="badge <?= $typeBadges[$reqType] ?? 'badge-default' ?>"><?= $typeLabels[$reqType] ?? $reqType ?></span>
</td>
<td class="text-sm"><?= View::e($req['org_id'] ?? '-') ?></td>
<td class="text-sm"><?= View::e($summary) ?></td>
<td>
<span class="badge <?= $statusBadges[$reqStatus] ?? 'badge-default' ?>"><?= $statusLabels[$reqStatus] ?? $reqStatus ?></span>
</td>
<td class="text-xs tabular-nums"><?= !empty($req['created_at']) ? date('Y-m-d H:i', strtotime($req['created_at'])) : '-' ?></td>
<td>
<div class="flex gap-2">
<a href="/workflows/<?= $req['id'] ?>" class="btn btn-icon btn-ghost" title="عرض">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
</a>
<?php if ($reqStatus === 'pending'): ?>
<button class="btn btn-icon btn-ghost text-success" title="موافقة" onclick="quickApprove('<?= $req['id'] ?>')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</button>
<button class="btn btn-icon btn-ghost text-danger" title="رفض" onclick="showRejectModal('<?= $req['id'] ?>')">
<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>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&per_page=<?= $pagination->perPage ?>&status=<?= urlencode($status) ?>&type=<?= urlencode($type) ?>&search=<?= urlencode($search) ?>" 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 ?>&status=<?= urlencode($status) ?>&type=<?= urlencode($type) ?>&search=<?= urlencode($search) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&per_page=<?= $pagination->perPage ?>&status=<?= urlencode($status) ?>&type=<?= urlencode($type) ?>&search=<?= urlencode($search) ?>" 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; ?>
</div>
<!-- Approve Modal -->
<div class="modal" id="approveModal">
<div class="modal-backdrop" onclick="closeModal('approveModal')"></div>
<div class="modal-content">
<div class="modal-header">
<h3>تأكيد الموافقة</h3>
<button class="btn btn-icon btn-ghost" onclick="closeModal('approveModal')">
<svg width="20" height="20" 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>
<form method="POST" id="approveForm">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="modal-body">
<div class="form-group">
<label class="form-label">ملاحظة (اختياري)</label>
<textarea name="review_note" class="form-input" rows="3" placeholder="أضف ملاحظة للموافقة..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" onclick="closeModal('approveModal')">إلغاء</button>
<button type="submit" class="btn btn-success">موافقة</button>
</div>
</form>
</div>
</div>
<!-- Reject Modal -->
<div class="modal" id="rejectModal">
<div class="modal-backdrop" onclick="closeModal('rejectModal')"></div>
<div class="modal-content">
<div class="modal-header">
<h3>رفض الطلب</h3>
<button class="btn btn-icon btn-ghost" onclick="closeModal('rejectModal')">
<svg width="20" height="20" 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>
<form method="POST" id="rejectForm">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="modal-body">
<div class="form-group">
<label class="form-label">سبب الرفض <span class="text-danger">*</span></label>
<textarea name="review_note" class="form-input" rows="3" placeholder="أدخل سبب الرفض..." required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" onclick="closeModal('rejectModal')">إلغاء</button>
<button type="submit" class="btn btn-danger">رفض</button>
</div>
</form>
</div>
</div>
<!-- Bulk Approve Modal -->
<div class="modal" id="bulkApproveModal">
<div class="modal-backdrop" onclick="closeModal('bulkApproveModal')"></div>
<div class="modal-content">
<div class="modal-header">
<h3>موافقة جماعية</h3>
<button class="btn btn-icon btn-ghost" onclick="closeModal('bulkApproveModal')">
<svg width="20" height="20" 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>
<form method="POST" action="/workflows/bulk-approve" id="bulkApproveForm">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="ids" id="bulkIds" value="">
<div class="modal-body">
<p class="mb-4">سيتم الموافقة على <strong id="bulkCount">0</strong> طلب</p>
<div class="form-group">
<label class="form-label">ملاحظة (اختياري)</label>
<textarea name="review_note" class="form-input" rows="3" placeholder="ملاحظة للموافقة الجماعية..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" onclick="closeModal('bulkApproveModal')">إلغاء</button>
<button type="submit" class="btn btn-success">موافقة على الكل</button>
</div>
</form>
</div>
</div>
<?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; ?>
<?php
$data = is_string($request['data'] ?? null) ? json_decode($request['data'], true) : ($request['data'] ?? []);
$reqStatus = $request['status'] ?? 'pending';
$reqType = $request['type'] ?? '';
$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',
];
$roleLabels = [
'superadmin' => 'مسؤول أعلى',
'admin' => 'مدير',
'moderator' => 'مشرف',
'org_admin' => 'مدير منظمة',
'org_manager' => 'مدير عمليات',
'tournament_organizer' => 'منظم بطولات',
'sponsor' => 'راعي',
'charity' => 'جمعية خيرية',
'viewer' => 'مشاهد',
];
// Data field labels by type
$fieldLabels = [
'tournament_create' => [
'name' => 'اسم البطولة',
'game' => 'اللعبة',
'max_players' => 'أقصى عدد لاعبين',
'prize_pool' => 'الجوائز',
'prize_currency' => 'عملة الجوائز',
'start_date' => 'تاريخ البداية',
],
'ad_create' => [
'campaign_name' => 'اسم الحملة',
'budget' => 'الميزانية',
'target_audience' => 'الجمهور المستهدف',
'duration_days' => 'المدة (أيام)',
'ad_type' => 'نوع الإعلان',
],
'org_verify' => [
'org_name' => 'اسم المنظمة',
'documents_submitted' => 'المستندات المقدمة',
'verification_type' => 'نوع التوثيق',
],
'economy_grant' => [
'target_player' => 'اللاعب المستهدف',
'amount' => 'المبلغ',
'currency' => 'العملة',
'reason' => 'السبب',
],
'member_add' => [
'org_id' => 'معرف المنظمة',
'player_id' => 'معرف اللاعب',
'role' => 'الدور في المنظمة',
],
];
$currentFieldLabels = $fieldLabels[$reqType] ?? [];
?>
<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>
<span class="badge <?= $statusBadges[$reqStatus] ?? 'badge-default' ?>"><?= $statusLabels[$reqStatus] ?? $reqStatus ?></span>
</div>
<?php if ($reqStatus === 'pending'): ?>
<div class="flex gap-3">
<button class="btn btn-success" onclick="showModal('approveModal')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
موافقة
</button>
<button class="btn btn-danger" onclick="showModal('rejectModal')">
<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 endif; ?>
</div>
<div class="grid grid-3 mb-6">
<!-- Requester Info Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">معلومات مقدم الطلب</h3>
</div>
<div class="flex flex-col gap-3 text-sm p-4">
<div class="flex items-center gap-3 mb-3">
<div class="avatar avatar-lg"><?= mb_substr($request['requester_id'] ?? '?', 0, 1) ?></div>
<div>
<div class="font-semibold"><?= View::e($request['requester_id'] ?? '-') ?></div>
<span class="badge badge-default"><?= $roleLabels[$request['requester_role'] ?? ''] ?? ($request['requester_role'] ?? '-') ?></span>
</div>
</div>
<div class="flex justify-between">
<span class="text-secondary">نوع الطلب</span>
<span class="font-medium"><?= $typeLabels[$reqType] ?? $reqType ?></span>
</div>
<?php if (!empty($request['org_id'])): ?>
<div class="flex justify-between">
<span class="text-secondary">المنظمة</span>
<span class="font-medium"><?= View::e($request['org_id']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($request['resource_type'])): ?>
<div class="flex justify-between">
<span class="text-secondary">نوع المورد</span>
<span class="font-medium"><?= View::e($request['resource_type']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($request['resource_id'])): ?>
<div class="flex justify-between">
<span class="text-secondary">معرف المورد</span>
<span class="font-medium" dir="ltr"><?= View::e($request['resource_id']) ?></span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Request Data Card -->
<div class="card" style="grid-column: span 2;">
<div class="card-header">
<h3 class="card-title">بيانات الطلب</h3>
</div>
<div class="p-4">
<?php if (!empty($data)): ?>
<div class="request-data-grid">
<?php foreach ($data as $key => $value): ?>
<div class="request-data-item">
<div class="request-data-label"><?= View::e($currentFieldLabels[$key] ?? $key) ?></div>
<div class="request-data-value">
<?php if (is_array($value)): ?>
<ul class="request-data-list">
<?php foreach ($value as $item): ?>
<li><?= View::e(is_string($item) ? $item : json_encode($item, JSON_UNESCAPED_UNICODE)) ?></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<?= View::e((string)$value) ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-secondary text-center">لا توجد بيانات إضافية</p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Timeline -->
<div class="card">
<div class="card-header">
<h3 class="card-title">المخطط الزمني</h3>
</div>
<div class="p-4">
<div class="workflow-timeline">
<!-- Submitted -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot-info"></div>
<div class="timeline-content">
<div class="timeline-title">تم تقديم الطلب</div>
<div class="timeline-meta">
<span><?= View::e($request['requester_id'] ?? '-') ?></span>
<span class="timeline-date"><?= !empty($request['created_at']) ? date('Y-m-d H:i', strtotime($request['created_at'])) : '-' ?></span>
</div>
</div>
</div>
<?php if ($reqStatus === 'approved'): ?>
<!-- Approved -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot-success"></div>
<div class="timeline-content">
<div class="timeline-title">تمت الموافقة</div>
<div class="timeline-meta">
<span>بواسطة: <?= View::e($request['reviewer_id'] ?? '-') ?></span>
<span class="timeline-date"><?= !empty($request['reviewed_at']) ? date('Y-m-d H:i', strtotime($request['reviewed_at'])) : '-' ?></span>
</div>
<?php if (!empty($request['review_note'])): ?>
<div class="timeline-note"><?= View::e($request['review_note']) ?></div>
<?php endif; ?>
</div>
</div>
<?php elseif ($reqStatus === 'rejected'): ?>
<!-- Rejected -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot-danger"></div>
<div class="timeline-content">
<div class="timeline-title">تم الرفض</div>
<div class="timeline-meta">
<span>بواسطة: <?= View::e($request['reviewer_id'] ?? '-') ?></span>
<span class="timeline-date"><?= !empty($request['reviewed_at']) ? date('Y-m-d H:i', strtotime($request['reviewed_at'])) : '-' ?></span>
</div>
<?php if (!empty($request['review_note'])): ?>
<div class="timeline-note timeline-note-danger"><?= View::e($request['review_note']) ?></div>
<?php endif; ?>
</div>
</div>
<?php elseif ($reqStatus === 'pending'): ?>
<!-- Pending -->
<div class="timeline-item timeline-item-pending">
<div class="timeline-dot timeline-dot-warning"></div>
<div class="timeline-content">
<div class="timeline-title text-secondary">في انتظار المراجعة</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($request['resource_type']) && !empty($request['resource_id'])): ?>
<!-- Related Resource Link -->
<div class="card mt-4">
<div class="card-header">
<h3 class="card-title">المورد المرتبط</h3>
</div>
<div class="p-4">
<?php
$resourceLinks = [
'tournament' => '/tournaments/',
'ad' => '/ads/',
'organization' => '/organizations/',
'player' => '/players/',
];
$resourceType = $request['resource_type'];
$resourceId = $request['resource_id'];
$link = ($resourceLinks[$resourceType] ?? '#') . $resourceId;
?>
<a href="<?= $link ?>" class="btn btn-ghost">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
عرض <?= View::e($resourceType) ?> #<?= View::e($resourceId) ?>
</a>
</div>
</div>
<?php endif; ?>
<!-- Approve Modal -->
<div class="modal" id="approveModal">
<div class="modal-backdrop" onclick="closeModal('approveModal')"></div>
<div class="modal-content">
<div class="modal-header">
<h3>تأكيد الموافقة</h3>
<button class="btn btn-icon btn-ghost" onclick="closeModal('approveModal')">
<svg width="20" height="20" 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>
<form method="POST" action="/workflows/<?= $request['id'] ?>/approve">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="modal-body">
<p class="mb-4">هل تريد الموافقة على هذا الطلب؟</p>
<div class="form-group">
<label class="form-label">ملاحظة (اختياري)</label>
<textarea name="review_note" class="form-input" rows="3" placeholder="أضف ملاحظة للموافقة..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" onclick="closeModal('approveModal')">إلغاء</button>
<button type="submit" class="btn btn-success">تأكيد الموافقة</button>
</div>
</form>
</div>
</div>
<!-- Reject Modal -->
<div class="modal" id="rejectModal">
<div class="modal-backdrop" onclick="closeModal('rejectModal')"></div>
<div class="modal-content">
<div class="modal-header">
<h3>رفض الطلب</h3>
<button class="btn btn-icon btn-ghost" onclick="closeModal('rejectModal')">
<svg width="20" height="20" 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>
<form method="POST" action="/workflows/<?= $request['id'] ?>/reject">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="modal-body">
<div class="form-group">
<label class="form-label">سبب الرفض <span class="text-danger">*</span></label>
<textarea name="review_note" class="form-input" rows="3" placeholder="أدخل سبب رفض هذا الطلب..." required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" onclick="closeModal('rejectModal')">إلغاء</button>
<button type="submit" class="btn btn-danger">تأكيد الرفض</button>
</div>
</form>
</div>
</div>
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