Commit c08ba733 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: organizational steroids update — 18 new modules + bugfixes

- Add 18 org ecosystem modules: frames, invites, applications, chat,
  treasury, challenges, achievements, transfers, recruitment, events,
  content, leaderboards, partnerships, loyalty, rosters, training,
  media, spotlights (controllers + views + CSS assets)
- Add core services: SupabaseStorage, OrgPermissions, ThemeService
- Add migration 003 (23 new tables, RLS policies, indexes)
- Fix games edit not saving (wrong column names: icon→icon_url,
  matchmaking_config→config)
- Fix branding colors not reflecting (added ThemeService to inject
  DB theme colors as CSS variable overrides at render time)
- Update permissions.php with new module access for admin/org roles
- Update sidebar with org ecosystem navigation section
- Add 120+ new routes for all org modules
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8b678c5b
.DS_Store .DS_Store
storage/uploads/* storage/uploads/*
!storage/uploads/.gitkeep !storage/uploads/.gitkeep
storage/cache/*
!storage/cache/.gitkeep
...@@ -35,6 +35,25 @@ return [ ...@@ -35,6 +35,25 @@ return [
'analytics.*', 'analytics.*',
'audit-log.*', 'audit-log.*',
'reports.*', 'reports.*',
'org-frames.*',
'org-invites.*',
'org-applications.*',
'org-chat.*',
'org-treasury.*',
'org-challenges.*',
'org-achievements.*',
'org-transfers.*',
'org-recruitment.*',
'org-events.*',
'org-content.*',
'org-leaderboards.*',
'org-partnerships.*',
'org-loyalty.*',
'org-rosters.*',
'org-training.*',
'org-media.*',
'org-spotlights.*',
'org-announcements.*',
], ],
// ─── Moderation focused ────────────────────────────────────────────── // ─── Moderation focused ──────────────────────────────────────────────
...@@ -75,6 +94,25 @@ return [ ...@@ -75,6 +94,25 @@ return [
'notifications.own', 'notifications.own',
'economy.list', 'economy.list',
'economy.show', 'economy.show',
'org-frames.list',
'org-frames.show',
'org-invites.*',
'org-applications.*',
'org-chat.*',
'org-treasury.*',
'org-challenges.*',
'org-achievements.*',
'org-recruitment.*',
'org-events.*',
'org-content.*',
'org-leaderboards.*',
'org-partnerships.*',
'org-loyalty.*',
'org-rosters.*',
'org-training.*',
'org-media.*',
'org-spotlights.*',
'org-announcements.*',
], ],
// ─── Org manager: like org_admin without delete & member write ─────── // ─── Org manager: like org_admin without delete & member write ───────
...@@ -97,6 +135,32 @@ return [ ...@@ -97,6 +135,32 @@ return [
'notifications.own', 'notifications.own',
'economy.list', 'economy.list',
'economy.show', 'economy.show',
'org-invites.list',
'org-invites.create',
'org-applications.list',
'org-applications.show',
'org-chat.list',
'org-chat.show',
'org-treasury.list',
'org-challenges.list',
'org-challenges.show',
'org-achievements.list',
'org-recruitment.list',
'org-recruitment.create',
'org-events.list',
'org-events.create',
'org-content.list',
'org-content.create',
'org-leaderboards.list',
'org-rosters.list',
'org-rosters.show',
'org-training.list',
'org-training.create',
'org-media.list',
'org-media.create',
'org-spotlights.list',
'org-announcements.list',
'org-announcements.create',
], ],
// ─── Tournament organizer: manages own tournaments ─────────────────── // ─── Tournament organizer: manages own tournaments ───────────────────
......
This diff is collapsed.
<?php
class OrgPermissions
{
private static array $roleHierarchy = [
'owner' => 4,
'admin' => 3,
'arbiter' => 2,
'member' => 1,
];
private static array $rolePermissions = [
'owner' => ['*'],
'admin' => [
'members.manage', 'members.invite', 'members.mute', 'members.kick',
'chat.manage', 'chat.moderate', 'chat.create_channel',
'treasury.view', 'treasury.deposit', 'treasury.withdraw',
'announcements.manage', 'events.manage', 'content.manage',
'rosters.manage', 'training.manage', 'media.manage',
'invites.manage', 'applications.review',
'achievements.manage', 'spotlights.manage',
'recruitment.manage', 'loyalty.manage',
'settings.view',
],
'arbiter' => [
'members.view', 'members.invite',
'chat.moderate',
'treasury.view',
'announcements.view', 'events.view', 'events.create',
'content.view', 'content.create',
'rosters.view',
'training.view', 'training.create',
'media.view', 'media.upload',
'applications.view',
'spotlights.view',
],
'member' => [
'members.view',
'chat.view', 'chat.send',
'announcements.view', 'events.view',
'content.view',
'rosters.view',
'training.view',
'media.view',
],
];
public static function check(string $orgId, string $permission): bool
{
$session = $_SESSION['user'] ?? null;
if (!$session) {
return false;
}
if (($session['role'] ?? '') === 'superadmin') {
return true;
}
$db = Database::getInstance();
$member = $db->selectOne('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$session['id']}",
'status' => 'eq.active',
]);
if (!$member) {
return false;
}
return self::roleHasPermission($member['role'], $permission);
}
public static function requireAccess(string $orgId): void
{
$session = $_SESSION['user'] ?? null;
if (!$session) {
http_response_code(403);
exit('Access denied');
}
if (($session['role'] ?? '') === 'superadmin') {
return;
}
$db = Database::getInstance();
$member = $db->selectOne('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$session['id']}",
'status' => 'eq.active',
]);
if (!$member) {
http_response_code(403);
exit('Access denied');
}
}
public static function requirePermission(string $orgId, string $permission): void
{
if (!self::check($orgId, $permission)) {
http_response_code(403);
exit('Insufficient permissions');
}
}
public static function getRolePermissions(string $role): array
{
return self::$rolePermissions[$role] ?? [];
}
public static function memberCan(string $orgId, string $playerId, string $permission): bool
{
$db = Database::getInstance();
$member = $db->selectOne('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
'status' => 'eq.active',
]);
if (!$member) {
return false;
}
$customPerms = $member['permissions'] ?? [];
if (is_string($customPerms)) {
$customPerms = json_decode($customPerms, true) ?? [];
}
if (in_array($permission, $customPerms, true)) {
return true;
}
return self::roleHasPermission($member['role'], $permission);
}
public static function getMemberRole(string $orgId, string $playerId): ?string
{
$db = Database::getInstance();
$member = $db->selectOne('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
'status' => 'eq.active',
]);
return $member['role'] ?? null;
}
public static function isRoleHigherOrEqual(string $role, string $targetRole): bool
{
$roleLevel = self::$roleHierarchy[$role] ?? 0;
$targetLevel = self::$roleHierarchy[$targetRole] ?? 0;
return $roleLevel >= $targetLevel;
}
private static function roleHasPermission(string $role, string $permission): bool
{
$perms = self::$rolePermissions[$role] ?? [];
if (in_array('*', $perms, true)) {
return true;
}
if (in_array($permission, $perms, true)) {
return true;
}
$domain = explode('.', $permission)[0] ?? '';
return in_array("{$domain}.*", $perms, true);
}
}
<?php
class SupabaseStorage
{
private static ?self $instance = null;
private string $baseUrl;
private string $serviceKey;
private function __construct()
{
$this->baseUrl = SUPABASE_URL . '/storage/v1';
$this->serviceKey = SUPABASE_SERVICE_KEY;
}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function upload(string $bucket, string $path, string $tmpFile, string $mimeType): string
{
$url = "{$this->baseUrl}/object/{$bucket}/{$path}";
$fileContent = file_get_contents($tmpFile);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $fileContent,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->serviceKey,
'Content-Type: ' . $mimeType,
'x-upsert: true',
],
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
$error = json_decode($response, true);
throw new RuntimeException('Storage upload failed: ' . ($error['message'] ?? $response));
}
return $this->getPublicUrl($bucket, $path);
}
public function delete(string $bucket, string $path): bool
{
$url = "{$this->baseUrl}/object/{$bucket}/{$path}";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->serviceKey,
],
CURLOPT_TIMEOUT => 30,
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $status >= 200 && $status < 300;
}
public function getPublicUrl(string $bucket, string $path): string
{
return SUPABASE_URL . "/storage/v1/object/public/{$bucket}/{$path}";
}
public function getSignedUrl(string $bucket, string $path, int $expiresIn = 3600): string
{
$url = "{$this->baseUrl}/object/sign/{$bucket}/{$path}";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['expiresIn' => $expiresIn]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->serviceKey,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
throw new RuntimeException('Failed to generate signed URL');
}
$data = json_decode($response, true);
return SUPABASE_URL . '/storage/v1' . ($data['signedURL'] ?? '');
}
public function validateFile(array $file, array $allowedMimes, int $maxSizeBytes): ?string
{
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
return 'No valid file uploaded';
}
if ($file['error'] !== UPLOAD_ERR_OK) {
return match ($file['error']) {
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'File exceeds maximum size',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
default => 'Upload error occurred',
};
}
if ($file['size'] > $maxSizeBytes) {
$maxMb = round($maxSizeBytes / 1048576, 1);
return "File exceeds maximum size of {$maxMb}MB";
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($file['tmp_name']);
if (!in_array($detectedMime, $allowedMimes, true)) {
return 'File type not allowed. Allowed: ' . implode(', ', $allowedMimes);
}
return null;
}
public function generatePath(string $prefix, string $originalName): string
{
$ext = pathinfo($originalName, PATHINFO_EXTENSION) ?: 'bin';
$unique = bin2hex(random_bytes(16));
return "{$prefix}/{$unique}.{$ext}";
}
}
<?php
class ThemeService
{
private static ?array $colors = null;
private static array $idToCssVar = [
'color_primary' => '--brand-blue',
'color_primary_hover' => '--brand-blue-hover',
'color_secondary' => '--brand-orange',
'color_gold' => '--brand-gold',
'color_gold_light' => '--brand-sand',
'color_accent_cyan' => '--brand-cyan',
'color_accent_purple' => '--brand-purple',
'color_success' => '--success',
'color_warning' => '--warning',
'color_error' => '--danger',
'surface_background' => '--bg-primary',
'surface_card' => '--bg-secondary',
'surface_elevated' => '--bg-elevated',
'surface_sidebar' => '--sidebar-bg',
'surface_input' => '--input-bg',
'text_primary' => '--text-primary',
'text_secondary' => '--text-secondary',
'text_tertiary' => '--text-muted',
];
public static function getColors(): array
{
if (self::$colors !== null) {
return self::$colors;
}
$cacheFile = STORAGE_PATH . '/cache/theme_colors.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 300) {
self::$colors = json_decode(file_get_contents($cacheFile), true) ?: [];
return self::$colors;
}
$db = Database::getInstance();
$rows = $db->select('platform_theme', [
'select' => 'id,value',
'category' => 'eq.color',
]);
self::$colors = [];
foreach ($rows as $row) {
self::$colors[$row['id']] = $row['value'];
}
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
file_put_contents($cacheFile, json_encode(self::$colors));
return self::$colors;
}
public static function renderCssOverrides(): string
{
$colors = self::getColors();
if (empty($colors)) {
return '';
}
$vars = [];
foreach ($colors as $id => $value) {
if (isset(self::$idToCssVar[$id]) && preg_match('/^#[0-9A-Fa-f]{3,8}$/', $value)) {
$vars[] = self::$idToCssVar[$id] . ': ' . $value;
}
}
if (empty($vars)) {
return '';
}
return '<style id="theme-overrides">:root { ' . implode('; ', $vars) . '; }</style>';
}
public static function invalidateCache(): void
{
$cacheFile = STORAGE_PATH . '/cache/theme_colors.json';
if (file_exists($cacheFile)) {
unlink($cacheFile);
}
self::$colors = null;
}
}
...@@ -12,6 +12,9 @@ require_once __DIR__ . '/core/ApiProxy.php'; ...@@ -12,6 +12,9 @@ require_once __DIR__ . '/core/ApiProxy.php';
require_once __DIR__ . '/core/AuditLog.php'; require_once __DIR__ . '/core/AuditLog.php';
require_once __DIR__ . '/core/Pagination.php'; require_once __DIR__ . '/core/Pagination.php';
require_once __DIR__ . '/core/Response.php'; require_once __DIR__ . '/core/Response.php';
require_once __DIR__ . '/core/ThemeService.php';
require_once __DIR__ . '/core/SupabaseStorage.php';
require_once __DIR__ . '/core/OrgPermissions.php';
$route = trim($_GET['route'] ?? '', '/'); $route = trim($_GET['route'] ?? '', '/');
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
<?php if (isset($moduleCSS)): ?> <?php if (isset($moduleCSS)): ?>
<link rel="stylesheet" href="/modules/<?= $moduleCSS ?>/assets/<?= basename($moduleCSS) ?>.css"> <link rel="stylesheet" href="/modules/<?= $moduleCSS ?>/assets/<?= basename($moduleCSS) ?>.css">
<?php endif; ?> <?php endif; ?>
<?= ThemeService::renderCssOverrides() ?>
<meta name="csrf-token" content="<?= Auth::csrfToken() ?>"> <meta name="csrf-token" content="<?= Auth::csrfToken() ?>">
</head> </head>
<body> <body>
......
...@@ -113,6 +113,53 @@ try { $pendingWorkflows = $isAdmin ? $db->count('approval_requests', ['status' = ...@@ -113,6 +113,53 @@ try { $pendingWorkflows = $isAdmin ? $db->count('approval_requests', ['status' =
</div> </div>
</div> </div>
<!-- Admin: Org Ecosystem Section -->
<div class="nav-section" data-section="org-ecosystem">
<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="/org-frames" class="nav-item <?= navActive('org-frames', $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="3" y="3" width="18" height="18" rx="2"/><rect x="7" y="7" width="10" height="10" rx="1"/></svg>
<span class="nav-text">الإطارات</span>
</a>
<a href="/org-applications" class="nav-item <?= navActive('org-applications', $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"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
<span class="nav-text">طلبات العضوية</span>
<?php
try { $pendingApps = $db->count('org_membership_applications', ['status' => 'eq.pending']); } catch (\Throwable $e) { $pendingApps = 0; }
if ($pendingApps > 0): ?>
<span class="nav-badge"><?= $pendingApps ?></span>
<?php endif; ?>
</a>
<a href="/org-transfers" class="nav-item <?= navActive('org-transfers', $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="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
<span class="nav-text">الانتقالات</span>
</a>
<a href="/org-challenges" class="nav-item <?= navActive('org-challenges', $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="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
<span class="nav-text">التحديات</span>
</a>
<a href="/org-achievements" class="nav-item <?= navActive('org-achievements', $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="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>
<span class="nav-text">الإنجازات</span>
</a>
<a href="/org-recruitment" class="nav-item <?= navActive('org-recruitment', $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="/org-leaderboards" class="nav-item <?= navActive('org-leaderboards', $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="/org-partnerships" class="nav-item <?= navActive('org-partnerships', $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 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<span class="nav-text">الشراكات</span>
</a>
</div>
</div>
<!-- Admin: System Section --> <!-- Admin: System Section -->
<div class="nav-section" data-section="system"> <div class="nav-section" data-section="system">
<div class="nav-section-header"> <div class="nav-section-header">
......
This diff is collapsed.
...@@ -48,6 +48,7 @@ class BrandingController ...@@ -48,6 +48,7 @@ class BrandingController
'updated_at' => date('c'), 'updated_at' => date('c'),
]); ]);
ThemeService::invalidateCache();
AuditLog::log('update', 'platform_theme', $id, null, ['value' => $value]); AuditLog::log('update', 'platform_theme', $id, null, ['value' => $value]);
Response::success('تم تحديث اللون', '/branding'); Response::success('تم تحديث اللون', '/branding');
} }
......
...@@ -50,7 +50,7 @@ class GamesController ...@@ -50,7 +50,7 @@ class GamesController
'name_ar' => trim($_POST['name_ar']), 'name_ar' => trim($_POST['name_ar']),
'description' => trim($_POST['description'] ?? ''), 'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''), 'description_ar' => trim($_POST['description_ar'] ?? ''),
'icon' => trim($_POST['icon'] ?? ''), 'icon_url' => trim($_POST['icon'] ?? ''),
'min_players' => (int)($_POST['min_players'] ?? 2), 'min_players' => (int)($_POST['min_players'] ?? 2),
'max_players' => (int)($_POST['max_players'] ?? 2), 'max_players' => (int)($_POST['max_players'] ?? 2),
'supports_ranked' => isset($_POST['supports_ranked']), 'supports_ranked' => isset($_POST['supports_ranked']),
...@@ -60,7 +60,7 @@ class GamesController ...@@ -60,7 +60,7 @@ class GamesController
]; ];
if (!empty($_POST['matchmaking_config'])) { if (!empty($_POST['matchmaking_config'])) {
$data['matchmaking_config'] = $_POST['matchmaking_config']; $data['config'] = $_POST['matchmaking_config'];
} }
$this->db->insert('game_plugins', $data); $this->db->insert('game_plugins', $data);
...@@ -100,7 +100,7 @@ class GamesController ...@@ -100,7 +100,7 @@ class GamesController
'name_ar' => trim($_POST['name_ar']), 'name_ar' => trim($_POST['name_ar']),
'description' => trim($_POST['description'] ?? ''), 'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''), 'description_ar' => trim($_POST['description_ar'] ?? ''),
'icon' => trim($_POST['icon'] ?? ''), 'icon_url' => trim($_POST['icon'] ?? ''),
'min_players' => (int)($_POST['min_players'] ?? 2), 'min_players' => (int)($_POST['min_players'] ?? 2),
'max_players' => (int)($_POST['max_players'] ?? 2), 'max_players' => (int)($_POST['max_players'] ?? 2),
'supports_ranked' => isset($_POST['supports_ranked']), 'supports_ranked' => isset($_POST['supports_ranked']),
...@@ -110,7 +110,7 @@ class GamesController ...@@ -110,7 +110,7 @@ class GamesController
]; ];
if (!empty($_POST['matchmaking_config'])) { if (!empty($_POST['matchmaking_config'])) {
$data['matchmaking_config'] = $_POST['matchmaking_config']; $data['config'] = $_POST['matchmaking_config'];
} }
$this->db->update('game_plugins', ['game_key' => "eq.{$id}"], $data); $this->db->update('game_plugins', ['game_key' => "eq.{$id}"], $data);
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">أيقونة (emoji أو نص)</label> <label class="form-label">أيقونة (emoji أو نص)</label>
<input type="text" name="icon" class="form-input" value="<?= View::e($game['icon'] ?? '') ?>" placeholder="♟️"> <input type="text" name="icon" class="form-input" value="<?= View::e($game['icon_url'] ?? '') ?>" placeholder="♟️">
</div> </div>
<div class="grid grid-3 gap-4"> <div class="grid grid-3 gap-4">
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">إعدادات التوفيق (JSON)</label> <label class="form-label">إعدادات التوفيق (JSON)</label>
<textarea name="matchmaking_config" class="form-input" dir="ltr" style="font-family: monospace; min-height: 120px;"><?= View::e(is_string($game['matchmaking_config'] ?? '') ? ($game['matchmaking_config'] ?? '') : json_encode($game['matchmaking_config'] ?? new stdClass(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></textarea> <textarea name="matchmaking_config" class="form-input" dir="ltr" style="font-family: monospace; min-height: 120px;"><?= View::e(is_string($game['config'] ?? '') ? ($game['config'] ?? '') : json_encode($game['config'] ?? new stdClass(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></textarea>
</div> </div>
<div class="flex gap-3 mt-6"> <div class="flex gap-3 mt-6">
......
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
<div class="flex items-start justify-between mb-4"> <div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="stat-icon blue" style="width: 40px; height: 40px;"> <div class="stat-icon blue" style="width: 40px; height: 40px;">
<?php if (!empty($game['icon'])): ?> <?php if (!empty($game['icon_url'])): ?>
<span style="font-size: 20px;"><?= $game['icon'] ?></span> <span style="font-size: 20px;"><?= $game['icon_url'] ?></span>
<?php else: ?> <?php else: ?>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/></svg>
<?php endif; ?> <?php endif; ?>
......
.achievement-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
object-fit: contain;
background: var(--bg-secondary);
padding: 4px;
}
.achievement-rewards {
display: flex;
gap: var(--space-2);
font-size: 0.8rem;
}
.achievement-rewards .coins { color: #f59e0b; }
.achievement-rewards .gems { color: #8b5cf6; }
.progress-bar {
height: 6px;
background: var(--bg-secondary);
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--primary);
border-radius: 3px;
transition: width 0.3s ease;
}
This diff is collapsed.
<?php $isEdit = !empty($achievement['id']); ?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-achievements" 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 ? "/org-achievements/{$achievement['id']}/update" : '/org-achievements/store' ?>" enctype="multipart/form-data" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<!-- Name -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">الاسم (English) *</label>
<input type="text" name="name" class="form-input" value="<?= View::e($achievement['name'] ?? '') ?>" required dir="ltr">
<span class="form-error"></span>
</div>
<div class="form-group">
<label class="form-label">الاسم (عربي)</label>
<input type="text" name="name_ar" class="form-input" value="<?= View::e($achievement['name_ar'] ?? '') ?>">
</div>
</div>
<!-- Description -->
<div class="form-group">
<label class="form-label">الوصف (English)</label>
<textarea name="description" class="form-input" dir="ltr"><?= View::e($achievement['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">الوصف (عربي)</label>
<textarea name="description_ar" class="form-input"><?= View::e($achievement['description_ar'] ?? '') ?></textarea>
</div>
<!-- Organization -->
<div class="form-group">
<label class="form-label">المنظمة (اختياري)</label>
<select name="org_id" class="form-select">
<option value="">-- بدون منظمة --</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= View::e($org['id']) ?>" <?= ($achievement['org_id'] ?? '') === $org['id'] ? 'selected' : '' ?>><?= View::e($org['name_ar'] ?: $org['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Category -->
<div class="form-group">
<label class="form-label">التصنيف *</label>
<select name="category" class="form-select" required>
<option value="general" <?= ($achievement['category'] ?? '') === 'general' ? 'selected' : '' ?>>عام</option>
<option value="tournament" <?= ($achievement['category'] ?? '') === 'tournament' ? 'selected' : '' ?>>بطولات</option>
<option value="social" <?= ($achievement['category'] ?? '') === 'social' ? 'selected' : '' ?>>اجتماعي</option>
<option value="loyalty" <?= ($achievement['category'] ?? '') === 'loyalty' ? 'selected' : '' ?>>ولاء</option>
<option value="special" <?= ($achievement['category'] ?? '') === 'special' ? 'selected' : '' ?>>خاص</option>
</select>
</div>
<!-- Rewards -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">مكافأة العملات</label>
<input type="number" name="reward_coins" class="form-input" value="<?= $achievement['reward_coins'] ?? 0 ?>" min="0">
</div>
<div class="form-group">
<label class="form-label">مكافأة الجواهر</label>
<input type="number" name="reward_gems" class="form-input" value="<?= $achievement['reward_gems'] ?? 0 ?>" min="0">
</div>
</div>
<!-- Reward Frame -->
<div class="form-group">
<label class="form-label">إطار المكافأة (اختياري)</label>
<select name="reward_frame_id" class="form-select">
<option value="">-- بدون إطار --</option>
<?php foreach ($frames as $frame): ?>
<option value="<?= View::e($frame['id']) ?>" <?= ($achievement['reward_frame_id'] ?? '') === $frame['id'] ? 'selected' : '' ?>><?= View::e($frame['name_ar'] ?: $frame['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Criteria -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">نوع المعيار</label>
<?php
$criteria = isset($achievement['criteria']) ? json_decode($achievement['criteria'], true) : [];
?>
<input type="text" name="criteria_type" class="form-input" value="<?= View::e($criteria['type'] ?? '') ?>" dir="ltr" placeholder="مثال: games_played">
</div>
<div class="form-group">
<label class="form-label">الحد المطلوب</label>
<input type="number" name="criteria_threshold" class="form-input" value="<?= $criteria['threshold'] ?? 0 ?>" min="0">
</div>
</div>
<!-- Repeatable -->
<div class="flex gap-6 mb-5">
<label class="toggle">
<input type="checkbox" name="is_repeatable" <?= ($achievement['is_repeatable'] ?? false) ? 'checked' : '' ?>>
<span class="toggle-track"></span>
<span>قابل للتكرار</span>
</label>
</div>
<!-- Sort Order -->
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" class="form-input" value="<?= $achievement['sort_order'] ?? 0 ?>">
</div>
<!-- Icon Upload -->
<div class="form-group">
<label class="form-label">أيقونة الإنجاز (PNG, SVG, WebP - أقصى 2MB)</label>
<?php if ($isEdit && !empty($achievement['icon_url'])): ?>
<div class="mb-3">
<img src="<?= View::e($achievement['icon_url']) ?>" alt="<?= View::e($achievement['name']) ?>" style="width: 48px; height: 48px; border-radius: 8px; object-fit: contain; border: 2px solid var(--border);">
<p class="text-xs text-muted mt-1">الأيقونة الحالية - اختر صورة جديدة للاستبدال</p>
</div>
<?php endif; ?>
<input type="file" name="icon" class="form-input" accept="image/png,image/svg+xml,image/webp">
</div>
<!-- Badge Upload -->
<div class="form-group">
<label class="form-label">شارة الإنجاز (اختياري)</label>
<?php if ($isEdit && !empty($achievement['badge_url'])): ?>
<div class="mb-3">
<img src="<?= View::e($achievement['badge_url']) ?>" alt="badge" style="width: 48px; height: 48px; border-radius: 8px; object-fit: contain; border: 2px solid var(--border);">
<p class="text-xs text-muted mt-1">الشارة الحالية - اختر صورة جديدة للاستبدال</p>
</div>
<?php endif; ?>
<input type="file" name="badge" class="form-input" accept="image/png,image/svg+xml,image/webp">
</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="/org-achievements" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
<div class="content-header">
<h1>الإنجازات والشارات</h1>
<a href="/org-achievements/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>
<div class="card mb-5">
<form method="GET" action="/org-achievements" class="flex gap-4 items-end flex-wrap">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">التصنيف</label>
<select name="category" class="form-select">
<option value="">الكل</option>
<option value="general" <?= $category === 'general' ? 'selected' : '' ?>>عام</option>
<option value="tournament" <?= $category === 'tournament' ? 'selected' : '' ?>>بطولات</option>
<option value="social" <?= $category === 'social' ? 'selected' : '' ?>>اجتماعي</option>
<option value="loyalty" <?= $category === 'loyalty' ? 'selected' : '' ?>>ولاء</option>
<option value="special" <?= $category === 'special' ? 'selected' : '' ?>>خاص</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">المنظمة</label>
<select name="org_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= View::e($org['id']) ?>" <?= $orgId === $org['id'] ? 'selected' : '' ?>><?= View::e($org['name_ar'] ?: $org['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
<?php if ($category || $orgId): ?>
<a href="/org-achievements" class="btn btn-ghost">إعادة تعيين</a>
<?php endif; ?>
</form>
</div>
<?php if (empty($achievements)): ?>
<div class="card">
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 15l-2 5l9-11h-5l2-5l-9 11h5z"/></svg>
<h3 class="empty-state-title">لا توجد إنجازات</h3>
<p class="empty-state-text">لم يتم إضافة أي إنجازات بعد</p>
<a href="/org-achievements/create" class="btn btn-primary">إضافة إنجاز</a>
</div>
</div>
<?php else: ?>
<?php
$categoryLabels = ['general' => 'عام', 'tournament' => 'بطولات', 'social' => 'اجتماعي', 'loyalty' => 'ولاء', 'special' => 'خاص'];
$categoryBadges = ['general' => 'badge-default', 'tournament' => 'badge-purple', 'social' => 'badge-info', 'loyalty' => 'badge-warning', 'special' => 'badge-success'];
?>
<div class="grid grid-3 stagger mb-5">
<?php foreach ($achievements as $achievement): ?>
<div class="card card-hover">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-3">
<div class="stat-icon blue" style="width: 44px; height: 44px;">
<?php if (!empty($achievement['icon_url'])): ?>
<img src="<?= View::e($achievement['icon_url']) ?>" alt="" style="width: 28px; height: 28px; object-fit: contain;">
<?php else: ?>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 15l-2 5l9-11h-5l2-5l-9 11h5z"/></svg>
<?php endif; ?>
</div>
<div>
<h3 class="font-semibold"><?= View::e($achievement['name_ar'] ?: $achievement['name']) ?></h3>
<p class="text-xs text-muted"><?= View::e($achievement['name']) ?></p>
</div>
</div>
<form method="POST" action="/org-achievements/<?= $achievement['id'] ?>/toggle">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<label class="toggle">
<input type="checkbox" <?= ($achievement['is_active'] ?? false) ? 'checked' : '' ?> onchange="this.closest('form').submit()">
<span class="toggle-track"></span>
</label>
</form>
</div>
<div class="flex gap-3 flex-wrap mb-4">
<?php $cat = $achievement['category'] ?? 'general'; ?>
<span class="badge <?= $categoryBadges[$cat] ?? 'badge-default' ?>"><?= $categoryLabels[$cat] ?? $cat ?></span>
<?php if (($achievement['reward_coins'] ?? 0) > 0): ?>
<span class="badge badge-warning"><?= $achievement['reward_coins'] ?> عملة</span>
<?php endif; ?>
<?php if (($achievement['reward_gems'] ?? 0) > 0): ?>
<span class="badge badge-info"><?= $achievement['reward_gems'] ?> جوهرة</span>
<?php endif; ?>
<?php if ($achievement['is_repeatable'] ?? false): ?>
<span class="badge badge-default">قابل للتكرار</span>
<?php endif; ?>
</div>
<?php if (!empty($achievement['org_id'])):
$orgName = '';
foreach ($organizations as $org) {
if ($org['id'] === $achievement['org_id']) {
$orgName = $org['name_ar'] ?: $org['name'];
break;
}
}
?>
<p class="text-xs text-muted mb-4">المنظمة: <?= View::e($orgName) ?></p>
<?php endif; ?>
<div class="flex items-center justify-between">
<span class="badge <?= ($achievement['is_active'] ?? false) ? 'badge-success' : 'badge-default' ?> badge-dot">
<?= ($achievement['is_active'] ?? false) ? 'نشط' : 'معطل' ?>
</span>
<div class="flex gap-2">
<a href="/org-achievements/<?= $achievement['id'] ?>/edit" class="btn btn-ghost btn-sm">تعديل</a>
<button class="btn btn-ghost btn-sm" style="color: var(--danger);" onclick="confirmDelete('/org-achievements/<?= $achievement['id'] ?>/delete', '<?= View::e($achievement['name_ar'] ?: $achievement['name']) ?>')">حذف</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&category=<?= urlencode($category) ?>&org_id=<?= urlencode($orgId) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Grant Achievement to Player -->
<div class="card mt-6">
<h3 class="font-semibold mb-4">منح إنجاز للاعب</h3>
<form method="POST" action="" id="grantForm" class="flex gap-4 items-end flex-wrap">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">الإنجاز</label>
<select name="achievement_id" id="grantAchievementSelect" class="form-select" required>
<option value="">اختر إنجاز</option>
<?php foreach ($achievements as $a): ?>
<option value="<?= View::e($a['id']) ?>"><?= View::e($a['name_ar'] ?: $a['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">معرف اللاعب</label>
<input type="text" name="player_id" class="form-input" placeholder="أدخل معرف اللاعب" required dir="ltr">
</div>
<button type="submit" class="btn btn-primary">منح الإنجاز</button>
</form>
</div>
<script>
document.getElementById('grantForm').addEventListener('submit', function(e) {
var achievementId = document.getElementById('grantAchievementSelect').value;
if (achievementId) {
this.action = '/org-achievements/' + achievementId + '/grant';
} else {
e.preventDefault();
}
});
</script>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-achievements" 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>
<div class="card mb-5">
<form method="GET" action="/org-achievements/progress" class="flex gap-4 items-end flex-wrap">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">معرف اللاعب</label>
<input type="text" name="player_id" class="form-input" value="<?= View::e($playerId) ?>" placeholder="أدخل معرف اللاعب" dir="ltr">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">المنظمة</label>
<select name="org_id" class="form-select">
<option value="">الكل</option>
<?php
$orgs = $organizations ?? [];
foreach ($orgs as $org): ?>
<option value="<?= View::e($org['id']) ?>" <?= $orgId === $org['id'] ? 'selected' : '' ?>><?= View::e($org['name_ar'] ?: $org['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">بحث</button>
<?php if ($playerId || $orgId): ?>
<a href="/org-achievements/progress" class="btn btn-ghost">إعادة تعيين</a>
<?php endif; ?>
</form>
</div>
<div class="data-table-wrapper">
<?php if (empty($progress)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>
<h3 class="empty-state-title">لا توجد بيانات تقدم</h3>
<p class="empty-state-text">لم يتم العثور على أي تقدم بهذه المعايير</p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>الإنجاز</th>
<th>معرف اللاعب</th>
<th>التقدم</th>
<th>الحالة</th>
<th>تاريخ الحصول</th>
</tr>
</thead>
<tbody>
<?php foreach ($progress as $item): ?>
<?php $ach = $achievements[$item['achievement_id']] ?? []; ?>
<tr>
<td>
<div class="flex items-center gap-3">
<?php if (!empty($ach['icon_url'])): ?>
<img src="<?= View::e($ach['icon_url']) ?>" alt="" style="width: 28px; height: 28px; object-fit: contain;">
<?php endif; ?>
<div>
<span class="font-semibold"><?= View::e($ach['name_ar'] ?? $ach['name'] ?? 'غير معروف') ?></span>
<?php if (!empty($ach['name'])): ?>
<p class="text-xs text-muted"><?= View::e($ach['name']) ?></p>
<?php endif; ?>
</div>
</div>
</td>
<td>
<code class="text-xs" style="font-family: monospace; background: var(--bg-secondary); padding: 2px 6px; border-radius: 4px;" dir="ltr"><?= View::e(substr($item['player_id'], 0, 8)) ?>...</code>
</td>
<td>
<?php
$threshold = 1;
if (!empty($ach['criteria'])) {
$criteria = json_decode($ach['criteria'], true);
$threshold = $criteria['threshold'] ?? 1;
}
$currentProgress = $item['progress'] ?? 0;
$percentage = $threshold > 0 ? min(100, round(($currentProgress / $threshold) * 100)) : 0;
?>
<div class="flex items-center gap-2">
<div style="width: 60px; height: 6px; background: var(--bg-secondary); border-radius: 3px; overflow: hidden;">
<div style="width: <?= $percentage ?>%; height: 100%; background: var(--primary); border-radius: 3px;"></div>
</div>
<span class="text-xs tabular-nums"><?= $currentProgress ?>/<?= $threshold ?></span>
</div>
</td>
<td>
<?php if ($item['is_complete'] ?? false): ?>
<span class="badge badge-success badge-dot">مكتمل</span>
<?php else: ?>
<span class="badge badge-warning badge-dot">قيد التقدم</span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($item['earned_at'])): ?>
<span class="text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($item['earned_at'])) ?></span>
<?php else: ?>
<span class="text-muted text-xs">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&player_id=<?= urlencode($playerId) ?>&org_id=<?= urlencode($orgId) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
.announcement-priority {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 0.8rem;
}
.pinned-indicator {
color: #f59e0b;
}
<?php
class OrgAnnouncementsController
{
private Database $db;
private SupabaseStorage $storage;
public function __construct()
{
$this->db = Database::getInstance();
$this->storage = SupabaseStorage::getInstance();
}
public function list(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$filters = [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'priority.desc,is_pinned.desc,created_at.desc',
];
if (!empty($_GET['type'])) {
$filters['type'] = "eq.{$_GET['type']}";
}
if (isset($_GET['is_draft']) && $_GET['is_draft'] !== '') {
$filters['is_draft'] = "eq.{$_GET['is_draft']}";
}
$total = $this->db->count('org_announcements', $filters);
$pagination = Pagination::fromRequest($total);
$filters['offset'] = $pagination->offset;
$filters['limit'] = $pagination->perPage;
$announcements = $this->db->select('org_announcements', $filters);
$pageTitle = 'الإعلانات - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-announcements/list', compact('org', 'announcements', 'pagination', 'pageTitle'));
}
public function create(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$pageTitle = 'إنشاء إعلان - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-announcements/form', compact('org', 'pageTitle'));
}
public function store(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$user = Auth::user();
$imageUrl = null;
// Handle image upload if provided
if (!empty($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['image'];
$validation = $this->storage->validateFile($file, ['image/*'], 10 * 1024 * 1024);
if ($validation !== true) {
Response::error($validation, "/organizations/{$orgId}/announcements/create");
return;
}
$path = $this->storage->generatePath('org-media', $orgId, $file['name']);
$uploadResult = $this->storage->upload('org-media', $path, $file['tmp_name'], $file['type']);
if ($uploadResult) {
$imageUrl = $this->storage->getPublicUrl('org-media', $path);
}
}
$data = [
'org_id' => $orgId,
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'content' => trim($_POST['content'] ?? ''),
'content_ar' => trim($_POST['content_ar'] ?? ''),
'type' => $_POST['type'] ?? 'general',
'priority' => !empty($_POST['priority']) ? (int) $_POST['priority'] : 0,
'is_pinned' => isset($_POST['is_pinned']) ? true : false,
'image_url' => $imageUrl,
'author_id' => $user['id'],
'is_draft' => true,
];
$result = $this->db->insert('org_announcements', $data);
AuditLog::log('create', 'org_announcement', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء الإعلان بنجاح', "/organizations/{$orgId}/announcements");
}
public function edit(array $params, string $method): void
{
$orgId = $params['id'];
$annId = $params['annId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$announcement = $this->db->selectOne('org_announcements', [
'id' => "eq.{$annId}",
'org_id' => "eq.{$orgId}",
]);
if (!$announcement) {
Response::error('الإعلان غير موجود', "/organizations/{$orgId}/announcements");
return;
}
$pageTitle = 'تعديل الإعلان - ' . ($announcement['title_ar'] ?? $announcement['title']);
View::render('org-announcements/form', compact('org', 'announcement', 'pageTitle'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$annId = $params['annId'];
$old = $this->db->selectOne('org_announcements', [
'id' => "eq.{$annId}",
'org_id' => "eq.{$orgId}",
]);
if (!$old) {
Response::error('الإعلان غير موجود', "/organizations/{$orgId}/announcements");
return;
}
$data = [
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'content' => trim($_POST['content'] ?? ''),
'content_ar' => trim($_POST['content_ar'] ?? ''),
'type' => $_POST['type'] ?? 'general',
'priority' => !empty($_POST['priority']) ? (int) $_POST['priority'] : 0,
'is_pinned' => isset($_POST['is_pinned']) ? true : false,
];
// Handle image upload if new one provided
if (!empty($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['image'];
$validation = $this->storage->validateFile($file, ['image/*'], 10 * 1024 * 1024);
if ($validation === true) {
$path = $this->storage->generatePath('org-media', $orgId, $file['name']);
$uploadResult = $this->storage->upload('org-media', $path, $file['tmp_name'], $file['type']);
if ($uploadResult) {
$data['image_url'] = $this->storage->getPublicUrl('org-media', $path);
}
}
}
$this->db->update('org_announcements', ['id' => "eq.{$annId}"], $data);
AuditLog::log('update', 'org_announcement', $annId, $old, $data);
Response::success('تم تحديث الإعلان بنجاح', "/organizations/{$orgId}/announcements");
}
public function publish(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$annId = $params['annId'];
$announcement = $this->db->selectOne('org_announcements', [
'id' => "eq.{$annId}",
'org_id' => "eq.{$orgId}",
]);
if (!$announcement) {
Response::error('الإعلان غير موجود', "/organizations/{$orgId}/announcements");
return;
}
$data = [
'is_draft' => false,
'published_at' => date('c'),
];
$this->db->update('org_announcements', ['id' => "eq.{$annId}"], $data);
AuditLog::log('publish', 'org_announcement', $annId, $announcement, $data);
Response::success('تم نشر الإعلان بنجاح', "/organizations/{$orgId}/announcements");
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$annId = $params['annId'];
$announcement = $this->db->selectOne('org_announcements', [
'id' => "eq.{$annId}",
'org_id' => "eq.{$orgId}",
]);
if (!$announcement) {
Response::error('الإعلان غير موجود', "/organizations/{$orgId}/announcements");
return;
}
$this->db->delete('org_announcements', ['id' => "eq.{$annId}"]);
AuditLog::log('delete', 'org_announcement', $annId, $announcement, null);
Response::success('تم حذف الإعلان', "/organizations/{$orgId}/announcements");
}
}
<?php
$isEdit = !empty($announcement);
$formAction = $isEdit
? "/organizations/{$org['id']}/announcements/{$announcement['id']}/update"
: "/organizations/{$org['id']}/announcements/store";
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/announcements" 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 ? 'تعديل الإعلان' : 'إنشاء إعلان' ?> - <?= View::e($org['name_ar'] ?? $org['name']) ?></h1>
</div>
</div>
<div class="card max-w-lg">
<form method="POST" action="<?= $formAction ?>" enctype="multipart/form-data" data-validate>
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">المنظمة</label>
<select name="org_id" class="form-input" disabled>
<option value="<?= $org['id'] ?>" selected><?= View::e($org['name_ar'] ?? $org['name']) ?></option>
</select>
<input type="hidden" name="org_id" value="<?= $org['id'] ?>">
</div>
<div class="form-group">
<label class="form-label">العنوان (English) <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-input" dir="ltr" value="<?= View::e($announcement['title'] ?? '') ?>" required>
</div>
<div class="form-group">
<label class="form-label">العنوان (عربي)</label>
<input type="text" name="title_ar" class="form-input" value="<?= View::e($announcement['title_ar'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">المحتوى (English)</label>
<textarea name="content" class="form-input" rows="5" dir="ltr"><?= View::e($announcement['content'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">المحتوى (عربي)</label>
<textarea name="content_ar" class="form-input" rows="5"><?= View::e($announcement['content_ar'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">النوع</label>
<select name="type" class="form-input">
<?php
$annTypes = [
'general' => 'عام',
'important' => 'مهم',
'event' => 'فعالية',
'tournament' => 'بطولة',
'maintenance' => 'صيانة',
];
foreach ($annTypes as $val => $label):
?>
<option value="<?= $val ?>" <?= ($announcement['type'] ?? 'general') === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الأولوية</label>
<input type="number" name="priority" class="form-input" min="0" max="100" value="<?= View::e($announcement['priority'] ?? '0') ?>">
<span class="form-hint">كلما كان الرقم أعلى، ظهر الإعلان أولاً</span>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="is_pinned" value="1" <?= ($announcement['is_pinned'] ?? false) ? 'checked' : '' ?>>
<span class="checkbox-mark"></span>
<span class="checkbox-label">تثبيت الإعلان</span>
</label>
<span class="form-hint">الإعلانات المثبتة تظهر دائماً في الأعلى</span>
</div>
<div class="form-group">
<label class="form-label">صورة الإعلان</label>
<?php if (!empty($announcement['image_url'])): ?>
<div class="mb-3">
<img src="<?= View::e($announcement['image_url']) ?>" alt="صورة الإعلان" style="max-width: 200px; border-radius: 8px;">
</div>
<?php endif; ?>
<input type="file" name="image" class="form-input" accept="image/*">
<span class="form-hint">الحد الأقصى: 10MB. الصيغ المدعومة: JPG, PNG, GIF, WebP</span>
</div>
<div class="form-group">
<label class="form-label">تاريخ انتهاء الإعلان</label>
<input type="datetime-local" name="expires_at" class="form-input" dir="ltr" value="<?= View::e(!empty($announcement['expires_at']) ? date('Y-m-d\TH:i', strtotime($announcement['expires_at'])) : '') ?>">
<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="/organizations/<?= $org['id'] ?>/announcements" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>" 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($org['name_ar'] ?? $org['name']) ?></h1>
</div>
<a href="/organizations/<?= $org['id'] ?>/announcements/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>
<div class="card mb-5">
<form method="GET" class="flex gap-4 items-end flex-wrap">
<div class="form-group mb-0">
<label class="form-label">النوع</label>
<select name="type" class="form-input" onchange="this.form.submit()">
<option value="">الكل</option>
<?php
$annTypes = [
'general' => 'عام',
'important' => 'مهم',
'event' => 'فعالية',
'tournament' => 'بطولة',
'maintenance' => 'صيانة',
];
foreach ($annTypes as $val => $label):
?>
<option value="<?= $val ?>" <?= ($_GET['type'] ?? '') === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group mb-0">
<label class="form-label">الحالة</label>
<select name="is_draft" class="form-input" onchange="this.form.submit()">
<option value="">الكل</option>
<option value="true" <?= ($_GET['is_draft'] ?? '') === 'true' ? 'selected' : '' ?>>مسودة</option>
<option value="false" <?= ($_GET['is_draft'] ?? '') === 'false' ? 'selected' : '' ?>>منشور</option>
</select>
</div>
</form>
</div>
<div class="data-table-wrapper">
<?php if (empty($announcements)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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>
<h3 class="empty-state-title">لا توجد إعلانات</h3>
<p class="empty-state-text">لم يتم إنشاء أي إعلانات لهذه المنظمة بعد</p>
</div>
<?php else: ?>
<?php
$typeBadges = [
'general' => 'badge-default',
'important' => 'badge-danger',
'event' => 'badge-info',
'tournament' => 'badge-purple',
'maintenance' => 'badge-warning',
];
$typeLabels = [
'general' => 'عام',
'important' => 'مهم',
'event' => 'فعالية',
'tournament' => 'بطولة',
'maintenance' => 'صيانة',
];
?>
<table class="data-table">
<thead>
<tr>
<th>العنوان</th>
<th>المنظمة</th>
<th>النوع</th>
<th>الأولوية</th>
<th>مثبت</th>
<th>الحالة</th>
<th>تاريخ النشر</th>
<th>المشاهدات</th>
<th style="width: 60px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($announcements as $ann): ?>
<tr>
<td>
<span class="fw-500"><?= View::e($ann['title_ar'] ?? $ann['title']) ?></span>
</td>
<td><?= View::e($org['name_ar'] ?? $org['name']) ?></td>
<td>
<span class="badge <?= $typeBadges[$ann['type']] ?? 'badge-default' ?>">
<?= $typeLabels[$ann['type']] ?? $ann['type'] ?>
</span>
</td>
<td><?= (int)($ann['priority'] ?? 0) ?></td>
<td>
<?php if ($ann['is_pinned'] ?? false): ?>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none" class="text-warning"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<?php if ($ann['is_draft'] ?? true): ?>
<span class="badge badge-warning badge-dot">مسودة</span>
<?php else: ?>
<span class="badge badge-success badge-dot">منشور</span>
<?php endif; ?>
</td>
<td class="tabular-nums">
<?= !empty($ann['published_at']) ? date('Y-m-d H:i', strtotime($ann['published_at'])) : '-' ?>
</td>
<td><?= (int)($ann['view_count'] ?? 0) ?></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="/organizations/<?= $org['id'] ?>/announcements/<?= $ann['id'] ?>/edit" class="dropdown-item">تعديل</a>
<?php if ($ann['is_draft'] ?? true): ?>
<form method="POST" action="/organizations/<?= $org['id'] ?>/announcements/<?= $ann['id'] ?>/publish" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="dropdown-item">نشر</button>
</form>
<?php else: ?>
<form method="POST" action="/organizations/<?= $org['id'] ?>/announcements/<?= $ann['id'] ?>/unpublish" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="dropdown-item">إلغاء النشر</button>
</form>
<?php endif; ?>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger" onclick="confirmDelete('/organizations/<?= $org['id'] ?>/announcements/<?= $ann['id'] ?>/delete', '<?= View::e($ann['title_ar'] ?? $ann['title']) ?>')">حذف</button>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&type=<?= $_GET['type'] ?? '' ?>&is_draft=<?= $_GET['is_draft'] ?? '' ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?php
class OrgChallengesController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$status = $_GET['status'] ?? '';
$queryParams = ['select' => '*', 'order' => 'created_at.desc'];
if ($status) {
$queryParams['status'] = "eq.{$status}";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_challenges', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$challenges = $this->db->select('org_challenges', $queryParams);
$orgIds = array_unique(array_merge(
array_column($challenges, 'challenger_org_id'),
array_column($challenges, 'challenged_org_id')
));
$orgs = [];
if (!empty($orgIds)) {
$idList = '(' . implode(',', $orgIds) . ')';
$orgList = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar,logo_url',
'id' => "in.{$idList}",
]);
foreach ($orgList as $o) {
$orgs[$o['id']] = $o;
}
}
$pageTitle = 'تحديات المنظمات';
$moduleCSS = 'org-challenges';
View::render('org-challenges/list', compact('challenges', 'orgs', 'pagination', 'status', 'pageTitle', 'moduleCSS'));
}
public function create(array $params, string $method): void
{
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'إنشاء تحدي جديد';
$moduleCSS = 'org-challenges';
View::render('org-challenges/form', compact('organizations', 'pageTitle', 'moduleCSS'));
}
public function store(array $params, string $method): void
{
Auth::requireCsrf();
$challengerOrgId = $_POST['challenger_org_id'] ?? '';
$challengedOrgId = $_POST['challenged_org_id'] ?? '';
if (empty($challengerOrgId) || empty($challengedOrgId) || $challengerOrgId === $challengedOrgId) {
Response::error('يرجى تحديد منظمتين مختلفتين', '/org-challenges/create');
return;
}
$data = [
'challenger_org_id' => $challengerOrgId,
'challenged_org_id' => $challengedOrgId,
'game_key' => $_POST['game_key'] ?? 'chess',
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'status' => 'pending',
'format' => json_encode([
'type' => $_POST['format_type'] ?? 'best_of',
'count' => (int)($_POST['format_count'] ?? 5),
'time_control' => $_POST['time_control'] ?? 'rapid',
]),
'prize_pool' => json_encode([
'coins' => (int)($_POST['prize_coins'] ?? 0),
'description' => trim($_POST['prize_description'] ?? ''),
]),
];
if (!empty($_POST['scheduled_at'])) {
$data['scheduled_at'] = $_POST['scheduled_at'];
}
$result = $this->db->insert('org_challenges', $data);
AuditLog::log('create', 'org_challenge', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء التحدي', '/org-challenges');
}
public function show(array $params, string $method): void
{
$id = $params['id'];
$challenge = $this->db->selectOne('org_challenges', ['id' => "eq.{$id}"]);
if (!$challenge) {
Response::error('التحدي غير موجود', '/org-challenges');
return;
}
$challengerOrg = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$challenge['challenger_org_id']}"]);
$challengedOrg = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$challenge['challenged_org_id']}"]);
$pageTitle = $challenge['title'] ?: 'تفاصيل التحدي';
$moduleCSS = 'org-challenges';
View::render('org-challenges/show', compact('challenge', 'challengerOrg', 'challengedOrg', 'pageTitle', 'moduleCSS'));
}
public function accept(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_challenges', ['id' => "eq.{$id}"], ['status' => 'accepted', 'updated_at' => date('c')]);
AuditLog::log('accept', 'org_challenge', $id);
Response::success('تم قبول التحدي', "/org-challenges/{$id}");
}
public function reject(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_challenges', ['id' => "eq.{$id}"], ['status' => 'rejected', 'updated_at' => date('c')]);
AuditLog::log('reject', 'org_challenge', $id);
Response::success('تم رفض التحدي', '/org-challenges');
}
public function start(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_challenges', ['id' => "eq.{$id}"], [
'status' => 'in_progress',
'started_at' => date('c'),
'updated_at' => date('c'),
]);
AuditLog::log('start', 'org_challenge', $id);
Response::success('بدأ التحدي', "/org-challenges/{$id}");
}
public function complete(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$scoreChallenger = (float)($_POST['score_challenger'] ?? 0);
$scoreChallenged = (float)($_POST['score_challenged'] ?? 0);
$challenge = $this->db->selectOne('org_challenges', ['id' => "eq.{$id}"]);
$winnerId = null;
if ($scoreChallenger > $scoreChallenged) {
$winnerId = $challenge['challenger_org_id'];
} elseif ($scoreChallenged > $scoreChallenger) {
$winnerId = $challenge['challenged_org_id'];
}
$this->db->update('org_challenges', ['id' => "eq.{$id}"], [
'status' => 'completed',
'score_challenger' => $scoreChallenger,
'score_challenged' => $scoreChallenged,
'winner_org_id' => $winnerId,
'completed_at' => date('c'),
'updated_at' => date('c'),
]);
AuditLog::log('complete', 'org_challenge', $id, null, [
'score_challenger' => $scoreChallenger,
'score_challenged' => $scoreChallenged,
'winner' => $winnerId,
]);
Response::success('تم إنهاء التحدي', "/org-challenges/{$id}");
}
public function cancel(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_challenges', ['id' => "eq.{$id}"], ['status' => 'cancelled', 'updated_at' => date('c')]);
AuditLog::log('cancel', 'org_challenge', $id);
Response::success('تم إلغاء التحدي', '/org-challenges');
}
public function updateRoster(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$side = $_POST['side'] ?? 'challenger';
$roster = json_decode($_POST['roster'] ?? '[]', true);
$field = $side === 'challenged' ? 'roster_challenged' : 'roster_challenger';
$this->db->update('org_challenges', ['id' => "eq.{$id}"], [
$field => json_encode($roster),
'updated_at' => date('c'),
]);
AuditLog::log('update_roster', 'org_challenge', $id, null, ['side' => $side]);
Response::success('تم تحديث القائمة', "/org-challenges/{$id}");
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/challenges" 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>
<div class="card max-w-lg">
<form method="POST" action="/challenges/store" data-validate>
<?= Auth::csrfField() ?>
<!-- Organizations -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">المنظمة المتحدية *</label>
<select name="challenger_org_id" class="form-input" required>
<option value="">اختر المنظمة</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= $org['id'] ?>"><?= View::e($org['name_ar'] ?? $org['name']) ?></option>
<?php endforeach; ?>
</select>
<span class="form-error"></span>
</div>
<div class="form-group">
<label class="form-label">المنظمة المتحدّاة *</label>
<select name="challenged_org_id" class="form-input" required>
<option value="">اختر المنظمة</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= $org['id'] ?>"><?= View::e($org['name_ar'] ?? $org['name']) ?></option>
<?php endforeach; ?>
</select>
<span class="form-error"></span>
</div>
</div>
<!-- Game -->
<div class="form-group">
<label class="form-label">اللعبة *</label>
<select name="game_key" class="form-input" required>
<option value="">اختر اللعبة</option>
<option value="chess">شطرنج</option>
<option value="backgammon">طاولة</option>
<option value="cards">كوتشينة</option>
<option value="dominoes">دومينو</option>
</select>
<span class="form-error"></span>
</div>
<!-- Title -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">العنوان (English)</label>
<input type="text" name="title" class="form-input" dir="ltr" placeholder="Challenge title">
</div>
<div class="form-group">
<label class="form-label">العنوان (عربي)</label>
<input type="text" name="title_ar" class="form-input" placeholder="عنوان التحدي">
</div>
</div>
<!-- Format -->
<h3 style="margin: 20px 0 12px;">الصيغة</h3>
<div class="grid grid-3 gap-4">
<div class="form-group">
<label class="form-label">النوع</label>
<select name="format_type" class="form-input">
<option value="best_of">أفضل من (Best of)</option>
<option value="first_to">أول من يصل (First to)</option>
<option value="single">مباراة واحدة</option>
</select>
</div>
<div class="form-group">
<label class="form-label">العدد</label>
<input type="number" name="format_count" class="form-input" value="3" min="1" max="15">
</div>
<div class="form-group">
<label class="form-label">التحكم بالوقت</label>
<select name="time_control" class="form-input">
<option value="standard">عادي</option>
<option value="rapid">سريع</option>
<option value="blitz">خاطف</option>
<option value="none">بدون</option>
</select>
</div>
</div>
<!-- Prize -->
<h3 style="margin: 20px 0 12px;">الجائزة</h3>
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">عملات</label>
<input type="number" name="prize_coins" class="form-input" min="0" value="0" placeholder="0">
</div>
<div class="form-group">
<label class="form-label">وصف الجائزة</label>
<input type="text" name="prize_description" class="form-input" placeholder="وصف إضافي للجائزة">
</div>
</div>
<!-- Schedule -->
<div class="form-group">
<label class="form-label">موعد البدء</label>
<input type="datetime-local" name="scheduled_at" class="form-input" dir="ltr">
</div>
<!-- Submit -->
<div class="flex gap-3 mt-6">
<button type="submit" class="btn btn-primary">
<span class="btn-text">إنشاء التحدي</span>
<span class="btn-spinner"></span>
</button>
<a href="/challenges" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
<div class="content-header">
<h1>التحديات بين المنظمات</h1>
<a href="/challenges/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>
<!-- Filters -->
<div class="card mb-4">
<form method="GET" action="/challenges" class="flex gap-4 items-end">
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">الحالة</label>
<select name="status" class="form-input">
<option value="">الكل</option>
<option value="pending" <?= ($status ?? '') === 'pending' ? 'selected' : '' ?>>معلق</option>
<option value="accepted" <?= ($status ?? '') === 'accepted' ? 'selected' : '' ?>>مقبول</option>
<option value="in_progress" <?= ($status ?? '') === 'in_progress' ? 'selected' : '' ?>>جاري</option>
<option value="completed" <?= ($status ?? '') === 'completed' ? 'selected' : '' ?>>مكتمل</option>
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>ملغي</option>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
</form>
</div>
<!-- Challenges List -->
<div class="data-table-wrapper">
<?php if (empty($challenges)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 9l6 6 6-6"/></svg>
<h3 class="empty-state-title">لا توجد تحديات</h3>
<p class="empty-state-text">لم يتم إنشاء أي تحديات بعد</p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>المتحدي</th>
<th></th>
<th>المتحدى</th>
<th>اللعبة</th>
<th>الحالة</th>
<th>النتيجة</th>
<th>التاريخ</th>
<th style="width: 80px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($challenges as $challenge): ?>
<?php
$challengerOrg = $orgs[$challenge['challenger_org_id']] ?? null;
$challengedOrg = $orgs[$challenge['challenged_org_id']] ?? null;
$statusLabels = ['pending' => 'معلق', 'accepted' => 'مقبول', 'in_progress' => 'جاري', 'completed' => 'مكتمل', 'cancelled' => 'ملغي', 'rejected' => 'مرفوض'];
$statusBadges = ['pending' => 'badge-warning', 'accepted' => 'badge-info', 'in_progress' => 'badge-purple', 'completed' => 'badge-success', 'cancelled' => 'badge-default', 'rejected' => 'badge-danger'];
$cStatus = $challenge['status'] ?? 'pending';
?>
<tr>
<td>
<div class="flex items-center gap-2">
<?php if (!empty($challengerOrg['logo_url'])): ?>
<img src="<?= View::e($challengerOrg['logo_url']) ?>" alt="" style="width:24px;height:24px;border-radius:4px;">
<?php endif; ?>
<span><?= View::e($challengerOrg['name_ar'] ?? $challengerOrg['name'] ?? '-') ?></span>
</div>
</td>
<td class="text-center"><strong>ضد</strong></td>
<td>
<div class="flex items-center gap-2">
<?php if (!empty($challengedOrg['logo_url'])): ?>
<img src="<?= View::e($challengedOrg['logo_url']) ?>" alt="" style="width:24px;height:24px;border-radius:4px;">
<?php endif; ?>
<span><?= View::e($challengedOrg['name_ar'] ?? $challengedOrg['name'] ?? '-') ?></span>
</div>
</td>
<td><?= View::e($challenge['game_key'] ?? '-') ?></td>
<td>
<span class="badge <?= $statusBadges[$cStatus] ?? 'badge-default' ?>"><?= $statusLabels[$cStatus] ?? $cStatus ?></span>
</td>
<td>
<?php if ($cStatus === 'completed' && isset($challenge['challenger_score'])): ?>
<strong><?= (int)$challenge['challenger_score'] ?> - <?= (int)$challenge['challenged_score'] ?></strong>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td><?= date('Y-m-d', strtotime($challenge['created_at'])) ?></td>
<td>
<a href="/challenges/<?= $challenge['id'] ?>" class="btn btn-ghost btn-sm">عرض</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!empty($pagination) && $pagination['total_pages'] > 1): ?>
<div class="pagination" style="margin-top: 16px;">
<?php if ($pagination['current_page'] > 1): ?>
<a href="?page=<?= $pagination['current_page'] - 1 ?>&status=<?= urlencode($status ?? '') ?>" class="btn btn-ghost btn-sm">السابق</a>
<?php endif; ?>
<span class="text-sm text-muted">صفحة <?= $pagination['current_page'] ?> من <?= $pagination['total_pages'] ?></span>
<?php if ($pagination['current_page'] < $pagination['total_pages']): ?>
<a href="?page=<?= $pagination['current_page'] + 1 ?>&status=<?= urlencode($status ?? '') ?>" class="btn btn-ghost btn-sm">التالي</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
$statusLabels = ['pending' => 'معلق', 'accepted' => 'مقبول', 'in_progress' => 'جاري', 'completed' => 'مكتمل', 'cancelled' => 'ملغي', 'rejected' => 'مرفوض'];
$statusBadges = ['pending' => 'badge-warning', 'accepted' => 'badge-info', 'in_progress' => 'badge-purple', 'completed' => 'badge-success', 'cancelled' => 'badge-default', 'rejected' => 'badge-danger'];
$cStatus = $challenge['status'] ?? 'pending';
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/challenges" 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[$cStatus] ?? 'badge-default' ?>"><?= $statusLabels[$cStatus] ?? $cStatus ?></span>
</div>
</div>
<!-- Two-Column Matchup -->
<div class="card mb-4">
<div class="grid grid-3 gap-4" style="align-items: center; text-align: center;">
<!-- Challenger -->
<div>
<?php if (!empty($challengerOrg['logo_url'])): ?>
<img src="<?= View::e($challengerOrg['logo_url']) ?>" alt="" style="width:64px;height:64px;border-radius:8px;margin:0 auto 8px;">
<?php endif; ?>
<h3><?= View::e($challengerOrg['name_ar'] ?? $challengerOrg['name'] ?? '-') ?></h3>
<p class="text-sm text-muted">المتحدي</p>
</div>
<!-- Score / VS -->
<div>
<?php if ($cStatus === 'completed' && isset($challenge['challenger_score'])): ?>
<p class="text-3xl font-bold"><?= (int)$challenge['challenger_score'] ?> - <?= (int)$challenge['challenged_score'] ?></p>
<?php else: ?>
<p class="text-3xl font-bold text-muted">VS</p>
<?php endif; ?>
</div>
<!-- Challenged -->
<div>
<?php if (!empty($challengedOrg['logo_url'])): ?>
<img src="<?= View::e($challengedOrg['logo_url']) ?>" alt="" style="width:64px;height:64px;border-radius:8px;margin:0 auto 8px;">
<?php endif; ?>
<h3><?= View::e($challengedOrg['name_ar'] ?? $challengedOrg['name'] ?? '-') ?></h3>
<p class="text-sm text-muted">المتحدى</p>
</div>
</div>
</div>
<!-- Challenge Details -->
<div class="grid grid-2 gap-4 mb-4">
<div class="card">
<h3 style="margin-bottom: 12px;">معلومات التحدي</h3>
<table style="width:100%;">
<tr><td class="text-muted" style="padding:4px 0;">اللعبة</td><td style="padding:4px 0;"><?= View::e($challenge['game_key'] ?? '-') ?></td></tr>
<tr><td class="text-muted" style="padding:4px 0;">العنوان</td><td style="padding:4px 0;"><?= View::e($challenge['title_ar'] ?? $challenge['title'] ?? '-') ?></td></tr>
<tr><td class="text-muted" style="padding:4px 0;">تاريخ الإنشاء</td><td style="padding:4px 0;"><?= date('Y-m-d H:i', strtotime($challenge['created_at'])) ?></td></tr>
<?php if (!empty($challenge['scheduled_at'])): ?>
<tr><td class="text-muted" style="padding:4px 0;">موعد البدء</td><td style="padding:4px 0;"><?= date('Y-m-d H:i', strtotime($challenge['scheduled_at'])) ?></td></tr>
<?php endif; ?>
</table>
</div>
<div class="card">
<h3 style="margin-bottom: 12px;">الصيغة</h3>
<table style="width:100%;">
<tr><td class="text-muted" style="padding:4px 0;">النوع</td><td style="padding:4px 0;"><?= View::e($challenge['format_type'] ?? 'best_of') ?></td></tr>
<tr><td class="text-muted" style="padding:4px 0;">العدد</td><td style="padding:4px 0;"><?= (int)($challenge['format_count'] ?? 1) ?></td></tr>
<tr><td class="text-muted" style="padding:4px 0;">التحكم بالوقت</td><td style="padding:4px 0;"><?= View::e($challenge['time_control'] ?? '-') ?></td></tr>
<?php if (!empty($challenge['prize_coins'])): ?>
<tr><td class="text-muted" style="padding:4px 0;">الجائزة</td><td style="padding:4px 0;"><?= number_format($challenge['prize_coins']) ?> عملة</td></tr>
<?php endif; ?>
</table>
</div>
</div>
<!-- Actions -->
<div class="card">
<h3 style="margin-bottom: 16px;">الإجراءات</h3>
<?php if ($cStatus === 'pending'): ?>
<div class="flex gap-3">
<form method="POST" action="/challenges/<?= $challenge['id'] ?>/accept" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-primary">قبول</button>
</form>
<form method="POST" action="/challenges/<?= $challenge['id'] ?>/reject" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger">رفض</button>
</form>
</div>
<?php elseif ($cStatus === 'accepted'): ?>
<form method="POST" action="/challenges/<?= $challenge['id'] ?>/start" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-primary">بدء التحدي</button>
</form>
<?php elseif ($cStatus === 'in_progress'): ?>
<form method="POST" action="/challenges/<?= $challenge['id'] ?>/complete" data-validate>
<?= Auth::csrfField() ?>
<div class="grid grid-2 gap-4 mb-4">
<div class="form-group">
<label class="form-label">نتيجة المتحدي</label>
<input type="number" name="challenger_score" class="form-input" min="0" required>
</div>
<div class="form-group">
<label class="form-label">نتيجة المتحدى</label>
<input type="number" name="challenged_score" class="form-input" min="0" required>
</div>
</div>
<button type="submit" class="btn btn-primary">إنهاء التحدي</button>
</form>
<?php endif; ?>
<?php if (in_array($cStatus, ['pending', 'accepted', 'in_progress'])): ?>
<form method="POST" action="/challenges/<?= $challenge['id'] ?>/cancel" style="margin-top: 12px;" onsubmit="return confirm('هل أنت متأكد من إلغاء التحدي؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-ghost" style="color: var(--danger);">إلغاء التحدي</button>
</form>
<?php endif; ?>
</div>
This diff is collapsed.
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>" 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($org['name_ar'] ?? $org['name']) ?></h1>
</div>
<a href="/organizations/<?= $org['id'] ?>/chat/channels/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>
<div class="data-table-wrapper">
<?php if (empty($channels)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<h3 class="empty-state-title">لا توجد قنوات</h3>
<p class="empty-state-text">لم يتم إنشاء أي قنوات دردشة لهذه المنظمة بعد</p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>الاسم</th>
<th>الاسم (عربي)</th>
<th>النوع</th>
<th>الرسائل</th>
<th>افتراضية</th>
<th>الحالة</th>
<th style="width: 100px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($channels as $channel): ?>
<tr>
<td>
<a href="/organizations/<?= $org['id'] ?>/chat/channels/<?= $channel['id'] ?>" class="text-link">
<?= View::e($channel['name']) ?>
</a>
</td>
<td><?= View::e($channel['name_ar'] ?? '-') ?></td>
<td>
<?php
$typeBadges = [
'general' => 'badge-default',
'announcements' => 'badge-info',
'voice' => 'badge-purple',
'private' => 'badge-warning',
];
$typeLabels = [
'general' => 'عام',
'announcements' => 'إعلانات',
'voice' => 'صوتي',
'private' => 'خاص',
];
$type = $channel['type'] ?? 'general';
?>
<span class="badge <?= $typeBadges[$type] ?? 'badge-default' ?>"><?= $typeLabels[$type] ?? $type ?></span>
</td>
<td><?= number_format($channel['messages_count'] ?? 0) ?></td>
<td>
<?php if ($channel['is_default'] ?? false): ?>
<span class="badge badge-success">نعم</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<?php if ($channel['is_active'] ?? true): ?>
<span class="badge badge-success badge-dot">نشطة</span>
<?php else: ?>
<span class="badge badge-danger badge-dot">معطلة</span>
<?php endif; ?>
</td>
<td>
<div class="flex gap-2">
<a href="/organizations/<?= $org['id'] ?>/chat/channels/<?= $channel['id'] ?>" class="btn btn-ghost btn-sm">الرسائل</a>
<form method="POST" action="/organizations/<?= $org['id'] ?>/chat/channels/<?= $channel['id'] ?>/delete" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من حذف هذه القناة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger btn-sm">حذف</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/chat/channels" 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($channel['name_ar'] ?? $channel['name']) ?></h1>
<span class="badge badge-default"><?= number_format($total) ?> رسالة</span>
</div>
</div>
<div class="card">
<?php if (empty($messages)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<h3 class="empty-state-title">لا توجد رسائل</h3>
<p class="empty-state-text">هذه القناة لا تحتوي على رسائل بعد</p>
</div>
<?php else: ?>
<div class="messages-list">
<?php foreach ($messages as $msg): ?>
<div class="message-item" style="padding: 12px 16px; border-bottom: 1px solid var(--border-color); <?= ($msg['is_deleted'] ?? false) ? 'opacity: 0.5;' : '' ?>">
<div class="flex items-start justify-between">
<div>
<div class="flex items-center gap-2 mb-1">
<strong><?= View::e($senders[$msg['sender_id']]['name'] ?? 'مجهول') ?></strong>
<span class="text-xs text-muted"><?= date('Y-m-d H:i', strtotime($msg['created_at'])) ?></span>
<?php if ($msg['is_pinned'] ?? false): ?>
<span class="badge badge-warning">مثبتة</span>
<?php endif; ?>
<?php if ($msg['is_deleted'] ?? false): ?>
<span class="badge badge-danger">محذوفة</span>
<?php endif; ?>
</div>
<p style="margin: 0;"><?= View::e($msg['content'] ?? '') ?></p>
</div>
<div class="flex gap-2">
<form method="POST" action="/organizations/<?= $org['id'] ?>/chat/messages/<?= $msg['id'] ?>/pin" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-ghost btn-sm" title="<?= ($msg['is_pinned'] ?? false) ? 'إلغاء التثبيت' : 'تثبيت' ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="<?= ($msg['is_pinned'] ?? false) ? 'currentColor' : 'none' ?>" stroke="currentColor" stroke-width="2"><path d="M12 2L12 22M12 2L8 6M12 2L16 6"/></svg>
</button>
</form>
<?php if (!($msg['is_deleted'] ?? false)): ?>
<form method="POST" action="/organizations/<?= $org['id'] ?>/chat/messages/<?= $msg['id'] ?>/delete" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من حذف هذه الرسالة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger btn-sm">حذف</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($totalPages > 1): ?>
<div class="pagination" style="margin-top: 16px;">
<?php if ($page > 1): ?>
<a href="?page=<?= $page - 1 ?>" class="btn btn-ghost btn-sm">السابق</a>
<?php endif; ?>
<span class="text-sm text-muted">صفحة <?= $page ?> من <?= $totalPages ?></span>
<?php if ($page < $totalPages): ?>
<a href="?page=<?= $page + 1 ?>" class="btn btn-ghost btn-sm">التالي</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/chat/channels" 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($org['name_ar'] ?? $org['name']) ?></h1>
</div>
</div>
<!-- Mute/Ban Action Form -->
<div class="card mb-4">
<h3 style="margin-bottom: 16px;">إجراء جديد</h3>
<form method="POST" action="/organizations/<?= $org['id'] ?>/chat/moderation/action" data-validate>
<?= Auth::csrfField() ?>
<div class="grid grid-4 gap-4">
<div class="form-group">
<label class="form-label">اللاعب</label>
<select name="player_id" class="form-input" required>
<option value="">اختر لاعب</option>
<?php foreach ($players as $player): ?>
<option value="<?= $player['id'] ?>"><?= View::e($player['username'] ?? $player['name'] ?? $player['id']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الإجراء</label>
<select name="action" class="form-input" required>
<option value="mute">كتم</option>
<option value="ban">حظر</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المدة (دقائق)</label>
<input type="number" name="duration_minutes" class="form-input" min="1" placeholder="60">
</div>
<div class="form-group">
<label class="form-label">السبب</label>
<input type="text" name="reason" class="form-input" placeholder="سبب الإجراء">
</div>
</div>
<button type="submit" class="btn btn-primary" style="margin-top: 8px;">تنفيذ</button>
</form>
</div>
<!-- Muted Members -->
<div class="card mb-4">
<h3 style="margin-bottom: 16px;">الأعضاء المكتومين</h3>
<?php if (empty($mutedMembers)): ?>
<p class="text-muted">لا يوجد أعضاء مكتومين حاليًا</p>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>اللاعب</th>
<th>السبب</th>
<th>ينتهي في</th>
<th>المشرف</th>
<th style="width: 80px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($mutedMembers as $muted): ?>
<tr>
<td><?= View::e($muted['player_name'] ?? $muted['player_id']) ?></td>
<td><?= View::e($muted['reason'] ?? '-') ?></td>
<td>
<?php if (!empty($muted['expires_at'])): ?>
<?= date('Y-m-d H:i', strtotime($muted['expires_at'])) ?>
<?php else: ?>
<span class="text-muted">دائم</span>
<?php endif; ?>
</td>
<td><?= View::e($muted['moderator_name'] ?? '-') ?></td>
<td>
<form method="POST" action="/organizations/<?= $org['id'] ?>/chat/moderation/unmute" style="margin:0;">
<?= Auth::csrfField() ?>
<input type="hidden" name="player_id" value="<?= $muted['player_id'] ?>">
<button type="submit" class="btn btn-sm btn-primary">إلغاء الكتم</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Moderation Log -->
<div class="card">
<h3 style="margin-bottom: 16px;">سجل الإشراف</h3>
<?php if (empty($logs)): ?>
<p class="text-muted">لا توجد سجلات إشراف</p>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>اللاعب المستهدف</th>
<th>الإجراء</th>
<th>السبب</th>
<th>المشرف</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr>
<td><?= View::e($log['target_player_name'] ?? $log['target_player_id']) ?></td>
<td>
<?php
$actionLabels = ['mute' => 'كتم', 'unmute' => 'إلغاء كتم', 'ban' => 'حظر', 'unban' => 'إلغاء حظر', 'warn' => 'تحذير', 'delete_message' => 'حذف رسالة'];
$actionBadges = ['mute' => 'badge-warning', 'unmute' => 'badge-success', 'ban' => 'badge-danger', 'unban' => 'badge-success', 'warn' => 'badge-info', 'delete_message' => 'badge-default'];
$action = $log['action'] ?? '';
?>
<span class="badge <?= $actionBadges[$action] ?? 'badge-default' ?>"><?= $actionLabels[$action] ?? $action ?></span>
</td>
<td><?= View::e($log['reason'] ?? '-') ?></td>
<td><?= View::e($log['moderator_name'] ?? $log['moderator_id'] ?? '-') ?></td>
<td><?= date('Y-m-d H:i', strtotime($log['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!empty($pagination) && $pagination['total_pages'] > 1): ?>
<div class="pagination" style="margin-top: 16px;">
<?php if ($pagination['current_page'] > 1): ?>
<a href="?page=<?= $pagination['current_page'] - 1 ?>" class="btn btn-ghost btn-sm">السابق</a>
<?php endif; ?>
<span class="text-sm text-muted">صفحة <?= $pagination['current_page'] ?> من <?= $pagination['total_pages'] ?></span>
<?php if ($pagination['current_page'] < $pagination['total_pages']): ?>
<a href="?page=<?= $pagination['current_page'] + 1 ?>" class="btn btn-ghost btn-sm">التالي</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
.content-body-preview {
max-height: 100px;
overflow: hidden;
font-size: 0.85rem;
color: var(--text-secondary);
position: relative;
}
.content-body-preview::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30px;
background: linear-gradient(transparent, var(--card-bg));
}
This diff is collapsed.
<?php
$isEdit = !empty($content);
$formAction = $isEdit
? "/organizations/{$org['id']}/content/{$content['id']}/update"
: "/organizations/{$org['id']}/content/store";
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/content" 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 ? 'تعديل المحتوى' : 'إضافة محتوى' ?> - <?= View::e($org['name_ar'] ?? $org['name']) ?></h1>
</div>
</div>
<div class="card max-w-lg">
<form method="POST" action="<?= $formAction ?>" data-validate>
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">العنوان (English)</label>
<input type="text" name="title" class="form-input" dir="ltr" value="<?= View::e($content['title'] ?? '') ?>" required>
</div>
<div class="form-group">
<label class="form-label">العنوان (عربي)</label>
<input type="text" name="title_ar" class="form-input" value="<?= View::e($content['title_ar'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">المحتوى (English)</label>
<textarea name="body" class="form-input" rows="10" dir="ltr" placeholder="Write content here..."><?= View::e($content['body'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">المحتوى (عربي)</label>
<textarea name="body_ar" class="form-input" rows="10" placeholder="اكتب المحتوى هنا..."><?= View::e($content['body_ar'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">التصنيف</label>
<select name="category" class="form-input" required>
<option value="">اختر التصنيف</option>
<option value="guide" <?= ($content['category'] ?? '') === 'guide' ? 'selected' : '' ?>>دليل</option>
<option value="strategy" <?= ($content['category'] ?? '') === 'strategy' ? 'selected' : '' ?>>استراتيجية</option>
<option value="tutorial" <?= ($content['category'] ?? '') === 'tutorial' ? 'selected' : '' ?>>شرح</option>
<option value="announcement" <?= ($content['category'] ?? '') === 'announcement' ? 'selected' : '' ?>>إعلان</option>
<option value="resource" <?= ($content['category'] ?? '') === 'resource' ? 'selected' : '' ?>>مورد</option>
</select>
</div>
<div class="form-group">
<label class="form-label">مفتاح اللعبة</label>
<input type="text" name="game_key" class="form-input" dir="ltr" value="<?= View::e($content['game_key'] ?? '') ?>" placeholder="e.g. chess, ludo">
<span class="form-hint">اسم اللعبة المرتبطة بهذا المحتوى (اختياري)</span>
</div>
<div class="form-group">
<label class="form-label">الظهور</label>
<select name="visibility" class="form-input" required>
<option value="members" <?= ($content['visibility'] ?? 'members') === 'members' ? 'selected' : '' ?>>الأعضاء</option>
<option value="public" <?= ($content['visibility'] ?? '') === 'public' ? 'selected' : '' ?>>عام</option>
<option value="admin_only" <?= ($content['visibility'] ?? '') === 'admin_only' ? 'selected' : '' ?>>المديرين فقط</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الوسوم</label>
<input type="text" name="tags" class="form-input" value="<?= View::e(is_array($content['tags'] ?? null) ? implode(', ', $content['tags']) : (is_string($content['tags'] ?? null) ? implode(', ', json_decode($content['tags'], true) ?? []) : '')) ?>" placeholder="وسم1, وسم2, وسم3">
<span class="form-hint">أدخل الوسوم مفصولة بفواصل</span>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="is_pinned" value="1" <?= ($content['is_pinned'] ?? false) ? 'checked' : '' ?>>
<span class="checkbox-mark"></span>
<span class="checkbox-label">تثبيت المحتوى</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="/organizations/<?= $org['id'] ?>/content" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
This diff is collapsed.
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.calendar-header {
background: var(--bg-secondary);
padding: var(--space-2);
text-align: center;
font-weight: 600;
font-size: 0.8rem;
}
.calendar-cell {
background: var(--card-bg);
padding: var(--space-2);
min-height: 80px;
position: relative;
}
.calendar-cell.today {
background: rgba(99, 102, 241, 0.05);
}
.calendar-cell.other-month {
opacity: 0.4;
}
.calendar-day {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: var(--space-1);
}
.calendar-event {
font-size: 0.7rem;
padding: 1px 4px;
border-radius: 3px;
background: var(--primary);
color: white;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-nav {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.calendar-nav h2 {
font-size: 1.2rem;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
.loyalty-milestone {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.85rem;
}
.loyalty-days {
font-weight: 700;
color: var(--primary);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment