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 ───────────────────
......
...@@ -191,4 +191,190 @@ return [ ...@@ -191,4 +191,190 @@ return [
// API // API
'api/health' => ['module' => 'api', 'action' => 'health'], 'api/health' => ['module' => 'api', 'action' => 'health'],
'api/players/search' => ['module' => 'players', 'action' => 'apiSearch'],
// Player Avatar & Frame
'players/{id}/avatar/upload' => ['module' => 'players', 'action' => 'uploadAvatar'],
'players/{id}/avatar/remove' => ['module' => 'players', 'action' => 'removeAvatar'],
'players/{id}/frame/set' => ['module' => 'players', 'action' => 'setFrame'],
// Profile Frames
'org-frames' => ['module' => 'org-frames', 'action' => 'list'],
'org-frames/create' => ['module' => 'org-frames', 'action' => 'create'],
'org-frames/store' => ['module' => 'org-frames', 'action' => 'store'],
'org-frames/{id}/edit' => ['module' => 'org-frames', 'action' => 'edit'],
'org-frames/{id}/update' => ['module' => 'org-frames', 'action' => 'update'],
'org-frames/{id}/toggle' => ['module' => 'org-frames', 'action' => 'toggle'],
'org-frames/{id}/delete' => ['module' => 'org-frames', 'action' => 'delete'],
'org-frames/assign' => ['module' => 'org-frames', 'action' => 'assign'],
'org-frames/revoke' => ['module' => 'org-frames', 'action' => 'revokeFromPlayer'],
'org-frames/bulk-assign' => ['module' => 'org-frames', 'action' => 'bulkAssign'],
// Org Invite Links
'organizations/{id}/invites' => ['module' => 'org-invites', 'action' => 'list'],
'organizations/{id}/invites/create' => ['module' => 'org-invites', 'action' => 'create'],
'organizations/{id}/invites/store' => ['module' => 'org-invites', 'action' => 'store'],
'organizations/{id}/invites/{inviteId}/toggle' => ['module' => 'org-invites', 'action' => 'toggle'],
'organizations/{id}/invites/{inviteId}/delete' => ['module' => 'org-invites', 'action' => 'delete'],
'organizations/{id}/invites/{inviteId}/usage' => ['module' => 'org-invites', 'action' => 'usage'],
// Org Membership Applications
'org-applications' => ['module' => 'org-applications', 'action' => 'list'],
'org-applications/{id}' => ['module' => 'org-applications', 'action' => 'show'],
'org-applications/{id}/approve' => ['module' => 'org-applications', 'action' => 'approve'],
'org-applications/{id}/reject' => ['module' => 'org-applications', 'action' => 'reject'],
'org-applications/bulk-approve' => ['module' => 'org-applications', 'action' => 'bulkApprove'],
'org-applications/bulk-reject' => ['module' => 'org-applications', 'action' => 'bulkReject'],
// Org Chat Moderation
'organizations/{id}/chat' => ['module' => 'org-chat', 'action' => 'channels'],
'organizations/{id}/chat/channels/create' => ['module' => 'org-chat', 'action' => 'createChannel'],
'organizations/{id}/chat/channels/store' => ['module' => 'org-chat', 'action' => 'storeChannel'],
'organizations/{id}/chat/channels/{channelId}' => ['module' => 'org-chat', 'action' => 'messages'],
'organizations/{id}/chat/channels/{channelId}/update' => ['module' => 'org-chat', 'action' => 'updateChannel'],
'organizations/{id}/chat/channels/{channelId}/delete' => ['module' => 'org-chat', 'action' => 'deleteChannel'],
'organizations/{id}/chat/messages/{messageId}/delete' => ['module' => 'org-chat', 'action' => 'deleteMessage'],
'organizations/{id}/chat/messages/{messageId}/pin' => ['module' => 'org-chat', 'action' => 'pinMessage'],
'organizations/{id}/chat/moderation' => ['module' => 'org-chat', 'action' => 'moderation'],
'organizations/{id}/chat/mute' => ['module' => 'org-chat', 'action' => 'mutePlayer'],
'organizations/{id}/chat/unmute' => ['module' => 'org-chat', 'action' => 'unmutePlayer'],
'organizations/{id}/chat/ban' => ['module' => 'org-chat', 'action' => 'banFromChat'],
'api/org-chat/{orgId}/messages' => ['module' => 'org-chat', 'action' => 'apiMessages'],
// Org Treasury
'organizations/{id}/treasury' => ['module' => 'org-treasury', 'action' => 'index'],
'organizations/{id}/treasury/transactions' => ['module' => 'org-treasury', 'action' => 'transactions'],
'organizations/{id}/treasury/deposit' => ['module' => 'org-treasury', 'action' => 'deposit'],
'organizations/{id}/treasury/withdraw' => ['module' => 'org-treasury', 'action' => 'withdraw'],
'organizations/{id}/treasury/settings' => ['module' => 'org-treasury', 'action' => 'settings'],
'organizations/{id}/treasury/settings/update' => ['module' => 'org-treasury', 'action' => 'updateSettings'],
// Org Challenges
'org-challenges' => ['module' => 'org-challenges', 'action' => 'list'],
'org-challenges/create' => ['module' => 'org-challenges', 'action' => 'create'],
'org-challenges/store' => ['module' => 'org-challenges', 'action' => 'store'],
'org-challenges/{id}' => ['module' => 'org-challenges', 'action' => 'show'],
'org-challenges/{id}/accept' => ['module' => 'org-challenges', 'action' => 'accept'],
'org-challenges/{id}/reject' => ['module' => 'org-challenges', 'action' => 'reject'],
'org-challenges/{id}/start' => ['module' => 'org-challenges', 'action' => 'start'],
'org-challenges/{id}/complete' => ['module' => 'org-challenges', 'action' => 'complete'],
'org-challenges/{id}/cancel' => ['module' => 'org-challenges', 'action' => 'cancel'],
'org-challenges/{id}/roster' => ['module' => 'org-challenges', 'action' => 'updateRoster'],
// Org Achievements
'org-achievements' => ['module' => 'org-achievements', 'action' => 'list'],
'org-achievements/create' => ['module' => 'org-achievements', 'action' => 'create'],
'org-achievements/store' => ['module' => 'org-achievements', 'action' => 'store'],
'org-achievements/{id}/edit' => ['module' => 'org-achievements', 'action' => 'edit'],
'org-achievements/{id}/update' => ['module' => 'org-achievements', 'action' => 'update'],
'org-achievements/{id}/toggle' => ['module' => 'org-achievements', 'action' => 'toggle'],
'org-achievements/{id}/delete' => ['module' => 'org-achievements', 'action' => 'delete'],
'org-achievements/{id}/grant' => ['module' => 'org-achievements', 'action' => 'grantToPlayer'],
'org-achievements/player-progress' => ['module' => 'org-achievements', 'action' => 'playerProgress'],
// Player Transfers
'org-transfers' => ['module' => 'org-transfers', 'action' => 'list'],
'org-transfers/initiate' => ['module' => 'org-transfers', 'action' => 'initiate'],
'org-transfers/{id}' => ['module' => 'org-transfers', 'action' => 'show'],
'org-transfers/{id}/approve' => ['module' => 'org-transfers', 'action' => 'approve'],
'org-transfers/{id}/reject' => ['module' => 'org-transfers', 'action' => 'reject'],
'org-transfers/{id}/complete' => ['module' => 'org-transfers', 'action' => 'complete'],
'org-transfers/{id}/cancel' => ['module' => 'org-transfers', 'action' => 'cancel'],
// Org Recruitment
'org-recruitment' => ['module' => 'org-recruitment', 'action' => 'list'],
'org-recruitment/create' => ['module' => 'org-recruitment', 'action' => 'create'],
'org-recruitment/store' => ['module' => 'org-recruitment', 'action' => 'store'],
'org-recruitment/{id}/edit' => ['module' => 'org-recruitment', 'action' => 'edit'],
'org-recruitment/{id}/update' => ['module' => 'org-recruitment', 'action' => 'update'],
'org-recruitment/{id}/close' => ['module' => 'org-recruitment', 'action' => 'close'],
'org-recruitment/{id}/delete' => ['module' => 'org-recruitment', 'action' => 'delete'],
'org-recruitment/{id}/feature' => ['module' => 'org-recruitment', 'action' => 'toggleFeatured'],
// Org Events
'organizations/{id}/events' => ['module' => 'org-events', 'action' => 'list'],
'organizations/{id}/events/create' => ['module' => 'org-events', 'action' => 'create'],
'organizations/{id}/events/store' => ['module' => 'org-events', 'action' => 'store'],
'organizations/{id}/events/{eventId}/edit' => ['module' => 'org-events', 'action' => 'edit'],
'organizations/{id}/events/{eventId}/update' => ['module' => 'org-events', 'action' => 'update'],
'organizations/{id}/events/{eventId}/cancel' => ['module' => 'org-events', 'action' => 'cancel'],
'organizations/{id}/events/{eventId}/delete' => ['module' => 'org-events', 'action' => 'delete'],
'organizations/{id}/events/calendar' => ['module' => 'org-events', 'action' => 'calendar'],
'api/org-events/{orgId}/calendar' => ['module' => 'org-events', 'action' => 'apiCalendar'],
// Org Content
'organizations/{id}/content' => ['module' => 'org-content', 'action' => 'list'],
'organizations/{id}/content/create' => ['module' => 'org-content', 'action' => 'create'],
'organizations/{id}/content/store' => ['module' => 'org-content', 'action' => 'store'],
'organizations/{id}/content/{contentId}/edit' => ['module' => 'org-content', 'action' => 'edit'],
'organizations/{id}/content/{contentId}/update' => ['module' => 'org-content', 'action' => 'update'],
'organizations/{id}/content/{contentId}/toggle-publish' => ['module' => 'org-content', 'action' => 'togglePublish'],
'organizations/{id}/content/{contentId}/delete' => ['module' => 'org-content', 'action' => 'delete'],
// Org Leaderboards
'org-leaderboards' => ['module' => 'org-leaderboards', 'action' => 'index'],
'org-leaderboards/seasonal' => ['module' => 'org-leaderboards', 'action' => 'seasonal'],
'org-leaderboards/recalculate/{orgId}' => ['module' => 'org-leaderboards', 'action' => 'recalculate'],
'org-leaderboards/{orgId}' => ['module' => 'org-leaderboards', 'action' => 'orgBoard'],
'api/org-leaderboards/{orgId}' => ['module' => 'org-leaderboards', 'action' => 'apiLeaderboard'],
// Org Partnerships
'org-partnerships' => ['module' => 'org-partnerships', 'action' => 'list'],
'org-partnerships/initiate' => ['module' => 'org-partnerships', 'action' => 'initiate'],
'org-partnerships/{id}' => ['module' => 'org-partnerships', 'action' => 'show'],
'org-partnerships/{id}/approve' => ['module' => 'org-partnerships', 'action' => 'approve'],
'org-partnerships/{id}/reject' => ['module' => 'org-partnerships', 'action' => 'reject'],
'org-partnerships/{id}/dissolve' => ['module' => 'org-partnerships', 'action' => 'dissolve'],
// Org Loyalty
'organizations/{id}/loyalty' => ['module' => 'org-loyalty', 'action' => 'list'],
'organizations/{id}/loyalty/create' => ['module' => 'org-loyalty', 'action' => 'create'],
'organizations/{id}/loyalty/store' => ['module' => 'org-loyalty', 'action' => 'store'],
'organizations/{id}/loyalty/{rewardId}/edit' => ['module' => 'org-loyalty', 'action' => 'edit'],
'organizations/{id}/loyalty/{rewardId}/update' => ['module' => 'org-loyalty', 'action' => 'update'],
'organizations/{id}/loyalty/{rewardId}/toggle' => ['module' => 'org-loyalty', 'action' => 'toggle'],
'organizations/{id}/loyalty/{rewardId}/delete' => ['module' => 'org-loyalty', 'action' => 'delete'],
'organizations/{id}/loyalty/claims' => ['module' => 'org-loyalty', 'action' => 'claims'],
// Org Rosters
'organizations/{id}/rosters' => ['module' => 'org-rosters', 'action' => 'list'],
'organizations/{id}/rosters/create' => ['module' => 'org-rosters', 'action' => 'create'],
'organizations/{id}/rosters/store' => ['module' => 'org-rosters', 'action' => 'store'],
'organizations/{id}/rosters/{rosterId}' => ['module' => 'org-rosters', 'action' => 'show'],
'organizations/{id}/rosters/{rosterId}/update' => ['module' => 'org-rosters', 'action' => 'update'],
'organizations/{id}/rosters/{rosterId}/add-player' => ['module' => 'org-rosters', 'action' => 'addPlayer'],
'organizations/{id}/rosters/{rosterId}/remove-player' => ['module' => 'org-rosters', 'action' => 'removePlayer'],
'organizations/{id}/rosters/{rosterId}/archive' => ['module' => 'org-rosters', 'action' => 'archive'],
// Org Training
'organizations/{id}/training' => ['module' => 'org-training', 'action' => 'list'],
'organizations/{id}/training/create' => ['module' => 'org-training', 'action' => 'create'],
'organizations/{id}/training/store' => ['module' => 'org-training', 'action' => 'store'],
'organizations/{id}/training/{sessionId}/edit' => ['module' => 'org-training', 'action' => 'edit'],
'organizations/{id}/training/{sessionId}/update' => ['module' => 'org-training', 'action' => 'update'],
'organizations/{id}/training/{sessionId}/cancel' => ['module' => 'org-training', 'action' => 'cancel'],
'organizations/{id}/training/{sessionId}/complete' => ['module' => 'org-training', 'action' => 'complete'],
// Org Media
'organizations/{id}/media' => ['module' => 'org-media', 'action' => 'gallery'],
'organizations/{id}/media/upload' => ['module' => 'org-media', 'action' => 'upload'],
'organizations/{id}/media/store' => ['module' => 'org-media', 'action' => 'store'],
'organizations/{id}/media/{mediaId}/delete' => ['module' => 'org-media', 'action' => 'delete'],
'organizations/{id}/media/bulk-delete' => ['module' => 'org-media', 'action' => 'bulkDelete'],
// Org Spotlights
'organizations/{id}/spotlights' => ['module' => 'org-spotlights', 'action' => 'list'],
'organizations/{id}/spotlights/create' => ['module' => 'org-spotlights', 'action' => 'create'],
'organizations/{id}/spotlights/store' => ['module' => 'org-spotlights', 'action' => 'store'],
'organizations/{id}/spotlights/{spotId}/toggle' => ['module' => 'org-spotlights', 'action' => 'toggle'],
'organizations/{id}/spotlights/{spotId}/delete' => ['module' => 'org-spotlights', 'action' => 'delete'],
// Org Announcements
'organizations/{id}/announcements' => ['module' => 'org-announcements', 'action' => 'list'],
'organizations/{id}/announcements/create' => ['module' => 'org-announcements', 'action' => 'create'],
'organizations/{id}/announcements/store' => ['module' => 'org-announcements', 'action' => 'store'],
'organizations/{id}/announcements/{annId}/edit' => ['module' => 'org-announcements', 'action' => 'edit'],
'organizations/{id}/announcements/{annId}/update' => ['module' => 'org-announcements', 'action' => 'update'],
'organizations/{id}/announcements/{annId}/publish' => ['module' => 'org-announcements', 'action' => 'publish'],
'organizations/{id}/announcements/{annId}/delete' => ['module' => 'org-announcements', 'action' => 'delete'],
]; ];
<?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">
......
-- Migration 003: Organizational Steroids Update
-- Run against Supabase PostgreSQL
-- ============================================================
-- 1. PLAYER PROFILE IMAGES & FRAMES
-- ============================================================
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS avatar_frame_id UUID,
ADD COLUMN IF NOT EXISTS avatar_border_color TEXT,
ADD COLUMN IF NOT EXISTS active_org_frame_id UUID;
CREATE TABLE IF NOT EXISTS profile_frames (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
name_ar TEXT,
description TEXT,
description_ar TEXT,
image_url TEXT NOT NULL,
thumbnail_url TEXT,
category TEXT NOT NULL DEFAULT 'general' CHECK (category IN ('general','seasonal','achievement','org','event','premium')),
rarity TEXT DEFAULT 'common' CHECK (rarity IN ('common','uncommon','rare','epic','legendary')),
price_coins INT DEFAULT 0,
price_gems INT DEFAULT 0,
is_purchasable BOOLEAN DEFAULT true,
is_active BOOLEAN DEFAULT true,
required_level INT DEFAULT 0,
required_achievement_id UUID,
org_id UUID REFERENCES el3ab_organizations(id) ON DELETE SET NULL,
max_supply INT,
current_supply INT DEFAULT 0,
available_from TIMESTAMPTZ,
available_until TIMESTAMPTZ,
sort_order INT DEFAULT 0,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS player_frames (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
frame_id UUID NOT NULL REFERENCES profile_frames(id) ON DELETE CASCADE,
acquired_at TIMESTAMPTZ DEFAULT now(),
acquisition_type TEXT DEFAULT 'purchase' CHECK (acquisition_type IN ('purchase','reward','gift','achievement','org_membership','admin_grant')),
source_id TEXT,
is_equipped BOOLEAN DEFAULT false,
UNIQUE(player_id, frame_id)
);
-- ============================================================
-- 2. ORGANIZATION INVITE LINKS
-- ============================================================
CREATE TABLE IF NOT EXISTS org_invite_links (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
code TEXT NOT NULL UNIQUE,
created_by UUID NOT NULL,
max_uses INT,
current_uses INT DEFAULT 0,
expires_at TIMESTAMPTZ,
requires_approval BOOLEAN DEFAULT false,
target_role TEXT DEFAULT 'member' CHECK (target_role IN ('member','moderator','admin')),
welcome_message TEXT,
welcome_message_ar TEXT,
is_active BOOLEAN DEFAULT true,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS org_invite_uses (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
invite_link_id UUID NOT NULL REFERENCES org_invite_links(id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
used_at TIMESTAMPTZ DEFAULT now(),
status TEXT DEFAULT 'joined' CHECK (status IN ('joined','pending_approval','rejected')),
is_new_signup BOOLEAN DEFAULT false
);
-- ============================================================
-- 3. ORG MEMBERSHIP APPLICATIONS WITH PROOF
-- ============================================================
CREATE TABLE IF NOT EXISTS org_membership_applications (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected','withdrawn')),
application_text TEXT,
proof_documents JSONB DEFAULT '[]',
invite_link_id UUID REFERENCES org_invite_links(id),
reviewed_by UUID,
review_note TEXT,
reviewed_at TIMESTAMPTZ,
target_role TEXT DEFAULT 'member',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 4. ORG CHAT
-- ============================================================
CREATE TABLE IF NOT EXISTS org_chat_channels (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
name_ar TEXT,
type TEXT DEFAULT 'general' CHECK (type IN ('general','announcements','admin_only','tournament','custom')),
description TEXT,
is_default BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS org_chat_messages (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
channel_id UUID NOT NULL REFERENCES org_chat_channels(id) ON DELETE CASCADE,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
content TEXT NOT NULL,
message_type TEXT DEFAULT 'text' CHECK (message_type IN ('text','image','system','announcement')),
attachment_url TEXT,
reply_to_id UUID REFERENCES org_chat_messages(id),
is_pinned BOOLEAN DEFAULT false,
is_deleted BOOLEAN DEFAULT false,
deleted_by UUID,
deleted_at TIMESTAMPTZ,
edited_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_org_chat_messages_channel ON org_chat_messages(channel_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_org_chat_messages_org ON org_chat_messages(org_id, created_at DESC);
CREATE TABLE IF NOT EXISTS org_chat_moderation (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
message_id UUID REFERENCES org_chat_messages(id),
target_player_id UUID NOT NULL REFERENCES profiles(id),
action TEXT NOT NULL CHECK (action IN ('delete_message','mute','unmute','warn','ban_from_chat')),
reason TEXT,
moderator_id UUID NOT NULL,
duration_minutes INT,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 5. ORG ANNOUNCEMENTS
-- ============================================================
CREATE TABLE IF NOT EXISTS org_announcements (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
title_ar TEXT,
content TEXT NOT NULL,
content_ar TEXT,
type TEXT DEFAULT 'general' CHECK (type IN ('general','important','event','tournament','maintenance')),
priority INT DEFAULT 0,
is_pinned BOOLEAN DEFAULT false,
image_url TEXT,
author_id UUID NOT NULL,
published_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
is_draft BOOLEAN DEFAULT true,
view_count INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 6. ORG ACHIEVEMENTS / BADGES
-- ============================================================
CREATE TABLE IF NOT EXISTS org_achievements (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
name_ar TEXT,
description TEXT,
description_ar TEXT,
icon_url TEXT,
badge_url TEXT,
category TEXT DEFAULT 'general' CHECK (category IN ('general','tournament','social','loyalty','special')),
criteria JSONB DEFAULT '{}',
reward_coins INT DEFAULT 0,
reward_gems INT DEFAULT 0,
reward_frame_id UUID REFERENCES profile_frames(id),
is_repeatable BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS player_achievements (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
achievement_id UUID NOT NULL REFERENCES org_achievements(id) ON DELETE CASCADE,
org_id UUID REFERENCES el3ab_organizations(id),
earned_at TIMESTAMPTZ DEFAULT now(),
progress INT DEFAULT 0,
is_complete BOOLEAN DEFAULT false,
notified BOOLEAN DEFAULT false,
UNIQUE(player_id, achievement_id)
);
-- ============================================================
-- 7. ORG TREASURY
-- ============================================================
CREATE TABLE IF NOT EXISTS org_treasury (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL UNIQUE REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
balance_coins BIGINT DEFAULT 0,
balance_gems BIGINT DEFAULT 0,
total_deposited_coins BIGINT DEFAULT 0,
total_deposited_gems BIGINT DEFAULT 0,
total_withdrawn_coins BIGINT DEFAULT 0,
total_withdrawn_gems BIGINT DEFAULT 0,
settings JSONB DEFAULT '{}',
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS org_treasury_transactions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
type TEXT NOT NULL CHECK (type IN ('deposit','withdrawal','reward','tax','admin_grant','admin_revoke')),
currency TEXT NOT NULL CHECK (currency IN ('coins','gems')),
amount BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
player_id UUID REFERENCES profiles(id),
reason TEXT,
approved_by UUID,
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 8. ORG INTERNAL LEADERBOARD
-- ============================================================
CREATE TABLE IF NOT EXISTS org_leaderboards (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
season TEXT,
points INT DEFAULT 0,
matches_played INT DEFAULT 0,
matches_won INT DEFAULT 0,
tournaments_played INT DEFAULT 0,
tournaments_won INT DEFAULT 0,
rank INT,
previous_rank INT,
streak_current INT DEFAULT 0,
streak_best INT DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(org_id, player_id, season)
);
-- ============================================================
-- 9. ORG VS ORG CHALLENGES
-- ============================================================
CREATE TABLE IF NOT EXISTS org_challenges (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
challenger_org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
challenged_org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
game_key TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending','accepted','rejected','in_progress','completed','cancelled')),
title TEXT,
title_ar TEXT,
format JSONB DEFAULT '{}',
roster_challenger JSONB DEFAULT '[]',
roster_challenged JSONB DEFAULT '[]',
score_challenger NUMERIC(5,1) DEFAULT 0,
score_challenged NUMERIC(5,1) DEFAULT 0,
winner_org_id UUID REFERENCES el3ab_organizations(id),
prize_pool JSONB DEFAULT '{}',
scheduled_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 10. ORG ACTIVITY LOG
-- ============================================================
CREATE TABLE IF NOT EXISTS org_activity_log (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
actor_id UUID NOT NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
details JSONB DEFAULT '{}',
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_org_activity_log_org ON org_activity_log(org_id, created_at DESC);
-- ============================================================
-- 11. ORG BRANDING EXTENSION
-- ============================================================
ALTER TABLE el3ab_organizations
ADD COLUMN IF NOT EXISTS banner_url TEXT,
ADD COLUMN IF NOT EXISTS primary_color TEXT DEFAULT '#6366f1',
ADD COLUMN IF NOT EXISTS secondary_color TEXT DEFAULT '#8b5cf6',
ADD COLUMN IF NOT EXISTS social_links JSONB DEFAULT '{}',
ADD COLUMN IF NOT EXISTS custom_css TEXT,
ADD COLUMN IF NOT EXISTS tier TEXT DEFAULT 'bronze',
ADD COLUMN IF NOT EXISTS tier_points INT DEFAULT 0,
ADD COLUMN IF NOT EXISTS member_count INT DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_tournaments INT DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_matches INT DEFAULT 0,
ADD COLUMN IF NOT EXISTS founded_at DATE,
ADD COLUMN IF NOT EXISTS max_members INT DEFAULT 50,
ADD COLUMN IF NOT EXISTS features JSONB DEFAULT '{}';
-- ============================================================
-- 12. PLAYER TRANSFER SYSTEM
-- ============================================================
CREATE TABLE IF NOT EXISTS org_player_transfers (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
from_org_id UUID REFERENCES el3ab_organizations(id),
to_org_id UUID NOT NULL REFERENCES el3ab_organizations(id),
status TEXT DEFAULT 'pending' CHECK (status IN ('pending','player_accepted','from_org_approved','to_org_approved','completed','rejected','cancelled')),
transfer_type TEXT DEFAULT 'request' CHECK (transfer_type IN ('request','offer','free_agent')),
transfer_fee_coins INT DEFAULT 0,
transfer_fee_gems INT DEFAULT 0,
initiated_by UUID NOT NULL,
player_consent BOOLEAN DEFAULT false,
from_org_consent BOOLEAN DEFAULT false,
to_org_consent BOOLEAN DEFAULT false,
admin_override BOOLEAN DEFAULT false,
notes TEXT,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 13. ORG RECRUITMENT BOARD
-- ============================================================
CREATE TABLE IF NOT EXISTS org_recruitment_posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
title_ar TEXT,
description TEXT NOT NULL,
description_ar TEXT,
requirements JSONB DEFAULT '{}',
positions_available INT DEFAULT 1,
positions_filled INT DEFAULT 0,
game_key TEXT,
status TEXT DEFAULT 'open' CHECK (status IN ('open','closed','filled','expired')),
expires_at TIMESTAMPTZ,
is_featured BOOLEAN DEFAULT false,
applications_count INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 14. ORG EVENTS CALENDAR
-- ============================================================
CREATE TABLE IF NOT EXISTS org_events (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
title_ar TEXT,
description TEXT,
description_ar TEXT,
event_type TEXT DEFAULT 'general' CHECK (event_type IN ('general','tournament','training','meeting','social','external')),
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ,
location TEXT,
is_online BOOLEAN DEFAULT true,
link TEXT,
max_participants INT,
current_participants INT DEFAULT 0,
is_public BOOLEAN DEFAULT true,
is_recurring BOOLEAN DEFAULT false,
recurrence_rule JSONB DEFAULT '{}',
tournament_id UUID,
created_by UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS org_event_participants (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
event_id UUID NOT NULL REFERENCES org_events(id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
status TEXT DEFAULT 'attending' CHECK (status IN ('attending','maybe','declined','waitlisted')),
registered_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(event_id, player_id)
);
-- ============================================================
-- 15. PLAYER LOYALTY REWARDS
-- ============================================================
CREATE TABLE IF NOT EXISTS org_loyalty_rewards (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
name_ar TEXT,
description TEXT,
days_required INT NOT NULL,
reward_type TEXT NOT NULL CHECK (reward_type IN ('coins','gems','frame','achievement','title')),
reward_amount INT DEFAULT 0,
reward_frame_id UUID REFERENCES profile_frames(id),
reward_title TEXT,
is_active BOOLEAN DEFAULT true,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS player_loyalty_claims (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
reward_id UUID NOT NULL REFERENCES org_loyalty_rewards(id) ON DELETE CASCADE,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
claimed_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(player_id, reward_id)
);
-- ============================================================
-- 16. ORG CONTENT (GUIDES/STRATEGIES)
-- ============================================================
CREATE TABLE IF NOT EXISTS org_content (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
title_ar TEXT,
body TEXT NOT NULL,
body_ar TEXT,
category TEXT DEFAULT 'guide' CHECK (category IN ('guide','strategy','tutorial','announcement','resource')),
game_key TEXT,
author_id UUID NOT NULL REFERENCES profiles(id),
is_published BOOLEAN DEFAULT false,
is_pinned BOOLEAN DEFAULT false,
visibility TEXT DEFAULT 'members' CHECK (visibility IN ('members','public','admin_only')),
tags JSONB DEFAULT '[]',
view_count INT DEFAULT 0,
attachments JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 17. ORG MEDIA GALLERY
-- ============================================================
CREATE TABLE IF NOT EXISTS org_media (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
title TEXT,
description TEXT,
file_url TEXT NOT NULL,
thumbnail_url TEXT,
media_type TEXT NOT NULL CHECK (media_type IN ('image','video','document')),
file_size BIGINT DEFAULT 0,
mime_type TEXT,
album TEXT DEFAULT 'general',
uploaded_by UUID NOT NULL,
is_public BOOLEAN DEFAULT true,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 18. ORG ROSTER MANAGEMENT
-- ============================================================
CREATE TABLE IF NOT EXISTS org_rosters (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
name_ar TEXT,
game_key TEXT NOT NULL,
status TEXT DEFAULT 'active' CHECK (status IN ('active','archived','draft')),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS org_roster_players (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
roster_id UUID NOT NULL REFERENCES org_rosters(id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
position TEXT DEFAULT 'member' CHECK (position IN ('starter','substitute','reserve','captain','coach')),
jersey_number INT,
notes TEXT,
joined_roster_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(roster_id, player_id)
);
-- ============================================================
-- 19. SEASONAL ORG RANKINGS
-- ============================================================
CREATE TABLE IF NOT EXISTS org_seasonal_rankings (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
season TEXT NOT NULL,
rank INT,
points INT DEFAULT 0,
matches_won INT DEFAULT 0,
matches_lost INT DEFAULT 0,
tournaments_won INT DEFAULT 0,
challenges_won INT DEFAULT 0,
member_activity_score INT DEFAULT 0,
previous_rank INT,
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(org_id, season)
);
-- ============================================================
-- 20. ORG PARTNERSHIPS / ALLIANCES
-- ============================================================
CREATE TABLE IF NOT EXISTS org_partnerships (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_a_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
org_b_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending','active','dissolved','rejected')),
type TEXT DEFAULT 'alliance' CHECK (type IN ('alliance','partnership','sister_org','sponsor')),
terms TEXT,
benefits JSONB DEFAULT '{}',
initiated_by UUID NOT NULL,
accepted_by UUID,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
CONSTRAINT different_orgs CHECK (org_a_id != org_b_id)
);
-- ============================================================
-- 21. MEMBER SPOTLIGHT / MVP SYSTEM
-- ============================================================
CREATE TABLE IF NOT EXISTS org_member_spotlights (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
type TEXT DEFAULT 'mvp' CHECK (type IN ('mvp','newcomer','most_improved','top_contributor','custom')),
period TEXT,
title TEXT,
description TEXT,
stats JSONB DEFAULT '{}',
awarded_by UUID NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 22. ORG TRAINING SESSIONS
-- ============================================================
CREATE TABLE IF NOT EXISTS org_training_sessions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
title_ar TEXT,
description TEXT,
game_key TEXT,
coach_id UUID REFERENCES profiles(id),
starts_at TIMESTAMPTZ NOT NULL,
duration_minutes INT DEFAULT 60,
max_participants INT,
current_participants INT DEFAULT 0,
skill_level TEXT DEFAULT 'all' CHECK (skill_level IN ('beginner','intermediate','advanced','all')),
topics JSONB DEFAULT '[]',
recording_url TEXT,
materials_url TEXT,
status TEXT DEFAULT 'scheduled' CHECK (status IN ('scheduled','in_progress','completed','cancelled')),
is_recurring BOOLEAN DEFAULT false,
recurrence_rule JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 23. REFERRAL TRACKING
-- ============================================================
CREATE TABLE IF NOT EXISTS org_referrals (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID NOT NULL REFERENCES el3ab_organizations(id) ON DELETE CASCADE,
referrer_id UUID NOT NULL REFERENCES profiles(id),
referred_id UUID NOT NULL REFERENCES profiles(id),
invite_link_id UUID REFERENCES org_invite_links(id),
status TEXT DEFAULT 'pending' CHECK (status IN ('pending','active','rewarded','expired')),
reward_given BOOLEAN DEFAULT false,
reward_amount INT DEFAULT 0,
reward_currency TEXT DEFAULT 'coins',
referred_at TIMESTAMPTZ DEFAULT now(),
activated_at TIMESTAMPTZ,
UNIQUE(org_id, referred_id)
);
-- ============================================================
-- EXTEND org_members
-- ============================================================
ALTER TABLE org_members
ADD COLUMN IF NOT EXISTS display_name TEXT,
ADD COLUMN IF NOT EXISTS custom_title TEXT,
ADD COLUMN IF NOT EXISTS permissions JSONB DEFAULT '{}',
ADD COLUMN IF NOT EXISTS is_muted BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS muted_until TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS contribution_points INT DEFAULT 0,
ADD COLUMN IF NOT EXISTS last_active_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS invited_by UUID,
ADD COLUMN IF NOT EXISTS invite_link_id UUID;
-- ============================================================
-- RLS + SERVICE ROLE POLICIES
-- ============================================================
ALTER TABLE profile_frames ENABLE ROW LEVEL SECURITY;
ALTER TABLE player_frames ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_invite_links ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_invite_uses ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_membership_applications ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_chat_channels ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_chat_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_chat_moderation ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_announcements ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_achievements ENABLE ROW LEVEL SECURITY;
ALTER TABLE player_achievements ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_treasury ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_treasury_transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_leaderboards ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_challenges ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_activity_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_player_transfers ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_recruitment_posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_events ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_event_participants ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_loyalty_rewards ENABLE ROW LEVEL SECURITY;
ALTER TABLE player_loyalty_claims ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_content ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_media ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_rosters ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_roster_players ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_seasonal_rankings ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_partnerships ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_member_spotlights ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_training_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE org_referrals ENABLE ROW LEVEL SECURITY;
DO $$
DECLARE
t TEXT;
BEGIN
FOREACH t IN ARRAY ARRAY[
'profile_frames','player_frames','org_invite_links','org_invite_uses',
'org_membership_applications','org_chat_channels','org_chat_messages',
'org_chat_moderation','org_announcements','org_achievements',
'player_achievements','org_treasury','org_treasury_transactions',
'org_leaderboards','org_challenges','org_activity_log',
'org_player_transfers','org_recruitment_posts','org_events',
'org_event_participants','org_loyalty_rewards','player_loyalty_claims',
'org_content','org_media','org_rosters','org_roster_players',
'org_seasonal_rankings','org_partnerships','org_member_spotlights',
'org_training_sessions','org_referrals'
] LOOP
EXECUTE format('CREATE POLICY "service_role_all_%s" ON %I FOR ALL TO service_role USING (true) WITH CHECK (true)', t, t);
END LOOP;
END $$;
...@@ -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;
}
<?php
class OrgAchievementsController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$category = $_GET['category'] ?? '';
$orgId = $_GET['org_id'] ?? '';
$queryParams = ['select' => '*', 'order' => 'sort_order.asc,created_at.desc'];
if ($category) {
$queryParams['category'] = "eq.{$category}";
}
if ($orgId) {
$queryParams['org_id'] = "eq.{$orgId}";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_achievements', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$achievements = $this->db->select('org_achievements', $queryParams);
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'الإنجازات والشارات';
$moduleCSS = 'org-achievements';
View::render('org-achievements/list', compact('achievements', 'organizations', 'pagination', 'category', 'orgId', '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',
]);
$frames = $this->db->select('profile_frames', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$achievement = [];
$pageTitle = 'إضافة إنجاز';
$moduleCSS = 'org-achievements';
View::render('org-achievements/form', compact('achievement', 'organizations', 'frames', 'pageTitle', 'moduleCSS'));
}
public function store(array $params, string $method): void
{
Auth::requireCsrf();
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
Response::error('اسم الإنجاز مطلوب', '/org-achievements/create');
return;
}
$data = [
'name' => $name,
'name_ar' => trim($_POST['name_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'category' => $_POST['category'] ?? 'general',
'reward_coins' => (int)($_POST['reward_coins'] ?? 0),
'reward_gems' => (int)($_POST['reward_gems'] ?? 0),
'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null,
'is_repeatable' => isset($_POST['is_repeatable']),
'is_active' => true,
'sort_order' => (int)($_POST['sort_order'] ?? 0),
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'criteria' => json_encode([
'type' => $_POST['criteria_type'] ?? '',
'threshold' => (int)($_POST['criteria_threshold'] ?? 0),
]),
];
if (isset($_FILES['icon']) && $_FILES['icon']['error'] === UPLOAD_ERR_OK) {
$storage = SupabaseStorage::getInstance();
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['icon']['tmp_name']);
$path = $storage->generatePath('achievements', $_FILES['icon']['name']);
$data['icon_url'] = $storage->upload('profile-frames', $path, $_FILES['icon']['tmp_name'], $mime);
}
if (isset($_FILES['badge']) && $_FILES['badge']['error'] === UPLOAD_ERR_OK) {
$storage = SupabaseStorage::getInstance();
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['badge']['tmp_name']);
$path = $storage->generatePath('achievements/badges', $_FILES['badge']['name']);
$data['badge_url'] = $storage->upload('profile-frames', $path, $_FILES['badge']['tmp_name'], $mime);
}
$result = $this->db->insert('org_achievements', $data);
AuditLog::log('create', 'org_achievement', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء الإنجاز', '/org-achievements');
}
public function edit(array $params, string $method): void
{
$id = $params['id'];
$achievement = $this->db->selectOne('org_achievements', ['id' => "eq.{$id}"]);
if (!$achievement) {
Response::error('الإنجاز غير موجود', '/org-achievements');
return;
}
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$frames = $this->db->select('profile_frames', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'تعديل الإنجاز';
$moduleCSS = 'org-achievements';
View::render('org-achievements/form', compact('achievement', 'organizations', 'frames', 'pageTitle', 'moduleCSS'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$data = [
'name' => trim($_POST['name'] ?? ''),
'name_ar' => trim($_POST['name_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'category' => $_POST['category'] ?? 'general',
'reward_coins' => (int)($_POST['reward_coins'] ?? 0),
'reward_gems' => (int)($_POST['reward_gems'] ?? 0),
'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null,
'is_repeatable' => isset($_POST['is_repeatable']),
'sort_order' => (int)($_POST['sort_order'] ?? 0),
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'criteria' => json_encode([
'type' => $_POST['criteria_type'] ?? '',
'threshold' => (int)($_POST['criteria_threshold'] ?? 0),
]),
'updated_at' => date('c'),
];
$this->db->update('org_achievements', ['id' => "eq.{$id}"], $data);
AuditLog::log('update', 'org_achievement', $id, null, $data);
Response::success('تم تحديث الإنجاز', '/org-achievements');
}
public function toggle(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$achievement = $this->db->selectOne('org_achievements', ['id' => "eq.{$id}"]);
if (!$achievement) {
Response::error('الإنجاز غير موجود', '/org-achievements');
return;
}
$newStatus = !($achievement['is_active'] ?? false);
$this->db->update('org_achievements', ['id' => "eq.{$id}"], ['is_active' => $newStatus, 'updated_at' => date('c')]);
Response::success($newStatus ? 'تم تفعيل الإنجاز' : 'تم تعطيل الإنجاز', '/org-achievements');
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->delete('org_achievements', ['id' => "eq.{$id}"]);
AuditLog::log('delete', 'org_achievement', $id);
Response::success('تم حذف الإنجاز', '/org-achievements');
}
public function grantToPlayer(array $params, string $method): void
{
Auth::requireCsrf();
$achievementId = $params['id'];
$playerId = $_POST['player_id'] ?? '';
if (empty($playerId)) {
Response::error('يرجى تحديد اللاعب', '/org-achievements');
return;
}
$achievement = $this->db->selectOne('org_achievements', ['id' => "eq.{$achievementId}"]);
if (!$achievement) {
Response::error('الإنجاز غير موجود', '/org-achievements');
return;
}
$existing = $this->db->selectOne('player_achievements', [
'player_id' => "eq.{$playerId}",
'achievement_id' => "eq.{$achievementId}",
]);
if ($existing && !($achievement['is_repeatable'] ?? false)) {
Response::error('اللاعب يملك هذا الإنجاز بالفعل', '/org-achievements');
return;
}
if (!$existing) {
$this->db->insert('player_achievements', [
'player_id' => $playerId,
'achievement_id' => $achievementId,
'org_id' => $achievement['org_id'],
'is_complete' => true,
'progress' => ($achievement['criteria'] ? json_decode($achievement['criteria'], true)['threshold'] ?? 1 : 1),
]);
} else {
$this->db->update('player_achievements', [
'player_id' => "eq.{$playerId}",
'achievement_id' => "eq.{$achievementId}",
], ['is_complete' => true, 'earned_at' => date('c')]);
}
if (($achievement['reward_coins'] ?? 0) > 0 || ($achievement['reward_gems'] ?? 0) > 0) {
$player = $this->db->selectOne('profiles', ['id' => "eq.{$playerId}"]);
$updates = [];
if ($achievement['reward_coins'] > 0) {
$updates['coins'] = ($player['coins'] ?? 0) + $achievement['reward_coins'];
}
if ($achievement['reward_gems'] > 0) {
$updates['gems'] = ($player['gems'] ?? 0) + $achievement['reward_gems'];
}
if (!empty($updates)) {
$this->db->update('profiles', ['id' => "eq.{$playerId}"], $updates);
}
}
$this->db->insert('notifications', [
'user_id' => $playerId,
'title' => 'إنجاز جديد!',
'body' => "حصلت على إنجاز: {$achievement['name']}",
'type' => 'social',
]);
AuditLog::log('grant_achievement', 'player_achievement', null, null, [
'player_id' => $playerId,
'achievement_id' => $achievementId,
]);
Response::success('تم منح الإنجاز', '/org-achievements');
}
public function playerProgress(array $params, string $method): void
{
$playerId = $_GET['player_id'] ?? '';
$orgId = $_GET['org_id'] ?? '';
$queryParams = ['select' => '*', 'order' => 'earned_at.desc'];
if ($playerId) {
$queryParams['player_id'] = "eq.{$playerId}";
}
if ($orgId) {
$queryParams['org_id'] = "eq.{$orgId}";
}
$total = $this->db->count('player_achievements', $queryParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$progress = $this->db->select('player_achievements', $queryParams);
$achievementIds = array_column($progress, 'achievement_id');
$achievements = [];
if (!empty($achievementIds)) {
$idList = '(' . implode(',', $achievementIds) . ')';
foreach ($this->db->select('org_achievements', ['id' => "in.{$idList}"]) as $a) {
$achievements[$a['id']] = $a;
}
}
$pageTitle = 'تقدم الإنجازات';
$moduleCSS = 'org-achievements';
View::render('org-achievements/player-progress', compact('progress', 'achievements', 'pagination', 'playerId', 'orgId', 'pageTitle', 'moduleCSS'));
}
}
<?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>
<?php
class OrgApplicationsController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$status = $_GET['status'] ?? '';
$orgId = $_GET['org'] ?? '';
$queryParams = ['select' => '*', 'order' => 'created_at.desc'];
if ($status) {
$queryParams['status'] = "eq.{$status}";
}
if ($orgId) {
$queryParams['org_id'] = "eq.{$orgId}";
}
$countParams = array_diff_key($queryParams, ['select' => 1, 'order' => 1]);
$total = $this->db->count('org_membership_applications', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$applications = $this->db->select('org_membership_applications', $queryParams);
// Fetch related player and org data
foreach ($applications as &$app) {
$app['player'] = $this->db->selectOne('profiles', ['id' => "eq.{$app['player_id']}"]);
$app['organization'] = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$app['org_id']}"]);
}
unset($app);
$pendingCount = $this->db->count('org_membership_applications', ['status' => 'eq.pending']);
// Fetch organizations for filter dropdown
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'طلبات العضوية';
$moduleCSS = 'org-applications';
$moduleJS = 'org-applications';
View::render('org-applications/list', compact(
'applications', 'pagination', 'status', 'orgId',
'pendingCount', 'organizations', 'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function show(array $params, string $method): void
{
$id = $params['id'];
$application = $this->db->selectOne('org_membership_applications', ['id' => "eq.{$id}"]);
if (!$application) {
http_response_code(404);
View::render('errors/404');
return;
}
$player = $this->db->selectOne('profiles', ['id' => "eq.{$application['player_id']}"]);
$organization = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$application['org_id']}"]);
// Generate signed URLs for proof documents
$proofDocuments = [];
$docs = $application['proof_documents'] ?? [];
if (is_string($docs)) {
$docs = json_decode($docs, true) ?? [];
}
$storage = SupabaseStorage::getInstance();
foreach ($docs as $doc) {
$signedUrl = $storage->getSignedUrl('org-documents', $doc['path'], 3600);
$proofDocuments[] = [
'name' => $doc['name'] ?? basename($doc['path']),
'path' => $doc['path'],
'type' => $doc['type'] ?? 'file',
'url' => $signedUrl,
];
}
// Get player's current org memberships
$playerOrgs = $this->db->select('org_members', [
'select' => '*',
'user_id' => "eq.{$application['player_id']}",
'status' => 'eq.active',
]);
foreach ($playerOrgs as &$membership) {
$membership['organization'] = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$membership['org_id']}"]);
}
unset($membership);
// Get reviewer info if already reviewed
$reviewer = null;
if (!empty($application['reviewed_by'])) {
$reviewer = $this->db->selectOne('profiles', ['id' => "eq.{$application['reviewed_by']}"]);
}
$pageTitle = 'تفاصيل طلب العضوية';
$moduleCSS = 'org-applications';
$moduleJS = 'org-applications';
View::render('org-applications/show', compact(
'application', 'player', 'organization', 'proofDocuments',
'playerOrgs', 'reviewer', 'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function approve(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$application = $this->db->selectOne('org_membership_applications', ['id' => "eq.{$id}"]);
if (!$application) {
Response::error('الطلب غير موجود', '/org-applications');
return;
}
if ($application['status'] !== 'pending') {
Response::error('هذا الطلب تم مراجعته مسبقاً', "/org-applications/{$id}");
return;
}
$reviewNote = trim($_POST['review_note'] ?? '');
$user = Auth::user();
// Update application status
$old = $application;
$this->db->update('org_membership_applications', ['id' => "eq.{$id}"], [
'status' => 'approved',
'reviewed_by' => $user['id'],
'review_note' => $reviewNote,
'reviewed_at' => date('c'),
]);
// Add to org_members
$this->db->insert('org_members', [
'org_id' => $application['org_id'],
'user_id' => $application['player_id'],
'role' => $application['target_role'] ?? 'member',
'status' => 'active',
'joined_at' => date('c'),
]);
// Send notification
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$application['org_id']}"]);
$orgName = $org['name_ar'] ?? $org['name'] ?? '';
$this->db->insert('notifications', [
'user_id' => $application['player_id'],
'title' => 'تم قبول طلب العضوية',
'body' => "تم قبولك في {$orgName}",
'type' => 'social',
]);
AuditLog::log('approve', 'org_application', $id, $old, [
'status' => 'approved',
'reviewed_by' => $user['id'],
'review_note' => $reviewNote,
]);
Response::success('تم قبول الطلب بنجاح', '/org-applications');
}
public function reject(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$application = $this->db->selectOne('org_membership_applications', ['id' => "eq.{$id}"]);
if (!$application) {
Response::error('الطلب غير موجود', '/org-applications');
return;
}
if ($application['status'] !== 'pending') {
Response::error('هذا الطلب تم مراجعته مسبقاً', "/org-applications/{$id}");
return;
}
$reviewNote = trim($_POST['review_note'] ?? '');
if (empty($reviewNote)) {
Response::error('يجب كتابة سبب الرفض', "/org-applications/{$id}");
return;
}
$user = Auth::user();
$old = $application;
$this->db->update('org_membership_applications', ['id' => "eq.{$id}"], [
'status' => 'rejected',
'reviewed_by' => $user['id'],
'review_note' => $reviewNote,
'reviewed_at' => date('c'),
]);
AuditLog::log('reject', 'org_application', $id, $old, [
'status' => 'rejected',
'reviewed_by' => $user['id'],
'review_note' => $reviewNote,
]);
Response::success('تم رفض الطلب', '/org-applications');
}
public function bulkApprove(array $params, string $method): void
{
Auth::requireCsrf();
$ids = $_POST['ids'] ?? [];
if (is_string($ids)) {
$ids = array_filter(explode(',', $ids));
}
if (empty($ids)) {
Response::error('يجب اختيار طلب واحد على الأقل', '/org-applications');
return;
}
$user = Auth::user();
$approvedCount = 0;
foreach ($ids as $id) {
$application = $this->db->selectOne('org_membership_applications', ['id' => "eq.{$id}"]);
if (!$application || $application['status'] !== 'pending') {
continue;
}
$this->db->update('org_membership_applications', ['id' => "eq.{$id}"], [
'status' => 'approved',
'reviewed_by' => $user['id'],
'review_note' => 'موافقة جماعية',
'reviewed_at' => date('c'),
]);
$this->db->insert('org_members', [
'org_id' => $application['org_id'],
'user_id' => $application['player_id'],
'role' => $application['target_role'] ?? 'member',
'status' => 'active',
'joined_at' => date('c'),
]);
// Send notification
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$application['org_id']}"]);
$orgName = $org['name_ar'] ?? $org['name'] ?? '';
$this->db->insert('notifications', [
'user_id' => $application['player_id'],
'title' => 'تم قبول طلب العضوية',
'body' => "تم قبولك في {$orgName}",
'type' => 'social',
]);
$approvedCount++;
}
AuditLog::log('bulk_approve', 'org_application', null, null, ['count' => $approvedCount]);
Response::success("تم قبول {$approvedCount} طلب بنجاح", '/org-applications');
}
public function bulkReject(array $params, string $method): void
{
Auth::requireCsrf();
$ids = $_POST['ids'] ?? [];
if (is_string($ids)) {
$ids = array_filter(explode(',', $ids));
}
$reviewNote = trim($_POST['review_note'] ?? '');
if (empty($ids)) {
Response::error('يجب اختيار طلب واحد على الأقل', '/org-applications');
return;
}
if (empty($reviewNote)) {
Response::error('يجب كتابة سبب الرفض', '/org-applications');
return;
}
$user = Auth::user();
$rejectedCount = 0;
foreach ($ids as $id) {
$application = $this->db->selectOne('org_membership_applications', ['id' => "eq.{$id}"]);
if (!$application || $application['status'] !== 'pending') {
continue;
}
$this->db->update('org_membership_applications', ['id' => "eq.{$id}"], [
'status' => 'rejected',
'reviewed_by' => $user['id'],
'review_note' => $reviewNote,
'reviewed_at' => date('c'),
]);
$rejectedCount++;
}
AuditLog::log('bulk_reject', 'org_application', null, null, ['count' => $rejectedCount, 'review_note' => $reviewNote]);
Response::success("تم رفض {$rejectedCount} طلب", '/org-applications');
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<h1>طلبات العضوية</h1>
<?php if ($pendingCount > 0): ?>
<span class="badge badge-warning"><?= $pendingCount ?> معلق</span>
<?php endif; ?>
</div>
</div>
<!-- Filters -->
<div class="flex items-center gap-4 mb-5">
<div class="filter-pills">
<a href="/org-applications" class="filter-pill <?= empty($status) ? 'active' : '' ?>">الكل</a>
<a href="/org-applications?status=pending<?= $orgId ? "&org={$orgId}" : '' ?>" class="filter-pill <?= $status === 'pending' ? 'active' : '' ?>">معلق</a>
<a href="/org-applications?status=approved<?= $orgId ? "&org={$orgId}" : '' ?>" class="filter-pill <?= $status === 'approved' ? 'active' : '' ?>">مقبول</a>
<a href="/org-applications?status=rejected<?= $orgId ? "&org={$orgId}" : '' ?>" class="filter-pill <?= $status === 'rejected' ? 'active' : '' ?>">مرفوض</a>
</div>
<select class="form-select" style="width: auto; padding: var(--space-2) var(--space-4);" onchange="location.href='/org-applications?status=<?= urlencode($status) ?>&org='+this.value">
<option value="">كل المنظمات</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= $org['id'] ?>" <?= $orgId === $org['id'] ? 'selected' : '' ?>>
<?= View::e($org['name_ar'] ?? $org['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="data-table-wrapper">
<?php if (empty($applications)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
<h3 class="empty-state-title">لا توجد طلبات</h3>
<p class="empty-state-text">لم يتم العثور على أي طلبات عضوية<?= $status ? ' بهذه الحالة' : '' ?></p>
</div>
<?php else: ?>
<!-- Bulk Actions Bar -->
<div class="bulk-actions-bar" id="bulkActionsBar" style="display: none;">
<div class="flex items-center gap-3">
<span class="text-sm"><span id="selectedCount">0</span> محدد</span>
<button type="button" class="btn btn-success btn-sm" onclick="bulkApprove()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
قبول المحدد
</button>
<button type="button" class="btn btn-danger btn-sm" onclick="bulkReject()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
رفض المحدد
</button>
</div>
</div>
<table class="data-table" id="applicationsTable">
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" class="check-all" onchange="toggleAllChecks(this)"></th>
<th>اللاعب</th>
<th>المنظمة</th>
<th>الحالة</th>
<th>تاريخ التقديم</th>
<th>المستندات</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($applications as $app): ?>
<tr>
<td>
<?php if (($app['status'] ?? '') === 'pending'): ?>
<input type="checkbox" class="row-check" value="<?= $app['id'] ?>" onchange="updateBulkBar()">
<?php endif; ?>
</td>
<td>
<div class="flex items-center gap-3">
<div class="avatar">
<?php if (!empty($app['player']['avatar_url'])): ?>
<img src="<?= View::e($app['player']['avatar_url']) ?>" alt="">
<?php else: ?>
<?= mb_substr($app['player']['username'] ?? '?', 0, 1) ?>
<?php endif; ?>
</div>
<div>
<div class="font-medium"><?= View::e($app['player']['display_name'] ?? $app['player']['username'] ?? 'غير معروف') ?></div>
<div class="text-xs text-muted">@<?= View::e($app['player']['username'] ?? '') ?></div>
</div>
</div>
</td>
<td>
<div class="font-medium"><?= View::e($app['organization']['name_ar'] ?? $app['organization']['name'] ?? '-') ?></div>
</td>
<td>
<?php
$statusBadges = ['pending' => 'warning', 'approved' => 'success', 'rejected' => 'danger'];
$statusLabels = ['pending' => 'معلق', 'approved' => 'مقبول', 'rejected' => 'مرفوض'];
?>
<span class="badge badge-<?= $statusBadges[$app['status']] ?? 'default' ?> badge-dot">
<?= $statusLabels[$app['status']] ?? $app['status'] ?>
</span>
</td>
<td class="text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($app['created_at'])) ?></td>
<td>
<?php
$docs = $app['proof_documents'] ?? [];
if (is_string($docs)) {
$docs = json_decode($docs, true) ?? [];
}
$docCount = count($docs);
?>
<?php if ($docCount > 0): ?>
<span class="badge badge-info"><?= $docCount ?> مستند</span>
<?php else: ?>
<span class="text-muted text-xs">-</span>
<?php endif; ?>
</td>
<td>
<div class="flex items-center gap-2">
<a href="/org-applications/<?= $app['id'] ?>" class="btn btn-ghost btn-sm">عرض</a>
<?php if (($app['status'] ?? '') === 'pending'): ?>
<form method="POST" action="/org-applications/<?= $app['id'] ?>/approve" style="display:inline;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-success btn-sm" title="قبول">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</button>
</form>
<button type="button" class="btn btn-danger btn-sm" title="رفض" onclick="quickReject('<?= $app['id'] ?>')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($orgId) ?>" class="pagination-btn <?= !$pagination->hasPrev() ? 'disabled' : '' ?>" <?= !$pagination->hasPrev() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($orgId) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&status=<?= urlencode($status) ?>&org=<?= urlencode($orgId) ?>" class="pagination-btn <?= !$pagination->hasNext() ? 'disabled' : '' ?>" <?= !$pagination->hasNext() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</div>
</div>
<?php endif; ?>
</div>
<!-- Quick Reject Modal -->
<div class="modal" id="rejectModal" style="display:none;">
<div class="modal-backdrop" onclick="closeRejectModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>رفض الطلب</h3>
<button class="btn btn-icon btn-ghost" onclick="closeRejectModal()">&times;</button>
</div>
<form method="POST" id="rejectForm">
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">سبب الرفض *</label>
<textarea name="review_note" class="form-input" required placeholder="اكتب سبب رفض الطلب..."></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button" class="btn btn-ghost" onclick="closeRejectModal()">إلغاء</button>
<button type="submit" class="btn btn-danger">رفض الطلب</button>
</div>
</form>
</div>
</div>
<!-- Bulk Reject Modal -->
<div class="modal" id="bulkRejectModal" style="display:none;">
<div class="modal-backdrop" onclick="closeBulkRejectModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>رفض الطلبات المحددة</h3>
<button class="btn btn-icon btn-ghost" onclick="closeBulkRejectModal()">&times;</button>
</div>
<form method="POST" action="/org-applications/bulk-reject" id="bulkRejectForm">
<?= Auth::csrfField() ?>
<input type="hidden" name="ids" id="bulkRejectIds">
<div class="form-group">
<label class="form-label">سبب الرفض *</label>
<textarea name="review_note" class="form-input" required placeholder="اكتب سبب رفض الطلبات..."></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button" class="btn btn-ghost" onclick="closeBulkRejectModal()">إلغاء</button>
<button type="submit" class="btn btn-danger">رفض الطلبات</button>
</div>
</form>
</div>
</div>
<script>
function toggleAllChecks(el) {
document.querySelectorAll('.row-check').forEach(cb => {
cb.checked = el.checked;
});
updateBulkBar();
}
function updateBulkBar() {
const checked = document.querySelectorAll('.row-check:checked');
const bar = document.getElementById('bulkActionsBar');
const count = document.getElementById('selectedCount');
if (checked.length > 0) {
bar.style.display = 'block';
count.textContent = checked.length;
} else {
bar.style.display = 'none';
}
}
function getSelectedIds() {
return Array.from(document.querySelectorAll('.row-check:checked')).map(cb => cb.value);
}
function bulkApprove() {
const ids = getSelectedIds();
if (ids.length === 0) return;
if (!confirm('هل تريد قبول ' + ids.length + ' طلب؟')) return;
const form = document.createElement('form');
form.method = 'POST';
form.action = '/org-applications/bulk-approve';
form.innerHTML = '<?= Auth::csrfField() ?>';
ids.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'ids[]';
input.value = id;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
function bulkReject() {
const ids = getSelectedIds();
if (ids.length === 0) return;
document.getElementById('bulkRejectIds').value = ids.join(',');
document.getElementById('bulkRejectModal').style.display = 'flex';
}
function closeBulkRejectModal() {
document.getElementById('bulkRejectModal').style.display = 'none';
}
function quickReject(id) {
const form = document.getElementById('rejectForm');
form.action = '/org-applications/' + id + '/reject';
document.getElementById('rejectModal').style.display = 'flex';
}
function closeRejectModal() {
document.getElementById('rejectModal').style.display = 'none';
}
</script>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-applications" 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>
<?php
$statusBadges = ['pending' => 'warning', 'approved' => 'success', 'rejected' => 'danger'];
$statusLabels = ['pending' => 'معلق', 'approved' => 'مقبول', 'rejected' => 'مرفوض'];
?>
<span class="badge badge-<?= $statusBadges[$application['status']] ?? 'default' ?>">
<?= $statusLabels[$application['status']] ?? $application['status'] ?>
</span>
</div>
</div>
<div class="grid grid-2 mb-6">
<!-- Left Column: Player Card -->
<div class="card">
<div class="card-header"><h3 class="card-title">معلومات اللاعب</h3></div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-4">
<div class="avatar avatar-lg">
<?php if (!empty($player['avatar_url'])): ?>
<img src="<?= View::e($player['avatar_url']) ?>" alt="">
<?php else: ?>
<?= mb_substr($player['username'] ?? '?', 0, 1) ?>
<?php endif; ?>
</div>
<div>
<div class="font-medium text-lg"><?= View::e($player['display_name'] ?? $player['username'] ?? 'غير معروف') ?></div>
<div class="text-muted">@<?= View::e($player['username'] ?? '') ?></div>
<?php if ($player): ?>
<a href="/players/<?= $player['id'] ?>" class="text-xs text-blue mt-1">عرض الملف الشخصي</a>
<?php endif; ?>
</div>
</div>
<div class="flex flex-col gap-2 text-sm">
<?php if (!empty($player['level'])): ?>
<div class="flex justify-between">
<span class="text-secondary">المستوى</span>
<span class="badge badge-info"><?= View::e($player['level']) ?></span>
</div>
<?php endif; ?>
</div>
<?php if (!empty($playerOrgs)): ?>
<div class="mt-3">
<span class="text-xs text-muted">العضويات الحالية:</span>
<div class="flex flex-col gap-2 mt-2">
<?php foreach ($playerOrgs as $membership): ?>
<div class="flex items-center gap-2 text-sm">
<span class="badge badge-default"><?= View::e($membership['organization']['name_ar'] ?? $membership['organization']['name'] ?? '-') ?></span>
<span class="text-xs text-muted">(<?= View::e($membership['role'] ?? 'member') ?>)</span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="mt-3">
<span class="text-xs text-muted">لا يوجد عضويات حالية</span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Right Column: Application Details -->
<div class="card">
<div class="card-header"><h3 class="card-title">تفاصيل الطلب</h3></div>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-secondary">المنظمة</span>
<span class="font-medium">
<?php if ($organization): ?>
<a href="/organizations/<?= $organization['id'] ?>" class="text-blue">
<?= View::e($organization['name_ar'] ?? $organization['name']) ?>
</a>
<?php else: ?>
غير معروفة
<?php endif; ?>
</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">تاريخ التقديم</span>
<span class="tabular-nums"><?= date('Y-m-d H:i', strtotime($application['created_at'])) ?></span>
</div>
<div class="flex justify-between">
<span class="text-secondary">الدور المطلوب</span>
<span class="badge badge-default"><?= View::e($application['target_role'] ?? 'member') ?></span>
</div>
<div class="flex justify-between">
<span class="text-secondary">الحالة</span>
<span class="badge badge-<?= $statusBadges[$application['status']] ?? 'default' ?> badge-dot">
<?= $statusLabels[$application['status']] ?? $application['status'] ?>
</span>
</div>
<?php if (!empty($application['application_text'])): ?>
<div class="mt-3">
<span class="text-secondary">نص الطلب:</span>
<p class="mt-2 p-3" style="background: var(--bg-secondary); border-radius: var(--radius-md);">
<?= nl2br(View::e($application['application_text'])) ?>
</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Proof Documents Section -->
<div class="card mb-6">
<div class="card-header">
<h3 class="card-title">المستندات المرفقة</h3>
<?php if (!empty($proofDocuments)): ?>
<span class="badge badge-info"><?= count($proofDocuments) ?> مستند</span>
<?php endif; ?>
</div>
<?php if (empty($proofDocuments)): ?>
<div class="text-center text-muted py-4">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="margin: 0 auto; opacity: 0.5;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<p class="mt-2 text-sm">لا توجد مستندات مرفقة</p>
</div>
<?php else: ?>
<div class="grid grid-3 gap-4">
<?php foreach ($proofDocuments as $doc): ?>
<?php
$isImage = in_array($doc['type'], ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image'])
|| preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $doc['name']);
$isPdf = $doc['type'] === 'application/pdf' || str_ends_with(strtolower($doc['name']), '.pdf');
?>
<div class="proof-doc-card" style="border: 1px solid var(--border-color); border-radius: var(--radius-md); overflow: hidden;">
<?php if ($isImage): ?>
<a href="<?= View::e($doc['url']) ?>" target="_blank" class="proof-doc-preview">
<img src="<?= View::e($doc['url']) ?>" alt="<?= View::e($doc['name']) ?>" style="width: 100%; height: 160px; object-fit: cover; display: block;">
</a>
<?php elseif ($isPdf): ?>
<a href="<?= View::e($doc['url']) ?>" target="_blank" class="proof-doc-preview" style="display: flex; align-items: center; justify-content: center; height: 160px; background: var(--bg-secondary);">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity: 0.6;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
</a>
<?php else: ?>
<a href="<?= View::e($doc['url']) ?>" target="_blank" class="proof-doc-preview" style="display: flex; align-items: center; justify-content: center; height: 160px; background: var(--bg-secondary);">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity: 0.6;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
</a>
<?php endif; ?>
<div class="p-3 flex items-center justify-between" style="border-top: 1px solid var(--border-color);">
<span class="text-xs truncate" style="max-width: 150px;"><?= View::e($doc['name']) ?></span>
<a href="<?= View::e($doc['url']) ?>" target="_blank" class="btn btn-ghost btn-sm" title="تحميل">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Action / Review Section -->
<?php if ($application['status'] === 'pending'): ?>
<div class="grid grid-2">
<div class="card">
<div class="card-header"><h3 class="card-title">قبول الطلب</h3></div>
<form method="POST" action="/org-applications/<?= $application['id'] ?>/approve">
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">ملاحظة (اختياري)</label>
<textarea name="review_note" class="form-input" placeholder="أضف ملاحظة على القبول..."></textarea>
</div>
<button type="submit" class="btn btn-success w-full">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
قبول الطلب
</button>
</form>
</div>
<div class="card">
<div class="card-header"><h3 class="card-title">رفض الطلب</h3></div>
<form method="POST" action="/org-applications/<?= $application['id'] ?>/reject">
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">سبب الرفض *</label>
<textarea name="review_note" class="form-input" required placeholder="اكتب سبب رفض الطلب..."></textarea>
</div>
<button type="submit" class="btn btn-danger w-full">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
رفض الطلب
</button>
</form>
</div>
</div>
<?php else: ?>
<div class="card">
<div class="card-header"><h3 class="card-title">نتيجة المراجعة</h3></div>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-secondary">القرار</span>
<span class="badge badge-<?= $statusBadges[$application['status']] ?? 'default' ?>">
<?= $statusLabels[$application['status']] ?? $application['status'] ?>
</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">المراجع</span>
<span>
<?php if ($reviewer): ?>
<?= View::e($reviewer['display_name'] ?? $reviewer['username'] ?? 'غير معروف') ?>
<?php else: ?>
غير معروف
<?php endif; ?>
</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">تاريخ المراجعة</span>
<span class="tabular-nums"><?= $application['reviewed_at'] ? date('Y-m-d H:i', strtotime($application['reviewed_at'])) : '-' ?></span>
</div>
<?php if (!empty($application['review_note'])): ?>
<div class="mt-3">
<span class="text-secondary">ملاحظة المراجع:</span>
<p class="mt-2 p-3" style="background: var(--bg-secondary); border-radius: var(--radius-md);">
<?= nl2br(View::e($application['review_note'])) ?>
</p>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?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>
<?php
class OrgChatController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function channels(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$channels = $this->db->select('org_chat_channels', [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'is_default.desc,name.asc',
]);
foreach ($channels as &$ch) {
$ch['message_count'] = $this->db->count('org_chat_messages', [
'channel_id' => "eq.{$ch['id']}",
'is_deleted' => 'eq.false',
]);
}
$pageTitle = "قنوات الدردشة - {$org['name']}";
$moduleCSS = 'org-chat';
$moduleJS = 'org-chat';
View::render('org-chat/channels', compact('org', 'channels', 'pageTitle', 'moduleCSS', 'moduleJS'));
}
public function createChannel(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$channel = [];
$pageTitle = 'إنشاء قناة جديدة';
$moduleCSS = 'org-chat';
View::render('org-chat/channel-form', compact('org', 'channel', 'pageTitle', 'moduleCSS'));
}
public function storeChannel(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
Response::error('اسم القناة مطلوب', "/organizations/{$orgId}/chat/channels/create");
return;
}
$data = [
'org_id' => $orgId,
'name' => $name,
'name_ar' => trim($_POST['name_ar'] ?? ''),
'type' => $_POST['type'] ?? 'general',
'description' => trim($_POST['description'] ?? ''),
'is_default' => isset($_POST['is_default']),
'is_active' => true,
'settings' => json_encode([
'slow_mode' => (int)($_POST['slow_mode'] ?? 0),
'members_can_post' => isset($_POST['members_can_post']),
]),
];
$this->db->insert('org_chat_channels', $data);
AuditLog::log('create', 'org_chat_channel', null, null, $data);
Response::success('تم إنشاء القناة', "/organizations/{$orgId}/chat");
}
public function messages(array $params, string $method): void
{
$orgId = $params['id'];
$channelId = $params['channelId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
$channel = $this->db->selectOne('org_chat_channels', ['id' => "eq.{$channelId}", 'org_id' => "eq.{$orgId}"]);
if (!$org || !$channel) {
Response::error('القناة غير موجودة', '/organizations');
return;
}
$page = (int)($_GET['page'] ?? 1);
$perPage = 50;
$offset = ($page - 1) * $perPage;
$total = $this->db->count('org_chat_messages', [
'channel_id' => "eq.{$channelId}",
]);
$messages = $this->db->select('org_chat_messages', [
'select' => '*',
'channel_id' => "eq.{$channelId}",
'order' => 'created_at.desc',
'limit' => $perPage,
'offset' => $offset,
]);
$senderIds = array_unique(array_column($messages, 'sender_id'));
$senders = [];
if (!empty($senderIds)) {
$idList = '(' . implode(',', $senderIds) . ')';
$profiles = $this->db->select('profiles', [
'select' => 'id,username,display_name,avatar_url',
'id' => "in.{$idList}",
]);
foreach ($profiles as $p) {
$senders[$p['id']] = $p;
}
}
$totalPages = ceil($total / $perPage);
$pageTitle = "رسائل - {$channel['name']}";
$moduleCSS = 'org-chat';
$moduleJS = 'org-chat';
View::render('org-chat/messages', compact(
'org', 'channel', 'messages', 'senders', 'page', 'totalPages', 'total',
'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function updateChannel(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$channelId = $params['channelId'];
$data = [
'name' => trim($_POST['name'] ?? ''),
'name_ar' => trim($_POST['name_ar'] ?? ''),
'type' => $_POST['type'] ?? 'general',
'description' => trim($_POST['description'] ?? ''),
'is_default' => isset($_POST['is_default']),
'is_active' => isset($_POST['is_active']),
'settings' => json_encode([
'slow_mode' => (int)($_POST['slow_mode'] ?? 0),
'members_can_post' => isset($_POST['members_can_post']),
]),
'updated_at' => date('c'),
];
$this->db->update('org_chat_channels', ['id' => "eq.{$channelId}", 'org_id' => "eq.{$orgId}"], $data);
AuditLog::log('update', 'org_chat_channel', $channelId, null, $data);
Response::success('تم تحديث القناة', "/organizations/{$orgId}/chat");
}
public function deleteChannel(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$channelId = $params['channelId'];
$this->db->delete('org_chat_channels', ['id' => "eq.{$channelId}", 'org_id' => "eq.{$orgId}"]);
AuditLog::log('delete', 'org_chat_channel', $channelId);
Response::success('تم حذف القناة', "/organizations/{$orgId}/chat");
}
public function deleteMessage(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$messageId = $params['messageId'];
$this->db->update('org_chat_messages', ['id' => "eq.{$messageId}", 'org_id' => "eq.{$orgId}"], [
'is_deleted' => true,
'deleted_by' => Auth::user()['id'],
'deleted_at' => date('c'),
]);
$message = $this->db->selectOne('org_chat_messages', ['id' => "eq.{$messageId}"]);
if ($message) {
$this->db->insert('org_chat_moderation', [
'org_id' => $orgId,
'message_id' => $messageId,
'target_player_id' => $message['sender_id'],
'action' => 'delete_message',
'reason' => trim($_POST['reason'] ?? ''),
'moderator_id' => Auth::user()['id'],
]);
}
AuditLog::log('delete_message', 'org_chat_message', $messageId);
$redirect = $_POST['redirect'] ?? "/organizations/{$orgId}/chat";
Response::success('تم حذف الرسالة', $redirect);
}
public function pinMessage(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$messageId = $params['messageId'];
$message = $this->db->selectOne('org_chat_messages', ['id' => "eq.{$messageId}", 'org_id' => "eq.{$orgId}"]);
if (!$message) {
Response::error('الرسالة غير موجودة', "/organizations/{$orgId}/chat");
return;
}
$newPinned = !($message['is_pinned'] ?? false);
$this->db->update('org_chat_messages', ['id' => "eq.{$messageId}"], ['is_pinned' => $newPinned]);
$msg = $newPinned ? 'تم تثبيت الرسالة' : 'تم إلغاء تثبيت الرسالة';
$redirect = $_POST['redirect'] ?? "/organizations/{$orgId}/chat";
Response::success($msg, $redirect);
}
public function moderation(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$queryParams = [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'created_at.desc',
];
$total = $this->db->count('org_chat_moderation', ['org_id' => "eq.{$orgId}"]);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$logs = $this->db->select('org_chat_moderation', $queryParams);
$playerIds = array_unique(array_merge(
array_column($logs, 'target_player_id'),
array_column($logs, 'moderator_id')
));
$players = [];
if (!empty($playerIds)) {
$idList = '(' . implode(',', $playerIds) . ')';
$profiles = $this->db->select('profiles', [
'select' => 'id,username,display_name',
'id' => "in.{$idList}",
]);
foreach ($profiles as $p) {
$players[$p['id']] = $p;
}
}
$mutedMembers = $this->db->select('org_members', [
'select' => 'user_id,display_name,is_muted,muted_until',
'org_id' => "eq.{$orgId}",
'is_muted' => 'eq.true',
]);
$pageTitle = "إدارة الدردشة - {$org['name']}";
$moduleCSS = 'org-chat';
View::render('org-chat/moderation', compact('org', 'logs', 'players', 'mutedMembers', 'pagination', 'pageTitle', 'moduleCSS'));
}
public function mutePlayer(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$playerId = $_POST['player_id'] ?? '';
$durationMinutes = (int)($_POST['duration_minutes'] ?? 60);
$reason = trim($_POST['reason'] ?? '');
if (empty($playerId)) {
Response::error('يرجى تحديد اللاعب', "/organizations/{$orgId}/chat/moderation");
return;
}
$expiresAt = date('c', time() + ($durationMinutes * 60));
$this->db->update('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
], [
'is_muted' => true,
'muted_until' => $expiresAt,
]);
$this->db->insert('org_chat_moderation', [
'org_id' => $orgId,
'target_player_id' => $playerId,
'action' => 'mute',
'reason' => $reason,
'moderator_id' => Auth::user()['id'],
'duration_minutes' => $durationMinutes,
'expires_at' => $expiresAt,
]);
AuditLog::log('mute_player', 'org_chat', $playerId, null, ['org_id' => $orgId, 'duration' => $durationMinutes]);
Response::success('تم كتم اللاعب', "/organizations/{$orgId}/chat/moderation");
}
public function unmutePlayer(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$playerId = $_POST['player_id'] ?? '';
$this->db->update('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
], [
'is_muted' => false,
'muted_until' => null,
]);
$this->db->insert('org_chat_moderation', [
'org_id' => $orgId,
'target_player_id' => $playerId,
'action' => 'unmute',
'moderator_id' => Auth::user()['id'],
]);
AuditLog::log('unmute_player', 'org_chat', $playerId, null, ['org_id' => $orgId]);
Response::success('تم إلغاء كتم اللاعب', "/organizations/{$orgId}/chat/moderation");
}
public function banFromChat(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$playerId = $_POST['player_id'] ?? '';
$reason = trim($_POST['reason'] ?? '');
if (empty($playerId)) {
Response::error('يرجى تحديد اللاعب', "/organizations/{$orgId}/chat/moderation");
return;
}
$this->db->update('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
], [
'is_muted' => true,
'muted_until' => '2099-12-31T23:59:59Z',
]);
$this->db->insert('org_chat_moderation', [
'org_id' => $orgId,
'target_player_id' => $playerId,
'action' => 'ban_from_chat',
'reason' => $reason,
'moderator_id' => Auth::user()['id'],
]);
AuditLog::log('ban_from_chat', 'org_chat', $playerId, null, ['org_id' => $orgId, 'reason' => $reason]);
Response::success('تم حظر اللاعب من الدردشة', "/organizations/{$orgId}/chat/moderation");
}
public function apiMessages(array $params, string $method): void
{
$orgId = $params['orgId'];
$channelId = $_GET['channel_id'] ?? '';
$after = $_GET['after'] ?? '';
$limit = min((int)($_GET['limit'] ?? 50), 100);
$queryParams = [
'select' => '*',
'org_id' => "eq.{$orgId}",
'is_deleted' => 'eq.false',
'order' => 'created_at.desc',
'limit' => $limit,
];
if ($channelId) {
$queryParams['channel_id'] = "eq.{$channelId}";
}
if ($after) {
$queryParams['created_at'] = "gt.{$after}";
}
$messages = $this->db->select('org_chat_messages', $queryParams);
header('Content-Type: application/json');
echo json_encode(['messages' => $messages]);
}
}
<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));
}
<?php
class OrgContentController
{
private Database $db;
public function __construct()
{
$this->db = Database::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' => 'created_at.desc',
];
if (!empty($_GET['category'])) {
$filters['category'] = "eq.{$_GET['category']}";
}
if (!empty($_GET['visibility'])) {
$filters['visibility'] = "eq.{$_GET['visibility']}";
}
$total = $this->db->count('org_content', $filters);
$pagination = Pagination::fromRequest($total);
$filters['offset'] = $pagination->offset;
$filters['limit'] = $pagination->perPage;
$content = $this->db->select('org_content', $filters);
$pageTitle = 'المحتوى - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-content/list', compact('org', 'content', '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-content/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();
$tags = [];
if (!empty($_POST['tags'])) {
$tags = array_map('trim', explode(',', $_POST['tags']));
}
$data = [
'org_id' => $orgId,
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'body' => trim($_POST['body'] ?? ''),
'body_ar' => trim($_POST['body_ar'] ?? ''),
'category' => $_POST['category'] ?? null,
'game_key' => $_POST['game_key'] ?? null,
'author_id' => $user['id'],
'is_published' => isset($_POST['is_published']) ? true : false,
'is_pinned' => isset($_POST['is_pinned']) ? true : false,
'visibility' => $_POST['visibility'] ?? 'members',
'tags' => json_encode($tags),
];
$result = $this->db->insert('org_content', $data);
AuditLog::log('create', 'org_content', $result['id'] ?? null, null, $data);
Response::success('تم إضافة المحتوى بنجاح', "/organizations/{$orgId}/content");
}
public function edit(array $params, string $method): void
{
$orgId = $params['id'];
$contentId = $params['contentId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$content = $this->db->selectOne('org_content', [
'id' => "eq.{$contentId}",
'org_id' => "eq.{$orgId}",
]);
if (!$content) {
Response::error('المحتوى غير موجود', "/organizations/{$orgId}/content");
return;
}
$pageTitle = 'تعديل المحتوى - ' . ($content['title_ar'] ?? $content['title']);
View::render('org-content/form', compact('org', 'content', 'pageTitle'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$contentId = $params['contentId'];
$old = $this->db->selectOne('org_content', [
'id' => "eq.{$contentId}",
'org_id' => "eq.{$orgId}",
]);
if (!$old) {
Response::error('المحتوى غير موجود', "/organizations/{$orgId}/content");
return;
}
$tags = [];
if (!empty($_POST['tags'])) {
$tags = array_map('trim', explode(',', $_POST['tags']));
}
$data = [
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'body' => trim($_POST['body'] ?? ''),
'body_ar' => trim($_POST['body_ar'] ?? ''),
'category' => $_POST['category'] ?? null,
'game_key' => $_POST['game_key'] ?? null,
'is_published' => isset($_POST['is_published']) ? true : false,
'is_pinned' => isset($_POST['is_pinned']) ? true : false,
'visibility' => $_POST['visibility'] ?? 'members',
'tags' => json_encode($tags),
];
$this->db->update('org_content', ['id' => "eq.{$contentId}"], $data);
AuditLog::log('update', 'org_content', $contentId, $old, $data);
Response::success('تم تحديث المحتوى بنجاح', "/organizations/{$orgId}/content");
}
public function togglePublish(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$contentId = $params['contentId'];
$content = $this->db->selectOne('org_content', [
'id' => "eq.{$contentId}",
'org_id' => "eq.{$orgId}",
]);
if (!$content) {
Response::error('المحتوى غير موجود', "/organizations/{$orgId}/content");
return;
}
$newStatus = !($content['is_published'] ?? false);
$this->db->update('org_content', ['id' => "eq.{$contentId}"], [
'is_published' => $newStatus,
]);
AuditLog::log('toggle_publish', 'org_content', $contentId, $content, ['is_published' => $newStatus]);
Response::success(
$newStatus ? 'تم نشر المحتوى' : 'تم إلغاء نشر المحتوى',
"/organizations/{$orgId}/content"
);
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$contentId = $params['contentId'];
$content = $this->db->selectOne('org_content', [
'id' => "eq.{$contentId}",
'org_id' => "eq.{$orgId}",
]);
if (!$content) {
Response::error('المحتوى غير موجود', "/organizations/{$orgId}/content");
return;
}
$this->db->delete('org_content', ['id' => "eq.{$contentId}"]);
AuditLog::log('delete', 'org_content', $contentId, $content, null);
Response::success('تم حذف المحتوى', "/organizations/{$orgId}/content");
}
}
<?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>
<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'] ?>/content/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-5">
<form method="GET" action="/organizations/<?= $org['id'] ?>/content" 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-input">
<option value="">الكل</option>
<option value="guide" <?= ($_GET['category'] ?? '') === 'guide' ? 'selected' : '' ?>>دليل</option>
<option value="strategy" <?= ($_GET['category'] ?? '') === 'strategy' ? 'selected' : '' ?>>استراتيجية</option>
<option value="tutorial" <?= ($_GET['category'] ?? '') === 'tutorial' ? 'selected' : '' ?>>شرح</option>
<option value="announcement" <?= ($_GET['category'] ?? '') === 'announcement' ? 'selected' : '' ?>>إعلان</option>
<option value="resource" <?= ($_GET['category'] ?? '') === 'resource' ? 'selected' : '' ?>>مورد</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">الظهور</label>
<select name="visibility" class="form-input">
<option value="">الكل</option>
<option value="members" <?= ($_GET['visibility'] ?? '') === 'members' ? 'selected' : '' ?>>الأعضاء</option>
<option value="public" <?= ($_GET['visibility'] ?? '') === 'public' ? 'selected' : '' ?>>عام</option>
<option value="admin_only" <?= ($_GET['visibility'] ?? '') === 'admin_only' ? 'selected' : '' ?>>المديرين فقط</option>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
</form>
</div>
<!-- Content Table -->
<div class="data-table-wrapper">
<?php if (empty($content)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<h3 class="empty-state-title">لا يوجد محتوى</h3>
<p class="empty-state-text">لم يتم إضافة أي محتوى لهذه المنظمة بعد</p>
</div>
<?php else: ?>
<?php
$categoryLabels = ['guide' => 'دليل', 'strategy' => 'استراتيجية', 'tutorial' => 'شرح', 'announcement' => 'إعلان', 'resource' => 'مورد'];
$categoryBadges = ['guide' => 'badge-info', 'strategy' => 'badge-warning', 'tutorial' => 'badge-success', 'announcement' => 'badge-purple', 'resource' => 'badge-default'];
$visibilityLabels = ['members' => 'الأعضاء', 'public' => 'عام', 'admin_only' => 'المديرين فقط'];
?>
<table class="data-table">
<thead>
<tr>
<th>العنوان</th>
<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 ($content as $item): ?>
<?php $cat = $item['category'] ?? 'guide'; ?>
<tr>
<td>
<strong><?= View::e($item['title_ar'] ?: $item['title']) ?></strong>
</td>
<td>
<span class="badge <?= $categoryBadges[$cat] ?? 'badge-default' ?>"><?= $categoryLabels[$cat] ?? $cat ?></span>
</td>
<td><?= View::e($item['game_key'] ?? '-') ?></td>
<td class="text-xs"><?= $item['author_id'] ? substr($item['author_id'], 0, 8) . '...' : '-' ?></td>
<td>
<span class="badge badge-default"><?= $visibilityLabels[$item['visibility'] ?? 'members'] ?? $item['visibility'] ?></span>
</td>
<td>
<?php if ($item['is_published'] ?? false): ?>
<span class="badge badge-success badge-dot">نعم</span>
<?php else: ?>
<span class="badge badge-default badge-dot">لا</span>
<?php endif; ?>
</td>
<td>
<?php if ($item['is_pinned'] ?? false): ?>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="color: var(--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 class="tabular-nums"><?= number_format($item['view_count'] ?? 0) ?></td>
<td class="text-xs tabular-nums"><?= date('Y-m-d', strtotime($item['created_at'])) ?></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'] ?>/content/<?= $item['id'] ?>/edit" class="dropdown-item">تعديل</a>
<form method="POST" action="/organizations/<?= $org['id'] ?>/content/<?= $item['id'] ?>/toggle-publish" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="dropdown-item">
<?= ($item['is_published'] ?? false) ? 'إلغاء النشر' : 'نشر' ?>
</button>
</form>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger" onclick="confirmDelete('/organizations/<?= $org['id'] ?>/content/<?= $item['id'] ?>/delete', '<?= View::e($item['title_ar'] ?: $item['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 ?>&category=<?= urlencode($_GET['category'] ?? '') ?>&visibility=<?= urlencode($_GET['visibility'] ?? '') ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
.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;
}
<?php
class OrgEventsController
{
private Database $db;
public function __construct()
{
$this->db = Database::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;
}
$queryParams = [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'starts_at.desc',
];
if (!empty($_GET['event_type'])) {
$queryParams['event_type'] = 'eq.' . $_GET['event_type'];
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_events', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$events = $this->db->select('org_events', $queryParams);
$pageTitle = 'الفعاليات - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-events/list', compact('org', 'events', '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;
}
$event = [];
$pageTitle = 'إنشاء فعالية - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-events/form', compact('org', 'event', '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;
}
$title = trim($_POST['title'] ?? '');
$startsAt = trim($_POST['starts_at'] ?? '');
if (empty($title) || empty($startsAt)) {
Response::error('العنوان وتاريخ البداية مطلوبان', "/organizations/{$orgId}/events/create");
return;
}
$user = Auth::user();
$data = [
'org_id' => $orgId,
'title' => $title,
'title_ar' => trim($_POST['title_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'event_type' => $_POST['event_type'] ?? 'general',
'starts_at' => $startsAt,
'ends_at' => !empty($_POST['ends_at']) ? $_POST['ends_at'] : null,
'location' => trim($_POST['location'] ?? ''),
'is_online' => isset($_POST['is_online']),
'link' => trim($_POST['link'] ?? ''),
'max_participants' => !empty($_POST['max_participants']) ? (int) $_POST['max_participants'] : null,
'is_public' => isset($_POST['is_public']),
'is_recurring' => isset($_POST['is_recurring']),
'recurrence_rule' => !empty($_POST['recurrence_rule']) ? json_encode($_POST['recurrence_rule']) : null,
'tournament_id' => !empty($_POST['tournament_id']) ? $_POST['tournament_id'] : null,
'created_by' => $user['id'],
];
$result = $this->db->insert('org_events', $data);
AuditLog::log('create', 'org_event', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء الفعالية بنجاح', "/organizations/{$orgId}/events");
}
public function edit(array $params, string $method): void
{
$orgId = $params['id'];
$eventId = $params['eventId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$event = $this->db->selectOne('org_events', [
'id' => "eq.{$eventId}",
'org_id' => "eq.{$orgId}",
]);
if (!$event) {
Response::error('الفعالية غير موجودة', "/organizations/{$orgId}/events");
return;
}
$pageTitle = 'تعديل الفعالية - ' . ($event['title_ar'] ?? $event['title']);
View::render('org-events/form', compact('org', 'event', 'pageTitle'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$eventId = $params['eventId'];
$event = $this->db->selectOne('org_events', [
'id' => "eq.{$eventId}",
'org_id' => "eq.{$orgId}",
]);
if (!$event) {
Response::error('الفعالية غير موجودة', "/organizations/{$orgId}/events");
return;
}
$data = [
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'event_type' => $_POST['event_type'] ?? 'general',
'starts_at' => $_POST['starts_at'] ?? $event['starts_at'],
'ends_at' => !empty($_POST['ends_at']) ? $_POST['ends_at'] : null,
'location' => trim($_POST['location'] ?? ''),
'is_online' => isset($_POST['is_online']),
'link' => trim($_POST['link'] ?? ''),
'max_participants' => !empty($_POST['max_participants']) ? (int) $_POST['max_participants'] : null,
'is_public' => isset($_POST['is_public']),
'is_recurring' => isset($_POST['is_recurring']),
'recurrence_rule' => !empty($_POST['recurrence_rule']) ? json_encode($_POST['recurrence_rule']) : null,
'tournament_id' => !empty($_POST['tournament_id']) ? $_POST['tournament_id'] : null,
'updated_at' => date('c'),
];
$this->db->update('org_events', ['id' => "eq.{$eventId}"], $data);
AuditLog::log('update', 'org_event', $eventId, $event, $data);
Response::success('تم تحديث الفعالية', "/organizations/{$orgId}/events");
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$eventId = $params['eventId'];
$event = $this->db->selectOne('org_events', [
'id' => "eq.{$eventId}",
'org_id' => "eq.{$orgId}",
]);
if (!$event) {
Response::error('الفعالية غير موجودة', "/organizations/{$orgId}/events");
return;
}
// Delete participants first
$this->db->delete('org_event_participants', ['event_id' => "eq.{$eventId}"]);
$this->db->delete('org_events', ['id' => "eq.{$eventId}"]);
AuditLog::log('delete', 'org_event', $eventId, $event, null);
Response::success('تم حذف الفعالية', "/organizations/{$orgId}/events");
}
public function calendar(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;
}
$month = $_GET['month'] ?? date('m');
$year = $_GET['year'] ?? date('Y');
$startDate = "{$year}-{$month}-01T00:00:00";
$endDate = date('Y-m-t', strtotime($startDate)) . 'T23:59:59';
$events = $this->db->select('org_events', [
'select' => '*',
'org_id' => "eq.{$orgId}",
'starts_at' => "gte.{$startDate}",
'starts_at' => "lte.{$endDate}",
'order' => 'starts_at.asc',
]);
$pageTitle = 'تقويم الفعاليات - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-events/calendar', compact('org', 'events', 'month', 'year', 'pageTitle'));
}
public function apiCalendar(array $params, string $method): void
{
$orgId = $params['id'];
$start = $_GET['start'] ?? date('Y-m-01T00:00:00');
$end = $_GET['end'] ?? date('Y-m-t') . 'T23:59:59';
$events = $this->db->select('org_events', [
'select' => 'id,title,title_ar,starts_at,ends_at,event_type,is_online,location',
'org_id' => "eq.{$orgId}",
'starts_at' => "gte.{$start}",
'starts_at' => "lte.{$end}",
'order' => 'starts_at.asc',
]);
header('Content-Type: application/json');
echo json_encode($events);
}
}
<?php
$month = (int)$month;
$year = (int)$year;
$firstDayOfMonth = mktime(0, 0, 0, $month, 1, $year);
$daysInMonth = (int)date('t', $firstDayOfMonth);
$startDayOfWeek = (int)date('w', $firstDayOfMonth); // 0=Sunday
// Previous/Next month navigation
$prevMonth = $month - 1;
$prevYear = $year;
if ($prevMonth < 1) { $prevMonth = 12; $prevYear--; }
$nextMonth = $month + 1;
$nextYear = $year;
if ($nextMonth > 12) { $nextMonth = 1; $nextYear++; }
$monthNames = [
1 => 'يناير', 2 => 'فبراير', 3 => 'مارس', 4 => 'أبريل',
5 => 'مايو', 6 => 'يونيو', 7 => 'يوليو', 8 => 'أغسطس',
9 => 'سبتمبر', 10 => 'أكتوبر', 11 => 'نوفمبر', 12 => 'ديسمبر',
];
// Group events by day
$eventsByDay = [];
foreach ($events as $event) {
$day = (int)date('j', strtotime($event['starts_at']));
$eventsByDay[$day][] = $event;
}
$eventTypeBadges = [
'general' => 'badge-default',
'tournament' => 'badge-purple',
'training' => 'badge-info',
'meeting' => 'badge-warning',
'social' => 'badge-success',
'external' => 'badge-danger',
];
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/events" 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'] ?>/events" class="btn btn-ghost">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
عرض القائمة
</a>
</div>
<div class="card">
<div class="flex items-center justify-between mb-5">
<a href="/organizations/<?= $org['id'] ?>/events/calendar?month=<?= $prevMonth ?>&year=<?= $prevYear ?>" class="btn btn-ghost btn-sm">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
الشهر السابق
</a>
<h2 class="text-lg fw-600"><?= $monthNames[$month] ?> <?= $year ?></h2>
<a href="/organizations/<?= $org['id'] ?>/events/calendar?month=<?= $nextMonth ?>&year=<?= $nextYear ?>" class="btn btn-ghost btn-sm">
الشهر التالي
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</div>
<div class="table-responsive">
<table class="table calendar-table" style="table-layout: fixed;">
<thead>
<tr>
<th>الأحد</th>
<th>الاثنين</th>
<th>الثلاثاء</th>
<th>الأربعاء</th>
<th>الخميس</th>
<th>الجمعة</th>
<th>السبت</th>
</tr>
</thead>
<tbody>
<?php
$currentDay = 1;
$today = (int)date('j');
$currentMonth = (int)date('m');
$currentYear = (int)date('Y');
$totalCells = $startDayOfWeek + $daysInMonth;
$totalRows = ceil($totalCells / 7);
for ($row = 0; $row < $totalRows; $row++):
?>
<tr>
<?php for ($col = 0; $col < 7; $col++):
$cellIndex = $row * 7 + $col;
if ($cellIndex < $startDayOfWeek || $currentDay > $daysInMonth):
?>
<td class="calendar-cell calendar-cell-empty"></td>
<?php else:
$isToday = ($currentDay === $today && $month === $currentMonth && $year === $currentYear);
$dayEvents = $eventsByDay[$currentDay] ?? [];
?>
<td class="calendar-cell <?= $isToday ? 'calendar-cell-today' : '' ?>">
<div class="calendar-day-number <?= $isToday ? 'fw-700' : '' ?>"><?= $currentDay ?></div>
<?php if (!empty($dayEvents)): ?>
<div class="calendar-events">
<?php foreach (array_slice($dayEvents, 0, 3) as $dayEvent): ?>
<a href="/organizations/<?= $org['id'] ?>/events/<?= $dayEvent['id'] ?>/edit" class="calendar-event-item badge <?= $eventTypeBadges[$dayEvent['event_type']] ?? 'badge-default' ?>" title="<?= View::e($dayEvent['title_ar'] ?? $dayEvent['title']) ?>">
<?= View::e(mb_substr($dayEvent['title_ar'] ?? $dayEvent['title'], 0, 12)) ?>
</a>
<?php endforeach; ?>
<?php if (count($dayEvents) > 3): ?>
<span class="text-xs text-muted">+<?= count($dayEvents) - 3 ?> المزيد</span>
<?php endif; ?>
</div>
<?php endif; ?>
</td>
<?php
$currentDay++;
endif;
endfor; ?>
</tr>
<?php endfor; ?>
</tbody>
</table>
</div>
</div>
<style>
.calendar-table td {
vertical-align: top;
height: 100px;
padding: 8px;
border: 1px solid var(--border-color, #e5e7eb);
}
.calendar-cell-empty {
background: var(--bg-secondary, #f9fafb);
}
.calendar-cell-today {
background: var(--bg-primary-subtle, #eff6ff);
}
.calendar-day-number {
font-size: 0.875rem;
margin-bottom: 4px;
color: var(--text-secondary);
}
.calendar-cell-today .calendar-day-number {
color: var(--color-primary, #3b82f6);
}
.calendar-events {
display: flex;
flex-direction: column;
gap: 2px;
}
.calendar-event-item {
display: block;
font-size: 0.7rem;
padding: 1px 4px;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
cursor: pointer;
}
</style>
<?php
$isEdit = !empty($event);
$formAction = $isEdit
? "/organizations/{$org['id']}/events/{$event['id']}/update"
: "/organizations/{$org['id']}/events/store";
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/events" 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">المنظمة</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($event['title'] ?? '') ?>" required>
</div>
<div class="form-group">
<label class="form-label">العنوان (عربي)</label>
<input type="text" name="title_ar" class="form-input" value="<?= View::e($event['title_ar'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">الوصف (English)</label>
<textarea name="description" class="form-input" rows="3" dir="ltr"><?= View::e($event['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">الوصف (عربي)</label>
<textarea name="description_ar" class="form-input" rows="3"><?= View::e($event['description_ar'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">نوع الفعالية</label>
<select name="event_type" class="form-input">
<?php
$eventTypes = [
'general' => 'عام',
'tournament' => 'بطولة',
'training' => 'تدريب',
'meeting' => 'اجتماع',
'social' => 'اجتماعي',
'external' => 'خارجي',
];
foreach ($eventTypes as $val => $label):
?>
<option value="<?= $val ?>" <?= ($event['event_type'] ?? 'general') === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">تاريخ البداية <span class="text-danger">*</span></label>
<input type="datetime-local" name="starts_at" class="form-input" dir="ltr" value="<?= View::e(!empty($event['starts_at']) ? date('Y-m-d\TH:i', strtotime($event['starts_at'])) : '') ?>" required>
</div>
<div class="form-group">
<label class="form-label">تاريخ النهاية</label>
<input type="datetime-local" name="ends_at" class="form-input" dir="ltr" value="<?= View::e(!empty($event['ends_at']) ? date('Y-m-d\TH:i', strtotime($event['ends_at'])) : '') ?>">
</div>
</div>
<div class="form-group">
<label class="form-label">الموقع</label>
<input type="text" name="location" class="form-input" value="<?= View::e($event['location'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="is_online" value="1" <?= ($event['is_online'] ?? false) ? 'checked' : '' ?>>
<span class="checkbox-mark"></span>
<span class="checkbox-label">فعالية أونلاين</span>
</label>
</div>
<div class="form-group">
<label class="form-label">رابط الفعالية</label>
<input type="url" name="link" class="form-input" dir="ltr" value="<?= View::e($event['link'] ?? '') ?>" placeholder="https://...">
<span class="form-hint">رابط الانضمام للفعالية الأونلاين</span>
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للمشاركين</label>
<input type="number" name="max_participants" class="form-input" min="1" value="<?= View::e($event['max_participants'] ?? '') ?>" placeholder="اتركه فارغاً لعدد غير محدود">
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="is_public" value="1" <?= ($event['is_public'] ?? 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-checkbox">
<input type="checkbox" name="is_recurring" value="1" <?= ($event['is_recurring'] ?? false) ? 'checked' : '' ?>>
<span class="checkbox-mark"></span>
<span class="checkbox-label">فعالية متكررة</span>
</label>
</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'] ?>/events" 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>
<div class="flex gap-3">
<a href="/organizations/<?= $org['id'] ?>/events/calendar" class="btn btn-ghost">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
التقويم
</a>
<a href="/organizations/<?= $org['id'] ?>/events/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>
<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="event_type" class="form-input" onchange="this.form.submit()">
<option value="">الكل</option>
<?php
$eventTypes = [
'general' => 'عام',
'tournament' => 'بطولة',
'training' => 'تدريب',
'meeting' => 'اجتماع',
'social' => 'اجتماعي',
'external' => 'خارجي',
];
foreach ($eventTypes as $val => $label):
?>
<option value="<?= $val ?>" <?= ($_GET['event_type'] ?? '') === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
<div class="data-table-wrapper">
<?php if (empty($events)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<h3 class="empty-state-title">لا توجد فعاليات</h3>
<p class="empty-state-text">لم يتم إنشاء أي فعاليات لهذه المنظمة بعد</p>
</div>
<?php else: ?>
<?php
$eventTypeBadges = [
'general' => 'badge-default',
'tournament' => 'badge-purple',
'training' => 'badge-info',
'meeting' => 'badge-warning',
'social' => 'badge-success',
'external' => 'badge-danger',
];
$eventTypeLabels = [
'general' => 'عام',
'tournament' => 'بطولة',
'training' => 'تدريب',
'meeting' => 'اجتماع',
'social' => 'اجتماعي',
'external' => 'خارجي',
];
$statusBadges = [
'scheduled' => 'badge-info',
'active' => 'badge-success',
'cancelled' => 'badge-danger',
'completed' => 'badge-default',
];
$statusLabels = [
'scheduled' => 'مجدول',
'active' => 'نشط',
'cancelled' => 'ملغي',
'completed' => 'مكتمل',
];
?>
<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 ($events as $event): ?>
<tr>
<td>
<span class="fw-500"><?= View::e($event['title_ar'] ?? $event['title']) ?></span>
</td>
<td><?= View::e($org['name_ar'] ?? $org['name']) ?></td>
<td>
<span class="badge <?= $eventTypeBadges[$event['event_type']] ?? 'badge-default' ?>">
<?= $eventTypeLabels[$event['event_type']] ?? $event['event_type'] ?>
</span>
</td>
<td class="tabular-nums"><?= date('Y-m-d H:i', strtotime($event['starts_at'])) ?></td>
<td class="tabular-nums">
<?= !empty($event['ends_at']) ? date('Y-m-d H:i', strtotime($event['ends_at'])) : '-' ?>
</td>
<td><?= (int)($event['participants_count'] ?? 0) ?></td>
<td>
<?php if ($event['is_public'] ?? false): ?>
<span class="badge badge-success badge-dot">عام</span>
<?php else: ?>
<span class="badge badge-default badge-dot">خاص</span>
<?php endif; ?>
</td>
<td>
<span class="badge <?= $statusBadges[$event['status'] ?? 'scheduled'] ?? 'badge-default' ?> badge-dot">
<?= $statusLabels[$event['status'] ?? 'scheduled'] ?? ($event['status'] ?? 'مجدول') ?>
</span>
</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'] ?>/events/<?= $event['id'] ?>/edit" class="dropdown-item">تعديل</a>
<form method="POST" action="/organizations/<?= $org['id'] ?>/events/<?= $event['id'] ?>/cancel" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="dropdown-item">إلغاء الفعالية</button>
</form>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger" onclick="confirmDelete('/organizations/<?= $org['id'] ?>/events/<?= $event['id'] ?>/delete', '<?= View::e($event['title_ar'] ?? $event['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 ?>&event_type=<?= $_GET['event_type'] ?? '' ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
.frames-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--space-4);
margin-bottom: var(--space-6);
}
.frame-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.frame-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.frame-card-image {
width: 100%;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
padding: var(--space-3);
}
.frame-card-image img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.frame-card-body {
padding: var(--space-3);
}
.frame-card-name {
font-weight: 600;
margin-bottom: var(--space-1);
font-size: 0.9rem;
}
.frame-card-name-ar {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: var(--space-2);
}
.frame-card-badges {
display: flex;
gap: var(--space-1);
flex-wrap: wrap;
margin-bottom: var(--space-2);
}
.frame-card-price {
display: flex;
gap: var(--space-3);
font-size: 0.85rem;
margin-bottom: var(--space-2);
}
.frame-card-price .coins {
color: #f59e0b;
}
.frame-card-price .gems {
color: #8b5cf6;
}
.frame-card-supply {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: var(--space-2);
}
.frame-card-actions {
display: flex;
gap: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--border);
}
.frame-card-actions .btn {
flex: 1;
font-size: 0.75rem;
padding: var(--space-1) var(--space-2);
}
.frame-card.inactive {
opacity: 0.6;
}
.rarity-common { color: #9ca3af; }
.rarity-uncommon { color: #22c55e; }
.rarity-rare { color: #3b82f6; }
.rarity-epic { color: #a855f7; }
.rarity-legendary { color: #f59e0b; }
.badge-rarity {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: var(--radius-sm);
font-weight: 600;
text-transform: uppercase;
}
.badge-rarity.common { background: rgba(156, 163, 175, 0.15); color: #9ca3af; }
.badge-rarity.uncommon { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
.badge-rarity.rare { background: rgba(59, 130, 246, 0.15); color: #3b82f6; }
.badge-rarity.epic { background: rgba(168, 85, 247, 0.15); color: #a855f7; }
.badge-rarity.legendary { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
.assign-section {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-4);
margin-top: var(--space-6);
}
.assign-section h3 {
margin-bottom: var(--space-3);
font-size: 1rem;
}
.assign-form {
display: flex;
gap: var(--space-3);
align-items: flex-end;
flex-wrap: wrap;
}
.assign-form .form-group {
flex: 1;
min-width: 200px;
margin-bottom: 0;
}
.frame-preview {
width: 120px;
height: 120px;
border: 2px dashed var(--border);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-2);
overflow: hidden;
}
.frame-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
(function() {
'use strict';
function initImagePreview() {
const imageInput = document.getElementById('frameImage');
const preview = document.getElementById('framePreview');
if (!imageInput || !preview) return;
imageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
preview.innerHTML = '<img src="' + ev.target.result + '" alt="Preview">';
};
reader.readAsDataURL(file);
});
}
function initPlayerSearch() {
const input = document.getElementById('playerSearchInput');
const results = document.getElementById('playerSearchResults');
const hiddenInput = document.getElementById('playerIdInput');
if (!input || !results) return;
let timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
const q = this.value.trim();
if (q.length < 2) {
results.style.display = 'none';
return;
}
timeout = setTimeout(function() {
fetch('/api/players/search?q=' + encodeURIComponent(q))
.then(function(r) { return r.json(); })
.then(function(players) {
if (!players.length) {
results.style.display = 'none';
return;
}
results.innerHTML = players.map(function(p) {
return '<div class="search-result-item" data-id="' + p.id + '">' +
(p.display_name || p.username) + ' <small>@' + p.username + '</small></div>';
}).join('');
results.style.display = 'block';
})
.catch(function() { results.style.display = 'none'; });
}, 300);
});
results.addEventListener('click', function(e) {
var item = e.target.closest('.search-result-item');
if (!item) return;
hiddenInput.value = item.dataset.id;
input.value = item.textContent.trim();
results.style.display = 'none';
});
document.addEventListener('click', function(e) {
if (!results.contains(e.target) && e.target !== input) {
results.style.display = 'none';
}
});
}
document.addEventListener('DOMContentLoaded', function() {
initImagePreview();
initPlayerSearch();
});
})();
<?php
class OrgFramesController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$search = $_GET['search'] ?? '';
$category = $_GET['category'] ?? '';
$rarity = $_GET['rarity'] ?? '';
$orgId = $_GET['org_id'] ?? '';
$queryParams = ['select' => '*', 'order' => 'sort_order.asc,created_at.desc'];
if ($search) {
$queryParams['or'] = "(name.ilike.*{$search}*,name_ar.ilike.*{$search}*)";
}
if ($category) {
$queryParams['category'] = "eq.{$category}";
}
if ($rarity) {
$queryParams['rarity'] = "eq.{$rarity}";
}
if ($orgId) {
$queryParams['org_id'] = "eq.{$orgId}";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('profile_frames', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$frames = $this->db->select('profile_frames', $queryParams);
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'إطارات الملفات الشخصية';
$moduleCSS = 'org-frames';
$moduleJS = 'org-frames';
View::render('org-frames/list', compact(
'frames', 'pagination', 'search', 'category', 'rarity', 'orgId',
'organizations', 'pageTitle', 'moduleCSS', 'moduleJS'
));
}
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',
]);
$frame = [];
$pageTitle = 'إضافة إطار جديد';
$moduleCSS = 'org-frames';
View::render('org-frames/form', compact('frame', 'organizations', 'pageTitle', 'moduleCSS'));
}
public function store(array $params, string $method): void
{
Auth::requireCsrf();
$storage = SupabaseStorage::getInstance();
$allowedMimes = ['image/png', 'image/svg+xml', 'image/webp', 'image/gif'];
$maxSize = 5 * 1024 * 1024;
$error = $storage->validateFile($_FILES['image'] ?? [], $allowedMimes, $maxSize);
if ($error) {
Response::error($error, '/org-frames/create');
return;
}
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
Response::error('اسم الإطار مطلوب', '/org-frames/create');
return;
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['image']['tmp_name']);
$path = $storage->generatePath('frames', $_FILES['image']['name']);
$imageUrl = $storage->upload('profile-frames', $path, $_FILES['image']['tmp_name'], $mime);
$thumbnailUrl = $imageUrl;
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
$thumbPath = $storage->generatePath('frames/thumbs', $_FILES['thumbnail']['name']);
$thumbMime = $finfo->file($_FILES['thumbnail']['tmp_name']);
$thumbnailUrl = $storage->upload('profile-frames', $thumbPath, $_FILES['thumbnail']['tmp_name'], $thumbMime);
}
$data = [
'name' => $name,
'name_ar' => trim($_POST['name_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'image_url' => $imageUrl,
'thumbnail_url' => $thumbnailUrl,
'category' => $_POST['category'] ?? 'general',
'rarity' => $_POST['rarity'] ?? 'common',
'price_coins' => (int)($_POST['price_coins'] ?? 0),
'price_gems' => (int)($_POST['price_gems'] ?? 0),
'is_purchasable' => isset($_POST['is_purchasable']),
'is_active' => true,
'required_level' => (int)($_POST['required_level'] ?? 0),
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'max_supply' => !empty($_POST['max_supply']) ? (int)$_POST['max_supply'] : null,
'sort_order' => (int)($_POST['sort_order'] ?? 0),
];
if (!empty($_POST['available_from'])) {
$data['available_from'] = $_POST['available_from'];
}
if (!empty($_POST['available_until'])) {
$data['available_until'] = $_POST['available_until'];
}
$result = $this->db->insert('profile_frames', $data);
AuditLog::log('create', 'profile_frame', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء الإطار بنجاح', '/org-frames');
}
public function edit(array $params, string $method): void
{
$id = $params['id'];
$frame = $this->db->selectOne('profile_frames', ['id' => "eq.{$id}"]);
if (!$frame) {
Response::error('الإطار غير موجود', '/org-frames');
return;
}
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'تعديل الإطار';
$moduleCSS = 'org-frames';
View::render('org-frames/form', compact('frame', 'organizations', 'pageTitle', 'moduleCSS'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$frame = $this->db->selectOne('profile_frames', ['id' => "eq.{$id}"]);
if (!$frame) {
Response::error('الإطار غير موجود', '/org-frames');
return;
}
$data = [
'name' => trim($_POST['name'] ?? $frame['name']),
'name_ar' => trim($_POST['name_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'category' => $_POST['category'] ?? $frame['category'],
'rarity' => $_POST['rarity'] ?? $frame['rarity'],
'price_coins' => (int)($_POST['price_coins'] ?? 0),
'price_gems' => (int)($_POST['price_gems'] ?? 0),
'is_purchasable' => isset($_POST['is_purchasable']),
'required_level' => (int)($_POST['required_level'] ?? 0),
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'max_supply' => !empty($_POST['max_supply']) ? (int)$_POST['max_supply'] : null,
'sort_order' => (int)($_POST['sort_order'] ?? 0),
'updated_at' => date('c'),
];
if (!empty($_POST['available_from'])) {
$data['available_from'] = $_POST['available_from'];
}
if (!empty($_POST['available_until'])) {
$data['available_until'] = $_POST['available_until'];
}
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$storage = SupabaseStorage::getInstance();
$allowedMimes = ['image/png', 'image/svg+xml', 'image/webp', 'image/gif'];
$error = $storage->validateFile($_FILES['image'], $allowedMimes, 5 * 1024 * 1024);
if (!$error) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['image']['tmp_name']);
$path = $storage->generatePath('frames', $_FILES['image']['name']);
$data['image_url'] = $storage->upload('profile-frames', $path, $_FILES['image']['tmp_name'], $mime);
}
}
$this->db->update('profile_frames', ['id' => "eq.{$id}"], $data);
AuditLog::log('update', 'profile_frame', $id, $frame, $data);
Response::success('تم تحديث الإطار', '/org-frames');
}
public function toggle(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$frame = $this->db->selectOne('profile_frames', ['id' => "eq.{$id}"]);
if (!$frame) {
Response::error('الإطار غير موجود', '/org-frames');
return;
}
$newStatus = !($frame['is_active'] ?? false);
$this->db->update('profile_frames', ['id' => "eq.{$id}"], ['is_active' => $newStatus, 'updated_at' => date('c')]);
AuditLog::log('toggle', 'profile_frame', $id, null, ['is_active' => $newStatus]);
Response::success($newStatus ? 'تم تفعيل الإطار' : 'تم تعطيل الإطار', '/org-frames');
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('admin');
$id = $params['id'];
$this->db->delete('profile_frames', ['id' => "eq.{$id}"]);
AuditLog::log('delete', 'profile_frame', $id);
Response::success('تم حذف الإطار', '/org-frames');
}
public function assign(array $params, string $method): void
{
Auth::requireCsrf();
$playerId = $_POST['player_id'] ?? '';
$frameId = $_POST['frame_id'] ?? '';
if (empty($playerId) || empty($frameId)) {
Response::error('يرجى تحديد اللاعب والإطار', '/org-frames');
return;
}
$existing = $this->db->selectOne('player_frames', [
'player_id' => "eq.{$playerId}",
'frame_id' => "eq.{$frameId}",
]);
if ($existing) {
Response::error('اللاعب يملك هذا الإطار بالفعل', '/org-frames');
return;
}
$frame = $this->db->selectOne('profile_frames', ['id' => "eq.{$frameId}"]);
if ($frame && $frame['max_supply'] !== null && $frame['current_supply'] >= $frame['max_supply']) {
Response::error('تم الوصول للحد الأقصى من الكمية المتاحة', '/org-frames');
return;
}
$this->db->insert('player_frames', [
'player_id' => $playerId,
'frame_id' => $frameId,
'acquisition_type' => 'admin_grant',
'source_id' => 'admin:' . Auth::user()['username'],
]);
if ($frame && $frame['max_supply'] !== null) {
$this->db->update('profile_frames', ['id' => "eq.{$frameId}"], [
'current_supply' => ($frame['current_supply'] ?? 0) + 1,
]);
}
AuditLog::log('assign_frame', 'player_frame', null, null, ['player_id' => $playerId, 'frame_id' => $frameId]);
Response::success('تم منح الإطار للاعب', '/org-frames');
}
public function revokeFromPlayer(array $params, string $method): void
{
Auth::requireCsrf();
$playerId = $_POST['player_id'] ?? '';
$frameId = $_POST['frame_id'] ?? '';
if (empty($playerId) || empty($frameId)) {
Response::error('بيانات غير صالحة', '/org-frames');
return;
}
$this->db->delete('player_frames', [
'player_id' => "eq.{$playerId}",
'frame_id' => "eq.{$frameId}",
]);
$player = $this->db->selectOne('profiles', ['id' => "eq.{$playerId}"]);
if ($player && ($player['avatar_frame_id'] ?? '') === $frameId) {
$this->db->update('profiles', ['id' => "eq.{$playerId}"], [
'avatar_frame_id' => null,
'updated_at' => date('c'),
]);
}
AuditLog::log('revoke_frame', 'player_frame', null, null, ['player_id' => $playerId, 'frame_id' => $frameId]);
Response::success('تم سحب الإطار من اللاعب', '/org-frames');
}
public function bulkAssign(array $params, string $method): void
{
Auth::requireCsrf();
$frameId = $_POST['frame_id'] ?? '';
$orgId = $_POST['org_id'] ?? '';
if (empty($frameId) || empty($orgId)) {
Response::error('يرجى تحديد الإطار والمنظمة', '/org-frames');
return;
}
$members = $this->db->select('org_members', [
'select' => 'user_id',
'org_id' => "eq.{$orgId}",
'status' => 'eq.active',
]);
$assigned = 0;
foreach ($members as $member) {
$existing = $this->db->selectOne('player_frames', [
'player_id' => "eq.{$member['user_id']}",
'frame_id' => "eq.{$frameId}",
]);
if (!$existing) {
$this->db->insert('player_frames', [
'player_id' => $member['user_id'],
'frame_id' => $frameId,
'acquisition_type' => 'org_membership',
'source_id' => "org:{$orgId}",
]);
$assigned++;
}
}
AuditLog::log('bulk_assign_frame', 'player_frame', null, null, [
'frame_id' => $frameId,
'org_id' => $orgId,
'assigned_count' => $assigned,
]);
Response::success("تم منح الإطار لـ {$assigned} عضو", '/org-frames');
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-frames" 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>
<!-- Filter Bar -->
<div class="card mb-5">
<form method="GET" action="/org-frames/assignments" class="flex gap-4 items-end flex-wrap">
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">اللاعب</label>
<input type="text" name="player_id" class="form-input" value="<?= View::e($playerId ?? '') ?>" placeholder="معرّف اللاعب (UUID)" dir="ltr">
</div>
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">الإطار</label>
<select name="frame_id" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($frames as $f): ?>
<option value="<?= $f['id'] ?>" <?= ($frameId ?? '') === $f['id'] ? 'selected' : '' ?>><?= View::e($f['name_ar'] ?: $f['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm">تصفية</button>
<a href="/org-frames/assignments" class="btn btn-ghost btn-sm">مسح</a>
</form>
</div>
<?php
$acquisitionLabels = [
'purchase' => 'شراء',
'admin_grant' => 'منحة إدارية',
'org_membership' => 'عضوية منظمة',
'achievement' => 'إنجاز',
'event_reward' => 'مكافأة فعالية',
'gift' => 'هدية',
];
?>
<div class="data-table-wrapper">
<?php if (empty($assignments)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="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>
<th style="width: 80px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($assignments as $assignment): ?>
<tr>
<td>
<div class="flex items-center gap-3">
<div class="avatar">
<?php if (!empty($assignment['player_avatar'])): ?>
<img src="<?= View::e($assignment['player_avatar']) ?>" alt="">
<?php else: ?>
<?= mb_substr($assignment['player_name'] ?? '?', 0, 1) ?>
<?php endif; ?>
</div>
<div>
<div class="font-medium"><?= View::e($assignment['player_name'] ?? 'غير معروف') ?></div>
<div class="text-xs text-muted"><?= View::e($assignment['player_id'] ?? '') ?></div>
</div>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<?php if (!empty($assignment['frame_thumbnail'])): ?>
<img src="<?= View::e($assignment['frame_thumbnail']) ?>" alt="" style="width: 28px; height: 28px; border-radius: 4px; object-fit: cover;">
<?php endif; ?>
<span><?= View::e($assignment['frame_name_ar'] ?? $assignment['frame_name'] ?? '-') ?></span>
</div>
</td>
<td class="text-xs text-muted tabular-nums">
<?= !empty($assignment['acquired_at']) ? date('Y/m/d H:i', strtotime($assignment['acquired_at'])) : '-' ?>
</td>
<td>
<span class="badge badge-default">
<?= $acquisitionLabels[$assignment['acquisition_type'] ?? ''] ?? ($assignment['acquisition_type'] ?? '-') ?>
</span>
</td>
<td>
<?php if ($assignment['is_equipped'] ?? false): ?>
<span class="badge badge-success badge-dot">مجهّز</span>
<?php else: ?>
<span class="badge badge-default badge-dot">غير مجهّز</span>
<?php endif; ?>
</td>
<td>
<form method="POST" action="/org-frames/revoke" onsubmit="return confirm('هل أنت متأكد من سحب هذا الإطار من اللاعب؟')">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="player_id" value="<?= View::e($assignment['player_id']) ?>">
<input type="hidden" name="frame_id" value="<?= View::e($assignment['frame_id']) ?>">
<button type="submit" class="btn btn-ghost btn-sm" style="color: var(--danger);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
سحب
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (isset($pagination)): ?>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&per_page=<?= $pagination->perPage ?>&player_id=<?= urlencode($playerId ?? '') ?>&frame_id=<?= urlencode($frameId ?? '') ?>" class="pagination-btn <?= !$pagination->hasPrev() ? 'disabled' : '' ?>" <?= !$pagination->hasPrev() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&per_page=<?= $pagination->perPage ?>&player_id=<?= urlencode($playerId ?? '') ?>&frame_id=<?= urlencode($frameId ?? '') ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&per_page=<?= $pagination->perPage ?>&player_id=<?= urlencode($playerId ?? '') ?>&frame_id=<?= urlencode($frameId ?? '') ?>" class="pagination-btn <?= !$pagination->hasNext() ? 'disabled' : '' ?>" <?= !$pagination->hasNext() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php $isEdit = !empty($frame['id']); ?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-frames" 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-frames/{$frame['id']}/update" : '/org-frames/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($frame['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($frame['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($frame['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">الوصف (عربي)</label>
<textarea name="description_ar" class="form-input"><?= View::e($frame['description_ar'] ?? '') ?></textarea>
</div>
<!-- Image Upload -->
<div class="form-group">
<label class="form-label">صورة الإطار * (PNG, SVG, WebP, GIF - أقصى 5MB)</label>
<?php if ($isEdit && !empty($frame['image_url'])): ?>
<div class="mb-3">
<img src="<?= View::e($frame['image_url']) ?>" alt="<?= View::e($frame['name']) ?>" style="width: 80px; height: 80px; border-radius: 8px; object-fit: cover; border: 2px solid var(--border);">
<p class="text-xs text-muted mt-1">الصورة الحالية - اختر صورة جديدة للاستبدال</p>
</div>
<?php endif; ?>
<input type="file" name="image" class="form-input" accept="image/png,image/svg+xml,image/webp,image/gif" <?= !$isEdit ? 'required' : '' ?>>
<span class="form-error"></span>
</div>
<!-- Thumbnail Upload -->
<div class="form-group">
<label class="form-label">صورة مصغرة (اختياري)</label>
<?php if ($isEdit && !empty($frame['thumbnail_url']) && $frame['thumbnail_url'] !== $frame['image_url']): ?>
<div class="mb-3">
<img src="<?= View::e($frame['thumbnail_url']) ?>" alt="thumb" style="width: 48px; height: 48px; border-radius: 8px; object-fit: cover; border: 2px solid var(--border);">
</div>
<?php endif; ?>
<input type="file" name="thumbnail" class="form-input" accept="image/png,image/svg+xml,image/webp,image/gif">
</div>
<!-- Category & Rarity -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">التصنيف *</label>
<select name="category" class="form-select" required>
<option value="general" <?= ($frame['category'] ?? '') === 'general' ? 'selected' : '' ?>>عام</option>
<option value="seasonal" <?= ($frame['category'] ?? '') === 'seasonal' ? 'selected' : '' ?>>موسمي</option>
<option value="achievement" <?= ($frame['category'] ?? '') === 'achievement' ? 'selected' : '' ?>>إنجاز</option>
<option value="org" <?= ($frame['category'] ?? '') === 'org' ? 'selected' : '' ?>>منظمة</option>
<option value="event" <?= ($frame['category'] ?? '') === 'event' ? 'selected' : '' ?>>فعالية</option>
<option value="premium" <?= ($frame['category'] ?? '') === 'premium' ? 'selected' : '' ?>>مميز</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الندرة *</label>
<select name="rarity" class="form-select" required>
<option value="common" <?= ($frame['rarity'] ?? '') === 'common' ? 'selected' : '' ?>>عادي</option>
<option value="uncommon" <?= ($frame['rarity'] ?? '') === 'uncommon' ? 'selected' : '' ?>>غير شائع</option>
<option value="rare" <?= ($frame['rarity'] ?? '') === 'rare' ? 'selected' : '' ?>>نادر</option>
<option value="epic" <?= ($frame['rarity'] ?? '') === 'epic' ? 'selected' : '' ?>>ملحمي</option>
<option value="legendary" <?= ($frame['rarity'] ?? '') === 'legendary' ? 'selected' : '' ?>>أسطوري</option>
</select>
</div>
</div>
<!-- Price -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">السعر (عملات)</label>
<input type="number" name="price_coins" class="form-input" value="<?= $frame['price_coins'] ?? 0 ?>" min="0">
</div>
<div class="form-group">
<label class="form-label">السعر (جواهر)</label>
<input type="number" name="price_gems" class="form-input" value="<?= $frame['price_gems'] ?? 0 ?>" min="0">
</div>
</div>
<!-- Purchasable & Required Level -->
<div class="flex gap-6 mb-5">
<label class="toggle">
<input type="checkbox" name="is_purchasable" <?= ($frame['is_purchasable'] ?? false) ? 'checked' : '' ?>>
<span class="toggle-track"></span>
<span>قابل للشراء</span>
</label>
</div>
<div class="form-group">
<label class="form-label">المستوى المطلوب</label>
<input type="number" name="required_level" class="form-input" value="<?= $frame['required_level'] ?? 0 ?>" min="0">
</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']) ?>" <?= ($frame['org_id'] ?? '') === $org['id'] ? 'selected' : '' ?>><?= View::e($org['name_ar'] ?: $org['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Max Supply -->
<div class="form-group">
<label class="form-label">الكمية القصوى (اتركه فارغاً لغير محدود)</label>
<input type="number" name="max_supply" class="form-input" value="<?= $frame['max_supply'] ?? '' ?>" min="1" placeholder="غير محدود">
</div>
<!-- Availability Period -->
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">متاح من</label>
<input type="datetime-local" name="available_from" class="form-input" value="<?= !empty($frame['available_from']) ? date('Y-m-d\TH:i', strtotime($frame['available_from'])) : '' ?>" dir="ltr">
</div>
<div class="form-group">
<label class="form-label">متاح حتى</label>
<input type="datetime-local" name="available_until" class="form-input" value="<?= !empty($frame['available_until']) ? date('Y-m-d\TH:i', strtotime($frame['available_until'])) : '' ?>" dir="ltr">
</div>
</div>
<!-- Sort Order -->
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" class="form-input" value="<?= $frame['sort_order'] ?? 0 ?>">
</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-frames" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
<div class="content-header">
<h1>إطارات الملفات الشخصية</h1>
<a href="/org-frames/create" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
إضافة إطار
</a>
</div>
<!-- Filter Bar -->
<div class="card mb-5">
<form method="GET" action="/org-frames" class="flex gap-4 items-end flex-wrap">
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">بحث</label>
<input type="text" name="search" class="form-input" value="<?= View::e($search) ?>" placeholder="بحث بالاسم...">
</div>
<div class="form-group" style="margin-bottom: 0; min-width: 140px;">
<label class="form-label">التصنيف</label>
<select name="category" class="form-select">
<option value="">الكل</option>
<option value="general" <?= $category === 'general' ? 'selected' : '' ?>>عام</option>
<option value="seasonal" <?= $category === 'seasonal' ? 'selected' : '' ?>>موسمي</option>
<option value="achievement" <?= $category === 'achievement' ? 'selected' : '' ?>>إنجاز</option>
<option value="org" <?= $category === 'org' ? 'selected' : '' ?>>منظمة</option>
<option value="event" <?= $category === 'event' ? 'selected' : '' ?>>فعالية</option>
<option value="premium" <?= $category === 'premium' ? 'selected' : '' ?>>مميز</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0; min-width: 140px;">
<label class="form-label">الندرة</label>
<select name="rarity" class="form-select">
<option value="">الكل</option>
<option value="common" <?= $rarity === 'common' ? 'selected' : '' ?>>عادي</option>
<option value="uncommon" <?= $rarity === 'uncommon' ? 'selected' : '' ?>>غير شائع</option>
<option value="rare" <?= $rarity === 'rare' ? 'selected' : '' ?>>نادر</option>
<option value="epic" <?= $rarity === 'epic' ? 'selected' : '' ?>>ملحمي</option>
<option value="legendary" <?= $rarity === 'legendary' ? 'selected' : '' ?>>أسطوري</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0; min-width: 140px;">
<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 btn-sm">تصفية</button>
<a href="/org-frames" class="btn btn-ghost btn-sm">مسح</a>
</form>
</div>
<?php
$rarityColors = [
'common' => 'badge-default',
'uncommon' => 'badge-success',
'rare' => 'badge-info',
'epic' => 'badge-purple',
'legendary' => 'badge-warning',
];
$rarityLabels = [
'common' => 'عادي',
'uncommon' => 'غير شائع',
'rare' => 'نادر',
'epic' => 'ملحمي',
'legendary' => 'أسطوري',
];
$categoryLabels = [
'general' => 'عام',
'seasonal' => 'موسمي',
'achievement' => 'إنجاز',
'org' => 'منظمة',
'event' => 'فعالية',
'premium' => 'مميز',
];
?>
<?php if (empty($frames)): ?>
<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"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="4"/><path d="M3 3l4 4M21 3l-4 4M3 21l4-4M21 21l-4-4"/></svg>
<h3 class="empty-state-title">لا توجد إطارات</h3>
<p class="empty-state-text">لم يتم إضافة أي إطارات بعد<?= $search ? ' لبحثك "' . View::e($search) . '"' : '' ?></p>
<a href="/org-frames/create" class="btn btn-primary">إضافة إطار</a>
</div>
</div>
<?php else: ?>
<!-- Frames Card Grid -->
<div class="grid grid-3 stagger mb-5">
<?php foreach ($frames as $frame): ?>
<div class="card card-hover">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-3">
<?php if (!empty($frame['thumbnail_url'] ?? $frame['image_url'])): ?>
<img src="<?= View::e($frame['thumbnail_url'] ?? $frame['image_url']) ?>" alt="<?= View::e($frame['name']) ?>" style="width: 48px; height: 48px; border-radius: 8px; object-fit: cover;">
<?php else: ?>
<div class="stat-icon blue" style="width: 48px; height: 48px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
</div>
<?php endif; ?>
<div>
<h3 class="font-semibold"><?= View::e($frame['name_ar'] ?: $frame['name']) ?></h3>
<p class="text-xs text-muted"><?= View::e($frame['name']) ?></p>
</div>
</div>
<form method="POST" action="/org-frames/<?= $frame['id'] ?>/toggle">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<label class="toggle">
<input type="checkbox" <?= ($frame['is_active'] ?? false) ? 'checked' : '' ?> onchange="this.closest('form').submit()">
<span class="toggle-track"></span>
</label>
</form>
</div>
<div class="flex gap-2 flex-wrap mb-3">
<span class="badge badge-info"><?= $categoryLabels[$frame['category'] ?? 'general'] ?? $frame['category'] ?></span>
<span class="badge <?= $rarityColors[$frame['rarity'] ?? 'common'] ?? 'badge-default' ?>"><?= $rarityLabels[$frame['rarity'] ?? 'common'] ?? $frame['rarity'] ?></span>
</div>
<div class="flex gap-4 text-xs text-secondary mb-3">
<?php if (($frame['price_coins'] ?? 0) > 0): ?>
<span><?= number_format($frame['price_coins']) ?> عملة</span>
<?php endif; ?>
<?php if (($frame['price_gems'] ?? 0) > 0): ?>
<span><?= number_format($frame['price_gems']) ?> جوهرة</span>
<?php endif; ?>
<?php if (($frame['price_coins'] ?? 0) == 0 && ($frame['price_gems'] ?? 0) == 0): ?>
<span>مجاني</span>
<?php endif; ?>
</div>
<?php if ($frame['max_supply'] !== null): ?>
<div class="text-xs text-secondary mb-3">
الكمية: <?= (int)($frame['current_supply'] ?? 0) ?> / <?= number_format($frame['max_supply']) ?>
</div>
<?php endif; ?>
<div class="flex items-center justify-between">
<span class="badge <?= ($frame['is_active'] ?? false) ? 'badge-success' : 'badge-default' ?> badge-dot">
<?= ($frame['is_active'] ?? false) ? 'مفعّل' : 'معطّل' ?>
</span>
<div class="flex gap-2">
<a href="/org-frames/<?= $frame['id'] ?>/edit" class="btn btn-ghost btn-sm">تعديل</a>
<button class="btn btn-ghost btn-sm" style="color: var(--danger);" onclick="confirmDelete('/org-frames/<?= $frame['id'] ?>/delete', '<?= View::e($frame['name_ar'] ?: $frame['name']) ?>')">حذف</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&per_page=<?= $pagination->perPage ?>&search=<?= urlencode($search) ?>&category=<?= urlencode($category) ?>&rarity=<?= urlencode($rarity) ?>&org_id=<?= urlencode($orgId) ?>" class="pagination-btn <?= !$pagination->hasPrev() ? 'disabled' : '' ?>" <?= !$pagination->hasPrev() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&per_page=<?= $pagination->perPage ?>&search=<?= urlencode($search) ?>&category=<?= urlencode($category) ?>&rarity=<?= urlencode($rarity) ?>&org_id=<?= urlencode($orgId) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&per_page=<?= $pagination->perPage ?>&search=<?= urlencode($search) ?>&category=<?= urlencode($category) ?>&rarity=<?= urlencode($rarity) ?>&org_id=<?= urlencode($orgId) ?>" class="pagination-btn <?= !$pagination->hasNext() ? 'disabled' : '' ?>" <?= !$pagination->hasNext() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</div>
</div>
<?php endif; ?>
<!-- Assign Frame to Player -->
<div class="card mt-5">
<div class="card-header">
<h3 class="card-title">منح إطار للاعب</h3>
</div>
<form method="POST" action="/org-frames/assign" class="flex gap-4 items-end flex-wrap">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">اللاعب</label>
<input type="text" name="player_id" class="form-input" placeholder="معرّف اللاعب (UUID)" required dir="ltr">
</div>
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">الإطار</label>
<select name="frame_id" class="form-select" required>
<option value="">-- اختر إطار --</option>
<?php foreach ($frames as $f): ?>
<option value="<?= $f['id'] ?>"><?= View::e($f['name_ar'] ?: $f['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
منح الإطار
</button>
</form>
</div>
<!-- Bulk Assign to Organization -->
<div class="card mt-4">
<div class="card-header">
<h3 class="card-title">منح إطار لجميع أعضاء منظمة</h3>
</div>
<form method="POST" action="/org-frames/bulk-assign" class="flex gap-4 items-end flex-wrap">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">المنظمة</label>
<select name="org_id" class="form-select" required>
<option value="">-- اختر منظمة --</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= View::e($org['id']) ?>"><?= View::e($org['name_ar'] ?: $org['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 180px;">
<label class="form-label">الإطار</label>
<select name="frame_id" class="form-select" required>
<option value="">-- اختر إطار --</option>
<?php foreach ($frames as $f): ?>
<option value="<?= $f['id'] ?>"><?= View::e($f['name_ar'] ?: $f['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد؟ سيتم منح الإطار لجميع الأعضاء النشطين.')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
منح للجميع
</button>
</form>
</div>
<?php
class OrgInvitesController
{
private Database $db;
public function __construct()
{
$this->db = Database::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;
}
$invites = $this->db->select('org_invite_links', [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'created_at.desc',
]);
// Get usage counts for each invite
foreach ($invites as &$invite) {
$invite['uses_count'] = $this->db->count('org_invite_uses', [
'invite_link_id' => "eq.{$invite['id']}",
]);
}
unset($invite);
$pageTitle = 'روابط الدعوة - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-invites/list', compact('org', 'invites', '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-invites/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();
$code = bin2hex(random_bytes(4));
$data = [
'org_id' => $orgId,
'code' => $code,
'created_by' => $user['id'],
'max_uses' => !empty($_POST['max_uses']) ? (int) $_POST['max_uses'] : null,
'expires_at' => !empty($_POST['expires_at']) ? $_POST['expires_at'] : null,
'requires_approval' => isset($_POST['requires_approval']) ? true : false,
'target_role' => $_POST['target_role'] ?? 'member',
'welcome_message' => trim($_POST['welcome_message'] ?? ''),
'welcome_message_ar' => trim($_POST['welcome_message_ar'] ?? ''),
'is_active' => true,
];
$result = $this->db->insert('org_invite_links', $data);
AuditLog::log('create', 'org_invite_link', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء رابط الدعوة بنجاح', "/organizations/{$orgId}/invites");
}
public function toggle(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$inviteId = $params['inviteId'];
$invite = $this->db->selectOne('org_invite_links', [
'id' => "eq.{$inviteId}",
'org_id' => "eq.{$orgId}",
]);
if (!$invite) {
Response::error('رابط الدعوة غير موجود', "/organizations/{$orgId}/invites");
return;
}
$newStatus = !($invite['is_active'] ?? false);
$this->db->update('org_invite_links', ['id' => "eq.{$inviteId}"], [
'is_active' => $newStatus,
]);
AuditLog::log('toggle', 'org_invite_link', $inviteId, $invite, ['is_active' => $newStatus]);
Response::success(
$newStatus ? 'تم تفعيل رابط الدعوة' : 'تم تعطيل رابط الدعوة',
"/organizations/{$orgId}/invites"
);
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$inviteId = $params['inviteId'];
$invite = $this->db->selectOne('org_invite_links', [
'id' => "eq.{$inviteId}",
'org_id' => "eq.{$orgId}",
]);
if (!$invite) {
Response::error('رابط الدعوة غير موجود', "/organizations/{$orgId}/invites");
return;
}
$this->db->delete('org_invite_links', ['id' => "eq.{$inviteId}"]);
AuditLog::log('delete', 'org_invite_link', $inviteId, $invite, null);
Response::success('تم حذف رابط الدعوة', "/organizations/{$orgId}/invites");
}
public function usage(array $params, string $method): void
{
$orgId = $params['id'];
$inviteId = $params['inviteId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$invite = $this->db->selectOne('org_invite_links', [
'id' => "eq.{$inviteId}",
'org_id' => "eq.{$orgId}",
]);
if (!$invite) {
Response::error('رابط الدعوة غير موجود', "/organizations/{$orgId}/invites");
return;
}
$uses = $this->db->select('org_invite_uses', [
'select' => '*, profiles(username, display_name)',
'invite_link_id' => "eq.{$inviteId}",
'order' => 'used_at.desc',
]);
$pageTitle = 'استخدام رابط الدعوة - ' . $invite['code'];
View::render('org-invites/usage', compact('org', 'invite', 'uses', 'pageTitle'));
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/invites" 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>
<div class="card max-w-lg">
<form method="POST" action="/organizations/<?= $org['id'] ?>/invites/store" data-validate>
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">الحد الأقصى للاستخدام</label>
<input type="number" name="max_uses" class="form-input" min="1" placeholder="اتركه فارغاً لاستخدام غير محدود">
<span class="form-hint">عدد المرات التي يمكن فيها استخدام هذا الرابط. اتركه فارغاً للسماح باستخدام غير محدود.</span>
</div>
<div class="form-group">
<label class="form-label">تاريخ انتهاء الصلاحية</label>
<input type="datetime-local" name="expires_at" class="form-input" dir="ltr">
<span class="form-hint">اتركه فارغاً لرابط بدون انتهاء صلاحية.</span>
</div>
<div class="form-group">
<label class="form-label">الدور المستهدف</label>
<select name="target_role" class="form-input">
<option value="member">عضو</option>
<option value="moderator">مشرف</option>
<option value="admin">مدير</option>
</select>
<span class="form-hint">الدور الذي سيحصل عليه المستخدم عند الانضمام عبر هذا الرابط.</span>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="requires_approval" value="1">
<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">رسالة الترحيب (English)</label>
<textarea name="welcome_message" class="form-input" rows="3" placeholder="Welcome message for new members..." dir="ltr"></textarea>
</div>
<div class="form-group">
<label class="form-label">رسالة الترحيب (عربي)</label>
<textarea name="welcome_message_ar" class="form-input" rows="3" placeholder="رسالة ترحيب للأعضاء الجدد..."></textarea>
</div>
<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="/organizations/<?= $org['id'] ?>/invites" 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'] ?>/invites/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($invites)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></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: 60px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($invites as $invite): ?>
<tr>
<td>
<div class="flex items-center gap-2">
<code class="invite-code" style="font-family: monospace; font-size: 0.9rem; background: var(--bg-secondary); padding: 2px 8px; border-radius: 4px;"><?= View::e($invite['code']) ?></code>
<button type="button" class="btn btn-icon btn-ghost btn-sm" onclick="copyToClipboard('<?= View::e($invite['code']) ?>')" title="نسخ الكود">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
</td>
<td>
<?php
$roleLabels = ['member' => 'عضو', 'moderator' => 'مشرف', 'admin' => 'مدير'];
$role = $invite['target_role'] ?? 'member';
?>
<span class="badge badge-default"><?= $roleLabels[$role] ?? $role ?></span>
</td>
<td>
<?php
$usesCount = $invite['uses_count'] ?? 0;
$maxUses = $invite['max_uses'] ?? null;
?>
<span><?= $usesCount ?> / <?= $maxUses ? $maxUses : '&#8734;' ?></span>
</td>
<td>
<?php if (!empty($invite['expires_at'])): ?>
<?php
$expiresAt = strtotime($invite['expires_at']);
$isExpired = $expiresAt < time();
?>
<span class="<?= $isExpired ? 'text-danger' : '' ?>">
<?= date('Y-m-d H:i', $expiresAt) ?>
</span>
<?php else: ?>
<span class="text-muted">بدون انتهاء</span>
<?php endif; ?>
</td>
<td>
<?php if ($invite['is_active'] ?? false): ?>
<span class="badge badge-success badge-dot">نشط</span>
<?php else: ?>
<span class="badge badge-danger badge-dot">معطل</span>
<?php endif; ?>
</td>
<td>
<?= date('Y-m-d', strtotime($invite['created_at'])) ?>
</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'] ?>/invites/<?= $invite['id'] ?>/usage" class="dropdown-item">الاستخدام</a>
<form method="POST" action="/organizations/<?= $org['id'] ?>/invites/<?= $invite['id'] ?>/toggle" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="dropdown-item">
<?= ($invite['is_active'] ?? false) ? 'تعطيل' : 'تفعيل' ?>
</button>
</form>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger" onclick="confirmDelete('/organizations/<?= $org['id'] ?>/invites/<?= $invite['id'] ?>/delete', '<?= View::e($invite['code']) ?>')">حذف</button>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
showToast('تم نسخ الكود بنجاح', 'success');
}).catch(function() {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('تم نسخ الكود بنجاح', 'success');
});
}
</script>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/invites" 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>
<!-- Link Info Summary -->
<div class="card mb-5">
<div class="grid grid-3 gap-4">
<div>
<span class="text-sm text-muted">الكود</span>
<div class="font-medium mt-1">
<code style="font-family: monospace; font-size: 1rem; background: var(--bg-secondary); padding: 2px 8px; border-radius: 4px;"><?= View::e($invite['code']) ?></code>
</div>
</div>
<div>
<span class="text-sm text-muted">المنظمة</span>
<div class="font-medium mt-1"><?= View::e($org['name_ar'] ?? $org['name']) ?></div>
</div>
<div>
<span class="text-sm text-muted">أنشئ بواسطة</span>
<div class="font-medium mt-1"><?= View::e($invite['created_by'] ?? '-') ?></div>
</div>
</div>
<div class="grid grid-3 gap-4 mt-4">
<div>
<span class="text-sm text-muted">الدور المستهدف</span>
<?php
$roleLabels = ['member' => 'عضو', 'moderator' => 'مشرف', 'admin' => 'مدير'];
$role = $invite['target_role'] ?? 'member';
?>
<div class="font-medium mt-1"><span class="badge badge-default"><?= $roleLabels[$role] ?? $role ?></span></div>
</div>
<div>
<span class="text-sm text-muted">الحالة</span>
<div class="font-medium mt-1">
<?php if ($invite['is_active'] ?? false): ?>
<span class="badge badge-success badge-dot">نشط</span>
<?php else: ?>
<span class="badge badge-danger badge-dot">معطل</span>
<?php endif; ?>
</div>
</div>
<div>
<span class="text-sm text-muted">يتطلب موافقة</span>
<div class="font-medium mt-1"><?= ($invite['requires_approval'] ?? false) ? 'نعم' : 'لا' ?></div>
</div>
</div>
</div>
<!-- Usage Table -->
<div class="data-table-wrapper">
<?php if (empty($uses)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<h3 class="empty-state-title">لا يوجد استخدام</h3>
<p class="empty-state-text">لم يستخدم أحد هذا الرابط بعد</p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>اسم اللاعب</th>
<th>الحالة</th>
<th>تسجيل جديد؟</th>
<th>تاريخ الاستخدام</th>
</tr>
</thead>
<tbody>
<?php foreach ($uses as $use): ?>
<tr>
<td>
<?php
$profile = $use['profiles'] ?? null;
$username = $profile['username'] ?? $profile['display_name'] ?? '-';
?>
<span class="font-medium"><?= View::e($username) ?></span>
</td>
<td>
<?php
$status = $use['status'] ?? 'joined';
$statusLabels = [
'joined' => ['label' => 'انضم', 'class' => 'badge-success'],
'pending' => ['label' => 'بانتظار الموافقة', 'class' => 'badge-warning'],
'rejected' => ['label' => 'مرفوض', 'class' => 'badge-danger'],
];
$statusInfo = $statusLabels[$status] ?? ['label' => $status, 'class' => 'badge-default'];
?>
<span class="badge <?= $statusInfo['class'] ?>"><?= $statusInfo['label'] ?></span>
</td>
<td>
<?php if ($use['is_new_signup'] ?? false): ?>
<span class="badge badge-info">تسجيل جديد</span>
<?php else: ?>
<span class="text-muted">لا</span>
<?php endif; ?>
</td>
<td>
<?= date('Y-m-d H:i', strtotime($use['used_at'])) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
.rank-change {
font-size: 0.75rem;
font-weight: 600;
}
.rank-up { color: #22c55e; }
.rank-down { color: #ef4444; }
.rank-same { color: var(--text-muted); }
.rank-medal {
width: 28px;
height: 28px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.8rem;
}
.rank-medal.gold { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
.rank-medal.silver { background: rgba(156, 163, 175, 0.15); color: #9ca3af; }
.rank-medal.bronze { background: rgba(180, 83, 9, 0.15); color: #b45309; }
<?php
class OrgLeaderboardsController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function index(array $params, string $method): void
{
$queryParams = [
'select' => '*, el3ab_organizations(id,name,name_ar,logo_url)',
'order' => 'total_points.desc',
];
$total = $this->db->count('org_seasonal_rankings', []);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$rankings = $this->db->select('org_seasonal_rankings', $queryParams);
$pageTitle = 'لوحة المتصدرين العامة';
View::render('org-leaderboards/index', compact('rankings', 'pagination', 'pageTitle'));
}
public function orgBoard(array $params, string $method): void
{
$orgId = $params['orgId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$queryParams = [
'select' => '*, profiles(id,username,display_name,avatar_url)',
'org_id' => "eq.{$orgId}",
'order' => 'points.desc',
];
$total = $this->db->count('org_leaderboards', ['org_id' => "eq.{$orgId}"]);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$leaderboard = $this->db->select('org_leaderboards', $queryParams);
$pageTitle = 'المتصدرين - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-leaderboards/org-board', compact('org', 'leaderboard', 'pagination', 'pageTitle'));
}
public function seasonal(array $params, string $method): void
{
$season = $_GET['season'] ?? '';
$queryParams = [
'select' => '*, el3ab_organizations(id,name,name_ar,logo_url)',
'order' => 'total_points.desc',
];
if ($season) {
$queryParams['season'] = "eq.{$season}";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_seasonal_rankings', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$rankings = $this->db->select('org_seasonal_rankings', $queryParams);
$pageTitle = 'التصنيف الموسمي';
View::render('org-leaderboards/seasonal', compact('rankings', 'pagination', 'season', 'pageTitle'));
}
public function recalculate(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['orgId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/org-leaderboards');
return;
}
// Get all members of this org
$members = $this->db->select('org_members', [
'select' => 'player_id',
'org_id' => "eq.{$orgId}",
]);
foreach ($members as $member) {
$playerId = $member['player_id'];
// Count matches participated
$matchCount = $this->db->count('match_players', [
'player_id' => "eq.{$playerId}",
]);
// Count tournament participations
$tournamentCount = $this->db->count('tournament_participants', [
'player_id' => "eq.{$playerId}",
]);
// Calculate points
$points = ($matchCount * 10) + ($tournamentCount * 50);
// Check if entry exists
$existing = $this->db->selectOne('org_leaderboards', [
'org_id' => "eq.{$orgId}",
'player_id' => "eq.{$playerId}",
]);
if ($existing) {
$this->db->update('org_leaderboards', [
'org_id' => "eq.{$orgId}",
'player_id' => "eq.{$playerId}",
], [
'points' => $points,
'matches_played' => $matchCount,
'tournaments_played' => $tournamentCount,
'updated_at' => date('c'),
]);
} else {
$this->db->insert('org_leaderboards', [
'org_id' => $orgId,
'player_id' => $playerId,
'points' => $points,
'matches_played' => $matchCount,
'tournaments_played' => $tournamentCount,
]);
}
}
AuditLog::log('recalculate', 'org_leaderboard', $orgId, null, ['members_count' => count($members)]);
Response::success('تم إعادة حساب النقاط بنجاح', "/org-leaderboards/{$orgId}");
}
public function apiLeaderboard(array $params, string $method): void
{
$orgId = $params['orgId'];
$leaderboard = $this->db->select('org_leaderboards', [
'select' => '*, profiles(id,username,display_name,avatar_url)',
'org_id' => "eq.{$orgId}",
'order' => 'points.desc',
'limit' => 50,
]);
header('Content-Type: application/json');
echo json_encode($leaderboard);
}
}
<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>
<form method="POST" action="/org-leaderboards/<?= $org['id'] ?>/recalculate" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل تريد إعادة حساب النقاط؟')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
إعادة الحساب
</button>
</form>
</div>
<!-- Filters -->
<div class="card mb-5">
<form method="GET" action="/org-leaderboards/<?= $org['id'] ?>" 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="season" class="form-input" value="<?= View::e($_GET['season'] ?? '') ?>" placeholder="e.g. 2024-Q1" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
</form>
</div>
<!-- Leaderboard Table -->
<div class="data-table-wrapper">
<?php if (empty($leaderboard)): ?>
<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>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th style="width: 60px;">المركز</th>
<th>اللاعب</th>
<th>النقاط</th>
<th>المباريات</th>
<th>الفوز</th>
<th>البطولات</th>
<th>فوز بطولات</th>
<th>سلسلة حالية</th>
<th>أفضل سلسلة</th>
</tr>
</thead>
<tbody>
<?php foreach ($leaderboard as $index => $entry): ?>
<?php
$rank = $pagination->offset + $index + 1;
$player = $entry['profiles'] ?? null;
$playerName = $player['display_name'] ?? $player['username'] ?? substr($entry['player_id'], 0, 8) . '...';
?>
<tr>
<td>
<?php if ($rank <= 3): ?>
<span class="badge <?= $rank === 1 ? 'badge-warning' : ($rank === 2 ? 'badge-default' : 'badge-info') ?>">#<?= $rank ?></span>
<?php else: ?>
<span class="text-muted">#<?= $rank ?></span>
<?php endif; ?>
</td>
<td>
<div class="flex items-center gap-2">
<?php if (!empty($player['avatar_url'])): ?>
<img src="<?= View::e($player['avatar_url']) ?>" alt="" style="width:24px;height:24px;border-radius:50%;">
<?php endif; ?>
<span><?= View::e($playerName) ?></span>
</div>
</td>
<td><strong class="tabular-nums"><?= number_format($entry['points'] ?? 0) ?></strong></td>
<td class="tabular-nums"><?= number_format($entry['matches_played'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($entry['matches_won'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($entry['tournaments_played'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($entry['tournaments_won'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($entry['streak_current'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($entry['streak_best'] ?? 0) ?></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 ?>&season=<?= urlencode($_GET['season'] ?? '') ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="content-header">
<h1>التصنيف الموسمي</h1>
</div>
<!-- Filters -->
<div class="card mb-5">
<form method="GET" action="/org-leaderboards/seasonal" 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="season" class="form-input" value="<?= View::e($season ?? '') ?>" placeholder="e.g. 2024-Q1" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
</form>
</div>
<!-- Seasonal Rankings Table -->
<div class="data-table-wrapper">
<?php if (empty($rankings)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 21h8m-4-4v4m-4.5-8.5L12 8l4.5 4.5M3 3h18v4H3z"/></svg>
<h3 class="empty-state-title">لا توجد تصنيفات</h3>
<p class="empty-state-text">لم يتم تسجيل أي تصنيفات موسمية<?= !empty($season) ? ' لهذا الموسم' : '' ?></p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th style="width: 60px;">المركز</th>
<th>المنظمة</th>
<th>النقاط</th>
<th>فوز مباريات</th>
<th>خسارة مباريات</th>
<th>فوز بطولات</th>
<th>فوز تحديات</th>
<th>نشاط الأعضاء</th>
<th>التغيير</th>
</tr>
</thead>
<tbody>
<?php foreach ($rankings as $index => $ranking): ?>
<?php
$rank = $pagination->offset + $index + 1;
$org = $ranking['el3ab_organizations'] ?? null;
$orgName = $org['name_ar'] ?? $org['name'] ?? '-';
$previousRank = $ranking['previous_rank'] ?? null;
$rankChange = $previousRank ? ($previousRank - $rank) : 0;
?>
<tr>
<td>
<?php if ($rank <= 3): ?>
<span class="badge <?= $rank === 1 ? 'badge-warning' : ($rank === 2 ? 'badge-default' : 'badge-info') ?>">#<?= $rank ?></span>
<?php else: ?>
<span class="text-muted">#<?= $rank ?></span>
<?php endif; ?>
</td>
<td>
<div class="flex items-center gap-2">
<?php if (!empty($org['logo_url'])): ?>
<img src="<?= View::e($org['logo_url']) ?>" alt="" style="width:24px;height:24px;border-radius:4px;">
<?php endif; ?>
<span><?= View::e($orgName) ?></span>
</div>
</td>
<td><strong class="tabular-nums"><?= number_format($ranking['total_points'] ?? 0) ?></strong></td>
<td class="tabular-nums"><?= number_format($ranking['matches_won'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($ranking['matches_lost'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($ranking['tournaments_won'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($ranking['challenges_won'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($ranking['member_activity_score'] ?? 0) ?></td>
<td>
<?php if ($rankChange > 0): ?>
<span style="color: var(--success);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;"><polyline points="18 15 12 9 6 15"/></svg>
<?= $rankChange ?>
</span>
<?php elseif ($rankChange < 0): ?>
<span style="color: var(--danger);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;"><polyline points="6 9 12 15 18 9"/></svg>
<?= abs($rankChange) ?>
</span>
<?php elseif ($previousRank !== null): ?>
<span class="text-muted">-</span>
<?php else: ?>
<span class="badge badge-info">جديد</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 ?>&season=<?= urlencode($season ?? '') ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
.loyalty-milestone {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.85rem;
}
.loyalty-days {
font-weight: 700;
color: var(--primary);
}
<?php
class OrgLoyaltyController
{
private Database $db;
public function __construct()
{
$this->db = Database::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;
}
$rewards = $this->db->select('org_loyalty_rewards', [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'sort_order.asc',
]);
$pageTitle = 'مكافآت الولاء - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-loyalty/list', compact('org', 'rewards', '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;
}
$reward = [];
$pageTitle = 'إضافة مكافأة ولاء - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-loyalty/form', compact('org', 'reward', '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;
}
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
Response::error('اسم المكافأة مطلوب', "/organizations/{$orgId}/loyalty/create");
return;
}
$data = [
'org_id' => $orgId,
'name' => $name,
'name_ar' => trim($_POST['name_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'days_required' => (int) ($_POST['days_required'] ?? 0),
'reward_type' => $_POST['reward_type'] ?? 'coins',
'reward_amount' => (int) ($_POST['reward_amount'] ?? 0),
'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null,
'reward_title' => trim($_POST['reward_title'] ?? ''),
'is_active' => true,
'sort_order' => (int) ($_POST['sort_order'] ?? 0),
];
$result = $this->db->insert('org_loyalty_rewards', $data);
AuditLog::log('create', 'org_loyalty_reward', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء مكافأة الولاء بنجاح', "/organizations/{$orgId}/loyalty");
}
public function edit(array $params, string $method): void
{
$orgId = $params['id'];
$rewardId = $params['rewardId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$reward = $this->db->selectOne('org_loyalty_rewards', [
'id' => "eq.{$rewardId}",
'org_id' => "eq.{$orgId}",
]);
if (!$reward) {
Response::error('المكافأة غير موجودة', "/organizations/{$orgId}/loyalty");
return;
}
$pageTitle = 'تعديل مكافأة الولاء - ' . ($reward['name_ar'] ?? $reward['name']);
View::render('org-loyalty/form', compact('org', 'reward', 'pageTitle'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rewardId = $params['rewardId'];
$reward = $this->db->selectOne('org_loyalty_rewards', [
'id' => "eq.{$rewardId}",
'org_id' => "eq.{$orgId}",
]);
if (!$reward) {
Response::error('المكافأة غير موجودة', "/organizations/{$orgId}/loyalty");
return;
}
$data = [
'name' => trim($_POST['name'] ?? ''),
'name_ar' => trim($_POST['name_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'days_required' => (int) ($_POST['days_required'] ?? 0),
'reward_type' => $_POST['reward_type'] ?? 'coins',
'reward_amount' => (int) ($_POST['reward_amount'] ?? 0),
'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null,
'reward_title' => trim($_POST['reward_title'] ?? ''),
'sort_order' => (int) ($_POST['sort_order'] ?? 0),
'updated_at' => date('c'),
];
$this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], $data);
AuditLog::log('update', 'org_loyalty_reward', $rewardId, $reward, $data);
Response::success('تم تحديث المكافأة', "/organizations/{$orgId}/loyalty");
}
public function toggle(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rewardId = $params['rewardId'];
$reward = $this->db->selectOne('org_loyalty_rewards', [
'id' => "eq.{$rewardId}",
'org_id' => "eq.{$orgId}",
]);
if (!$reward) {
Response::error('المكافأة غير موجودة', "/organizations/{$orgId}/loyalty");
return;
}
$newStatus = !($reward['is_active'] ?? false);
$this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], [
'is_active' => $newStatus,
'updated_at' => date('c'),
]);
AuditLog::log('toggle', 'org_loyalty_reward', $rewardId, $reward, ['is_active' => $newStatus]);
Response::success(
$newStatus ? 'تم تفعيل المكافأة' : 'تم تعطيل المكافأة',
"/organizations/{$orgId}/loyalty"
);
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rewardId = $params['rewardId'];
$reward = $this->db->selectOne('org_loyalty_rewards', [
'id' => "eq.{$rewardId}",
'org_id' => "eq.{$orgId}",
]);
if (!$reward) {
Response::error('المكافأة غير موجودة', "/organizations/{$orgId}/loyalty");
return;
}
$this->db->delete('org_loyalty_rewards', ['id' => "eq.{$rewardId}"]);
AuditLog::log('delete', 'org_loyalty_reward', $rewardId, $reward, null);
Response::success('تم حذف المكافأة', "/organizations/{$orgId}/loyalty");
}
public function claims(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;
}
$queryParams = [
'select' => '*, profiles(id,username,display_name,avatar_url), org_loyalty_rewards(id,name,name_ar,reward_type,reward_amount)',
'org_id' => "eq.{$orgId}",
'order' => 'claimed_at.desc',
];
$total = $this->db->count('player_loyalty_claims', ['org_id' => "eq.{$orgId}"]);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$claims = $this->db->select('player_loyalty_claims', $queryParams);
$pageTitle = 'مطالبات الولاء - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-loyalty/claims', compact('org', 'claims', 'pagination', 'pageTitle'));
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/loyalty" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= View::e($pageTitle) ?></h1>
</div>
</div>
<?php if (empty($claims)): ?>
<div class="card">
<div class="empty-state">
<h3 class="empty-state-title">لا توجد مطالبات</h3>
<p class="empty-state-text">لم يطالب أي لاعب بمكافآت ولاء لهذه المنظمة بعد</p>
</div>
</div>
<?php else: ?>
<div class="card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>اللاعب</th>
<th>المكافأة</th>
<th>نوع المكافأة</th>
<th>المبلغ</th>
<th>تاريخ المطالبة</th>
</tr>
</thead>
<tbody>
<?php foreach ($claims as $claim): ?>
<tr>
<td>
<?php if (!empty($claim['profiles'])): ?>
<div class="flex items-center gap-2">
<?php if (!empty($claim['profiles']['avatar_url'])): ?>
<img src="<?= View::e($claim['profiles']['avatar_url']) ?>" alt="" style="width:28px;height:28px;border-radius:50%;">
<?php endif; ?>
<span><?= View::e($claim['profiles']['display_name'] ?? $claim['profiles']['username'] ?? '-') ?></span>
</div>
<?php else: ?>
<span class="text-muted"><?= View::e($claim['player_id'] ?? '-') ?></span>
<?php endif; ?>
</td>
<td><?= View::e($claim['org_loyalty_rewards']['name_ar'] ?? $claim['org_loyalty_rewards']['name'] ?? '-') ?></td>
<td>
<?php
$rewardTypeLabels = ['coins' => 'عملات', 'gems' => 'جواهر', 'frame' => 'إطار', 'achievement' => 'إنجاز', 'title' => 'لقب'];
$rType = $claim['org_loyalty_rewards']['reward_type'] ?? '';
?>
<span class="badge badge-default"><?= $rewardTypeLabels[$rType] ?? $rType ?></span>
</td>
<td><?= number_format((int)($claim['org_loyalty_rewards']['reward_amount'] ?? 0)) ?></td>
<td><?= !empty($claim['claimed_at']) ? date('Y-m-d H:i', strtotime($claim['claimed_at'])) : '-' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($pagination->totalPages > 1): ?>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination"><?= $pagination->links() ?></div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
$isEdit = !empty($reward);
$actionUrl = $isEdit
? "/organizations/{$org['id']}/loyalty/{$reward['id']}/update"
: "/organizations/{$org['id']}/loyalty/store";
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/loyalty" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= View::e($pageTitle) ?></h1>
</div>
</div>
<div class="card max-w-lg">
<form method="POST" action="<?= $actionUrl ?>" data-validate>
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">الاسم (English)</label>
<input type="text" name="name" class="form-input" dir="ltr" value="<?= View::e($reward['name'] ?? '') ?>" required>
</div>
<div class="form-group">
<label class="form-label">الاسم (عربي)</label>
<input type="text" name="name_ar" class="form-input" value="<?= View::e($reward['name_ar'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-input" rows="3"><?= View::e($reward['description'] ?? '') ?></textarea>
</div>
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">الأيام المطلوبة</label>
<input type="number" name="days_required" class="form-input" min="1" value="<?= (int)($reward['days_required'] ?? 0) ?>" required>
<span class="form-hint">عدد أيام العضوية المطلوبة للحصول على المكافأة</span>
</div>
<div class="form-group">
<label class="form-label">نوع المكافأة</label>
<select name="reward_type" class="form-input" id="reward-type-select" required>
<?php
$types = ['coins' => 'عملات', 'gems' => 'جواهر', 'frame' => 'إطار', 'achievement' => 'إنجاز', 'title' => 'لقب'];
$currentType = $reward['reward_type'] ?? 'coins';
foreach ($types as $val => $label): ?>
<option value="<?= $val ?>" <?= $currentType === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">مبلغ المكافأة</label>
<input type="number" name="reward_amount" class="form-input" min="0" value="<?= (int)($reward['reward_amount'] ?? 0) ?>">
</div>
<div class="form-group" id="frame-field" style="display: <?= $currentType === 'frame' ? 'block' : 'none' ?>;">
<label class="form-label">الإطار</label>
<select name="reward_frame_id" class="form-input">
<option value="">-- اختر إطار --</option>
<?php if (!empty($frames)): ?>
<?php foreach ($frames as $frame): ?>
<option value="<?= $frame['id'] ?>" <?= ($reward['reward_frame_id'] ?? '') == $frame['id'] ? 'selected' : '' ?>><?= View::e($frame['name_ar'] ?? $frame['name'] ?? $frame['id']) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="form-group" id="title-field" style="display: <?= $currentType === 'title' ? 'block' : 'none' ?>;">
<label class="form-label">اللقب</label>
<input type="text" name="reward_title" class="form-input" value="<?= View::e($reward['reward_title'] ?? '') ?>">
</div>
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">الترتيب</label>
<input type="number" name="sort_order" class="form-input" min="0" value="<?= (int)($reward['sort_order'] ?? 0) ?>">
</div>
<div class="form-group" style="display:flex;align-items:center;padding-top:24px;">
<label class="form-checkbox">
<input type="checkbox" name="is_active" value="1" <?= ($reward['is_active'] ?? true) ? 'checked' : '' ?>>
<span class="checkbox-mark"></span>
<span class="checkbox-label">مفعّلة</span>
</label>
</div>
</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'] ?>/loyalty" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
<script>
document.getElementById('reward-type-select').addEventListener('change', function() {
var type = this.value;
document.getElementById('frame-field').style.display = type === 'frame' ? 'block' : 'none';
document.getElementById('title-field').style.display = type === 'title' ? 'block' : 'none';
});
</script>
<?php
$rewardTypeLabels = [
'coins' => 'عملات',
'gems' => 'جواهر',
'frame' => 'إطار',
'achievement' => 'إنجاز',
'title' => 'لقب',
];
$rewardTypeBadges = [
'coins' => 'badge-warning',
'gems' => 'badge-purple',
'frame' => 'badge-info',
'achievement' => 'badge-success',
'title' => 'badge-default',
];
?>
<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($pageTitle) ?></h1>
</div>
<div class="flex gap-2">
<a href="/organizations/<?= $org['id'] ?>/loyalty/claims" class="btn btn-ghost">المطالبات</a>
<a href="/organizations/<?= $org['id'] ?>/loyalty/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>
<?php if (empty($rewards)): ?>
<div class="card">
<div class="empty-state">
<h3 class="empty-state-title">لا توجد مكافآت ولاء</h3>
<p class="empty-state-text">لم يتم إنشاء أي مكافآت ولاء لهذه المنظمة بعد</p>
</div>
</div>
<?php else: ?>
<div class="card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>الاسم</th>
<th>الأيام المطلوبة</th>
<th>نوع المكافأة</th>
<th>المبلغ</th>
<th>الحالة</th>
<th>الترتيب</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rewards as $reward): ?>
<tr>
<td>
<div>
<strong><?= View::e($reward['name_ar'] ?? $reward['name']) ?></strong>
<?php if (!empty($reward['name_ar']) && !empty($reward['name'])): ?>
<br><span class="text-sm text-muted"><?= View::e($reward['name']) ?></span>
<?php endif; ?>
</div>
</td>
<td><?= (int)($reward['days_required'] ?? 0) ?> يوم</td>
<td>
<?php $rType = $reward['reward_type'] ?? 'coins'; ?>
<span class="badge <?= $rewardTypeBadges[$rType] ?? 'badge-default' ?>"><?= $rewardTypeLabels[$rType] ?? $rType ?></span>
</td>
<td><?= number_format((int)($reward['reward_amount'] ?? 0)) ?></td>
<td>
<form method="POST" action="/organizations/<?= $org['id'] ?>/loyalty/<?= $reward['id'] ?>/toggle" style="margin:0;">
<?= Auth::csrfField() ?>
<label class="toggle">
<input type="checkbox" <?= ($reward['is_active'] ?? false) ? 'checked' : '' ?> onchange="this.closest('form').submit()">
<span class="toggle-track"></span>
</label>
</form>
</td>
<td><?= (int)($reward['sort_order'] ?? 0) ?></td>
<td>
<div class="flex gap-2">
<a href="/organizations/<?= $org['id'] ?>/loyalty/<?= $reward['id'] ?>/edit" class="btn btn-sm btn-ghost">تعديل</a>
<form method="POST" action="/organizations/<?= $org['id'] ?>/loyalty/<?= $reward['id'] ?>/delete" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من حذف هذه المكافأة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-sm btn-danger">حذف</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: var(--space-4);
}
.media-card {
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--border);
transition: transform 0.2s;
}
.media-card:hover {
transform: translateY(-2px);
}
.media-thumbnail {
height: 160px;
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.media-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.media-info {
padding: var(--space-3);
}
.media-size {
font-size: 0.75rem;
color: var(--text-muted);
}
<?php
class OrgMediaController
{
private Database $db;
private SupabaseStorage $storage;
public function __construct()
{
$this->db = Database::getInstance();
$this->storage = SupabaseStorage::getInstance();
}
public function gallery(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' => 'created_at.desc',
];
if (!empty($_GET['media_type'])) {
$filters['media_type'] = "eq.{$_GET['media_type']}";
}
if (!empty($_GET['album'])) {
$filters['album'] = "eq.{$_GET['album']}";
}
$total = $this->db->count('org_media', $filters);
$pagination = Pagination::fromRequest($total);
$filters['offset'] = $pagination->offset;
$filters['limit'] = $pagination->perPage;
$media = $this->db->select('org_media', $filters);
$pageTitle = 'الوسائط - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-media/gallery', compact('org', 'media', 'pagination', 'pageTitle'));
}
public function upload(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-media/upload', 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;
}
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
Response::error('يرجى اختيار ملف للرفع', "/organizations/{$orgId}/media");
return;
}
$file = $_FILES['file'];
$allowedTypes = ['image/*', 'video/*', 'application/pdf'];
$maxSize = 50 * 1024 * 1024; // 50MB
$validation = $this->storage->validateFile($file, $allowedTypes, $maxSize);
if ($validation !== true) {
Response::error($validation, "/organizations/{$orgId}/media");
return;
}
$mimeType = $file['type'];
if (str_starts_with($mimeType, 'image/')) {
$mediaType = 'image';
} elseif (str_starts_with($mimeType, 'video/')) {
$mediaType = 'video';
} else {
$mediaType = 'document';
}
$path = $this->storage->generatePath('org-media', $orgId, $file['name']);
$uploadResult = $this->storage->upload('org-media', $path, $file['tmp_name'], $mimeType);
if (!$uploadResult) {
Response::error('فشل رفع الملف', "/organizations/{$orgId}/media");
return;
}
$fileUrl = $this->storage->getPublicUrl('org-media', $path);
$user = Auth::user();
$data = [
'org_id' => $orgId,
'title' => trim($_POST['title'] ?? $file['name']),
'description' => trim($_POST['description'] ?? ''),
'file_url' => $fileUrl,
'thumbnail_url' => $mediaType === 'image' ? $fileUrl : null,
'media_type' => $mediaType,
'file_size' => $file['size'],
'mime_type' => $mimeType,
'album' => trim($_POST['album'] ?? ''),
'uploaded_by' => $user['id'],
'is_public' => isset($_POST['is_public']) ? true : false,
];
$result = $this->db->insert('org_media', $data);
AuditLog::log('upload', 'org_media', $result['id'] ?? null, null, $data);
Response::success('تم رفع الملف بنجاح', "/organizations/{$orgId}/media");
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$mediaId = $params['mediaId'];
$media = $this->db->selectOne('org_media', [
'id' => "eq.{$mediaId}",
'org_id' => "eq.{$orgId}",
]);
if (!$media) {
Response::error('الملف غير موجود', "/organizations/{$orgId}/media");
return;
}
// Extract path from URL and delete from storage
if (!empty($media['file_url'])) {
$urlParts = parse_url($media['file_url']);
$path = ltrim($urlParts['path'] ?? '', '/');
// Remove bucket prefix from path
$path = preg_replace('#^storage/v1/object/public/org-media/#', '', $path);
$this->storage->delete('org-media', $path);
}
$this->db->delete('org_media', ['id' => "eq.{$mediaId}"]);
AuditLog::log('delete', 'org_media', $mediaId, $media, null);
Response::success('تم حذف الملف', "/organizations/{$orgId}/media");
}
public function bulkDelete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
if (empty($_POST['ids']) || !is_array($_POST['ids'])) {
Response::error('يرجى تحديد الملفات للحذف', "/organizations/{$orgId}/media");
return;
}
$deletedCount = 0;
foreach ($_POST['ids'] as $mediaId) {
$media = $this->db->selectOne('org_media', [
'id' => "eq.{$mediaId}",
'org_id' => "eq.{$orgId}",
]);
if (!$media) {
continue;
}
// Delete from storage
if (!empty($media['file_url'])) {
$urlParts = parse_url($media['file_url']);
$path = ltrim($urlParts['path'] ?? '', '/');
$path = preg_replace('#^storage/v1/object/public/org-media/#', '', $path);
$this->storage->delete('org-media', $path);
}
$this->db->delete('org_media', ['id' => "eq.{$mediaId}"]);
AuditLog::log('delete', 'org_media', $mediaId, $media, null);
$deletedCount++;
}
Response::success("تم حذف {$deletedCount} ملف بنجاح", "/organizations/{$orgId}/media");
}
}
<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'] ?>/media/upload" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
رفع ملف
</a>
</div>
<div class="card mb-5">
<form method="GET" class="flex gap-4 items-end flex-wrap">
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">نوع الوسائط</label>
<select name="media_type" class="form-input">
<option value="">الكل</option>
<option value="image" <?= ($_GET['media_type'] ?? '') === 'image' ? 'selected' : '' ?>>صور</option>
<option value="video" <?= ($_GET['media_type'] ?? '') === 'video' ? 'selected' : '' ?>>فيديو</option>
<option value="document" <?= ($_GET['media_type'] ?? '') === 'document' ? 'selected' : '' ?>>مستندات</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">الألبوم</label>
<input type="text" name="album" class="form-input" value="<?= View::e($_GET['album'] ?? '') ?>" placeholder="اسم الألبوم">
</div>
<button type="submit" class="btn btn-primary btn-sm">تصفية</button>
<a href="/organizations/<?= $org['id'] ?>/media" class="btn btn-ghost btn-sm">إعادة تعيين</a>
</form>
</div>
<?php if (empty($media)): ?>
<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"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<h3 class="empty-state-title">لا توجد وسائط</h3>
<p class="empty-state-text">لم يتم رفع أي ملفات لهذه المنظمة بعد</p>
<a href="/organizations/<?= $org['id'] ?>/media/upload" class="btn btn-primary">رفع ملف</a>
</div>
</div>
<?php else: ?>
<form method="POST" action="/organizations/<?= $org['id'] ?>/media/bulk-delete" id="bulkDeleteForm">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="flex items-center gap-3 mb-4">
<button type="submit" class="btn btn-danger btn-sm" disabled id="bulkDeleteBtn">حذف المحدد</button>
<span class="text-xs text-muted" id="selectedCount"></span>
</div>
<div class="grid grid-3 stagger mb-5">
<?php foreach ($media as $item): ?>
<div class="card card-hover">
<div style="height:160px;background:var(--bg-secondary);display:flex;align-items:center;justify-content:center;border-radius:8px 8px 0 0;overflow:hidden;">
<?php if ($item['media_type'] === 'image' && !empty($item['thumbnail_url'])): ?>
<img src="<?= View::e($item['thumbnail_url']) ?>" style="max-width:100%;max-height:100%;object-fit:contain;" alt="<?= View::e($item['title']) ?>">
<?php elseif ($item['media_type'] === 'video'): ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity:0.5;"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<?php else: ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity:0.5;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<?php endif; ?>
</div>
<div class="p-4">
<div class="flex items-start justify-between mb-2">
<h4 class="font-semibold text-sm" style="word-break:break-word;"><?= View::e($item['title']) ?></h4>
<label class="toggle" style="margin-right:8px;">
<input type="checkbox" name="ids[]" value="<?= View::e($item['id']) ?>" class="bulk-checkbox">
</label>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<?php
$typeBadges = ['image' => 'badge-info', 'video' => 'badge-purple', 'document' => 'badge-warning'];
$typeLabels = ['image' => 'صورة', 'video' => 'فيديو', 'document' => 'مستند'];
$badgeClass = $typeBadges[$item['media_type']] ?? 'badge-default';
$typeLabel = $typeLabels[$item['media_type']] ?? $item['media_type'];
?>
<span class="badge <?= $badgeClass ?>"><?= $typeLabel ?></span>
<?php if (!empty($item['album'])): ?>
<span class="badge badge-default"><?= View::e($item['album']) ?></span>
<?php endif; ?>
</div>
<div class="text-xs text-muted mb-3">
<?php
$size = $item['file_size'] ?? 0;
if ($size >= 1048576) {
$sizeFormatted = number_format($size / 1048576, 1) . ' MB';
} else {
$sizeFormatted = number_format($size / 1024, 1) . ' KB';
}
?>
<span><?= $sizeFormatted ?></span>
<?php if (!empty($item['uploaded_by'])): ?>
&middot; <span><?= View::e(substr($item['uploaded_by'], 0, 8)) ?>...</span>
<?php endif; ?>
<br>
<span><?= date('Y-m-d H:i', strtotime($item['created_at'])) ?></span>
</div>
<div class="flex items-center justify-end">
<button class="btn btn-ghost btn-sm" style="color:var(--danger);" onclick="confirmDelete('/organizations/<?= $org['id'] ?>/media/<?= $item['id'] ?>/delete', '<?= View::e($item['title']) ?>')">حذف</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</form>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?><?= !empty($_GET['media_type']) ? '&media_type=' . urlencode($_GET['media_type']) : '' ?><?= !empty($_GET['album']) ? '&album=' . urlencode($_GET['album']) : '' ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('.bulk-checkbox');
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
const selectedCount = document.getElementById('selectedCount');
const bulkDeleteForm = document.getElementById('bulkDeleteForm');
function updateBulkState() {
const checked = document.querySelectorAll('.bulk-checkbox:checked');
bulkDeleteBtn.disabled = checked.length === 0;
selectedCount.textContent = checked.length > 0 ? checked.length + ' محدد' : '';
}
checkboxes.forEach(function(cb) {
cb.addEventListener('change', updateBulkState);
});
if (bulkDeleteForm) {
bulkDeleteForm.addEventListener('submit', function(e) {
const checked = document.querySelectorAll('.bulk-checkbox:checked');
if (checked.length === 0) {
e.preventDefault();
return;
}
if (!confirm('هل أنت متأكد من حذف ' + checked.length + ' ملف؟')) {
e.preventDefault();
}
});
}
});
</script>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/media" 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>
<div class="card max-w-lg">
<form method="POST" action="/organizations/<?= $org['id'] ?>/media/store" enctype="multipart/form-data" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group">
<label class="form-label">العنوان</label>
<input type="text" name="title" class="form-input" placeholder="عنوان الملف (اختياري)" value="<?= View::e($_POST['title'] ?? '') ?>">
<span class="form-hint">اتركه فارغاً لاستخدام اسم الملف.</span>
</div>
<div class="form-group">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-input" rows="3" placeholder="وصف مختصر للملف..."><?= View::e($_POST['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">الألبوم</label>
<input type="text" name="album" class="form-input" value="<?= View::e($_POST['album'] ?? 'general') ?>" placeholder="اسم الألبوم">
<span class="form-hint">يمكنك تصنيف الملفات حسب الألبومات.</span>
</div>
<div class="form-group">
<label class="form-label">الملف <span style="color:var(--danger);">*</span></label>
<input type="file" name="file" class="form-input" accept="image/*,video/*" required>
<span class="form-hint">الأنواع المسموحة: صور (JPG, PNG, GIF, WebP) وفيديو (MP4, WebM). الحد الأقصى: 50 ميجابايت.</span>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="is_public" value="1" <?= !empty($_POST['is_public']) ? '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">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<span class="btn-text">رفع</span>
<span class="btn-spinner"></span>
</button>
<a href="/organizations/<?= $org['id'] ?>/media" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
.partnership-orgs {
display: flex;
align-items: center;
gap: var(--space-3);
}
.partnership-connector {
width: 40px;
text-align: center;
color: var(--text-muted);
font-size: 1.2rem;
}
<?php
class OrgPartnershipsController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$queryParams = [
'select' => '*, org_a:el3ab_organizations!org_a_id(id,name,name_ar,logo_url), org_b:el3ab_organizations!org_b_id(id,name,name_ar,logo_url)',
'order' => 'created_at.desc',
];
if (!empty($_GET['status'])) {
$queryParams['status'] = 'eq.' . $_GET['status'];
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_partnerships', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$partnerships = $this->db->select('org_partnerships', $queryParams);
$pageTitle = 'الشراكات بين المنظمات';
View::render('org-partnerships/list', compact('partnerships', 'pagination', 'pageTitle'));
}
public function show(array $params, string $method): void
{
$id = $params['id'];
$partnership = $this->db->selectOne('org_partnerships', ['id' => "eq.{$id}"]);
if (!$partnership) {
http_response_code(404);
View::render('errors/404');
return;
}
$orgA = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$partnership['org_a_id']}"]);
$orgB = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$partnership['org_b_id']}"]);
$pageTitle = 'تفاصيل الشراكة';
View::render('org-partnerships/show', compact('partnership', 'orgA', 'orgB', 'pageTitle'));
}
public function initiate(array $params, string $method): void
{
Auth::requireCsrf();
$orgAId = $_POST['org_a_id'] ?? '';
$orgBId = $_POST['org_b_id'] ?? '';
if (empty($orgAId) || empty($orgBId)) {
Response::error('يجب تحديد المنظمتين', '/org-partnerships');
return;
}
if ($orgAId === $orgBId) {
Response::error('لا يمكن إنشاء شراكة مع نفس المنظمة', '/org-partnerships');
return;
}
$user = Auth::user();
$data = [
'org_a_id' => $orgAId,
'org_b_id' => $orgBId,
'type' => $_POST['type'] ?? 'collaboration',
'terms' => trim($_POST['terms'] ?? ''),
'benefits' => !empty($_POST['benefits']) ? json_encode($_POST['benefits']) : null,
'status' => 'pending',
'initiated_by' => $user['id'],
];
$result = $this->db->insert('org_partnerships', $data);
AuditLog::log('create', 'org_partnership', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء طلب الشراكة بنجاح', '/org-partnerships');
}
public function approve(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$partnership = $this->db->selectOne('org_partnerships', ['id' => "eq.{$id}"]);
if (!$partnership) {
Response::error('الشراكة غير موجودة', '/org-partnerships');
return;
}
if ($partnership['status'] !== 'pending') {
Response::error('لا يمكن قبول هذه الشراكة', '/org-partnerships');
return;
}
$user = Auth::user();
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'active',
'started_at' => date('c'),
'accepted_by' => $user['id'],
'updated_at' => date('c'),
]);
AuditLog::log('approve', 'org_partnership', $id, $partnership, ['status' => 'active']);
Response::success('تم قبول الشراكة', '/org-partnerships');
}
public function reject(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$partnership = $this->db->selectOne('org_partnerships', ['id' => "eq.{$id}"]);
if (!$partnership) {
Response::error('الشراكة غير موجودة', '/org-partnerships');
return;
}
if ($partnership['status'] !== 'pending') {
Response::error('لا يمكن رفض هذه الشراكة', '/org-partnerships');
return;
}
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'rejected',
'updated_at' => date('c'),
]);
AuditLog::log('reject', 'org_partnership', $id, $partnership, ['status' => 'rejected']);
Response::success('تم رفض الشراكة', '/org-partnerships');
}
public function dissolve(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$partnership = $this->db->selectOne('org_partnerships', ['id' => "eq.{$id}"]);
if (!$partnership) {
Response::error('الشراكة غير موجودة', '/org-partnerships');
return;
}
if ($partnership['status'] !== 'active') {
Response::error('لا يمكن حل شراكة غير نشطة', '/org-partnerships');
return;
}
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'dissolved',
'ended_at' => date('c'),
'updated_at' => date('c'),
]);
AuditLog::log('dissolve', 'org_partnership', $id, $partnership, ['status' => 'dissolved']);
Response::success('تم حل الشراكة', '/org-partnerships');
}
}
<?php
$statusLabels = [
'pending' => 'معلقة',
'active' => 'نشطة',
'dissolved' => 'منحلة',
'rejected' => 'مرفوضة',
];
$statusBadges = [
'pending' => 'badge-warning',
'active' => 'badge-success',
'dissolved' => 'badge-default',
'rejected' => 'badge-default',
];
$typeLabels = [
'alliance' => 'تحالف',
'partnership' => 'شراكة',
'sister_org' => 'منظمة شقيقة',
'sponsor' => 'راعي',
];
$typeBadges = [
'alliance' => 'badge-info',
'partnership' => 'badge-purple',
'sister_org' => 'badge-success',
'sponsor' => 'badge-warning',
];
?>
<div class="content-header">
<h1><?= View::e($pageTitle) ?></h1>
</div>
<div class="card mb-5">
<form method="GET" class="flex gap-4 items-end flex-wrap">
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">الحالة</label>
<select name="status" class="form-input" onchange="this.form.submit()">
<option value="">الكل</option>
<?php foreach ($statusLabels as $key => $label): ?>
<option value="<?= $key ?>" <?= ($_GET['status'] ?? '') === $key ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
<?php if (empty($partnerships)): ?>
<div class="card">
<div class="empty-state">
<h3 class="empty-state-title">لا توجد شراكات</h3>
<p class="empty-state-text">لم يتم إنشاء أي شراكات بين المنظمات بعد</p>
</div>
</div>
<?php else: ?>
<div class="card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>المنظمة (أ)</th>
<th>المنظمة (ب)</th>
<th>النوع</th>
<th>الحالة</th>
<th>تاريخ البدء</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($partnerships as $partnership): ?>
<tr>
<td><?= View::e($partnership['org_a']['name_ar'] ?? $partnership['org_a']['name'] ?? '-') ?></td>
<td><?= View::e($partnership['org_b']['name_ar'] ?? $partnership['org_b']['name'] ?? '-') ?></td>
<td>
<?php $type = $partnership['type'] ?? 'partnership'; ?>
<span class="badge <?= $typeBadges[$type] ?? 'badge-default' ?>"><?= $typeLabels[$type] ?? $type ?></span>
</td>
<td>
<?php $status = $partnership['status'] ?? 'pending'; ?>
<span class="badge <?= $statusBadges[$status] ?? 'badge-default' ?>"><?= $statusLabels[$status] ?? $status ?></span>
</td>
<td>
<?= !empty($partnership['started_at']) ? date('Y-m-d', strtotime($partnership['started_at'])) : '-' ?>
</td>
<td>
<div class="flex gap-2">
<a href="/org-partnerships/<?= $partnership['id'] ?>" class="btn btn-sm btn-ghost">عرض</a>
<?php if ($status === 'pending'): ?>
<form method="POST" action="/org-partnerships/<?= $partnership['id'] ?>/approve" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من قبول هذه الشراكة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-sm btn-primary">قبول</button>
</form>
<form method="POST" action="/org-partnerships/<?= $partnership['id'] ?>/reject" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من رفض هذه الشراكة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-sm btn-danger">رفض</button>
</form>
<?php elseif ($status === 'active'): ?>
<form method="POST" action="/org-partnerships/<?= $partnership['id'] ?>/dissolve" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من حل هذه الشراكة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-sm btn-danger">حل الشراكة</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($pagination->totalPages > 1): ?>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination"><?= $pagination->links() ?></div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
$statusLabels = [
'pending' => 'معلقة',
'active' => 'نشطة',
'dissolved' => 'منحلة',
'rejected' => 'مرفوضة',
];
$statusBadges = [
'pending' => 'badge-warning',
'active' => 'badge-success',
'dissolved' => 'badge-default',
'rejected' => 'badge-default',
];
$typeLabels = [
'alliance' => 'تحالف',
'partnership' => 'شراكة',
'sister_org' => 'منظمة شقيقة',
'sponsor' => 'راعي',
];
$typeBadges = [
'alliance' => 'badge-info',
'partnership' => 'badge-purple',
'sister_org' => 'badge-success',
'sponsor' => 'badge-warning',
];
$status = $partnership['status'] ?? 'pending';
$type = $partnership['type'] ?? 'partnership';
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-partnerships" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= View::e($pageTitle) ?></h1>
<span class="badge <?= $statusBadges[$status] ?? 'badge-default' ?>"><?= $statusLabels[$status] ?? $status ?></span>
</div>
</div>
<!-- Organizations -->
<div class="grid grid-2 gap-4 mb-4">
<div class="card">
<p class="text-sm text-muted mb-2">المنظمة (أ)</p>
<div class="flex items-center gap-3">
<?php if (!empty($orgA['logo_url'])): ?>
<img src="<?= View::e($orgA['logo_url']) ?>" alt="" style="width:40px;height:40px;border-radius:6px;">
<?php endif; ?>
<h3><?= View::e($orgA['name_ar'] ?? $orgA['name'] ?? '-') ?></h3>
</div>
</div>
<div class="card">
<p class="text-sm text-muted mb-2">المنظمة (ب)</p>
<div class="flex items-center gap-3">
<?php if (!empty($orgB['logo_url'])): ?>
<img src="<?= View::e($orgB['logo_url']) ?>" alt="" style="width:40px;height:40px;border-radius:6px;">
<?php endif; ?>
<h3><?= View::e($orgB['name_ar'] ?? $orgB['name'] ?? '-') ?></h3>
</div>
</div>
</div>
<!-- Partnership Details -->
<div class="card mb-4">
<h3 style="margin-bottom: 12px;">تفاصيل الشراكة</h3>
<div class="grid grid-2 gap-4">
<div>
<p class="text-sm text-muted">النوع</p>
<span class="badge <?= $typeBadges[$type] ?? 'badge-default' ?>"><?= $typeLabels[$type] ?? $type ?></span>
</div>
<div>
<p class="text-sm text-muted">الحالة</p>
<span class="badge <?= $statusBadges[$status] ?? 'badge-default' ?>"><?= $statusLabels[$status] ?? $status ?></span>
</div>
<div>
<p class="text-sm text-muted">بدأها</p>
<p><?= View::e($partnership['initiated_by'] ?? '-') ?></p>
</div>
<div>
<p class="text-sm text-muted">قبلها</p>
<p><?= View::e($partnership['accepted_by'] ?? '-') ?></p>
</div>
<div>
<p class="text-sm text-muted">تاريخ البدء</p>
<p><?= !empty($partnership['started_at']) ? date('Y-m-d H:i', strtotime($partnership['started_at'])) : '-' ?></p>
</div>
<div>
<p class="text-sm text-muted">تاريخ الانتهاء</p>
<p><?= !empty($partnership['ended_at']) ? date('Y-m-d H:i', strtotime($partnership['ended_at'])) : '-' ?></p>
</div>
</div>
</div>
<!-- Terms -->
<?php if (!empty($partnership['terms'])): ?>
<div class="card mb-4">
<h3 style="margin-bottom: 12px;">الشروط</h3>
<p><?= nl2br(View::e($partnership['terms'])) ?></p>
</div>
<?php endif; ?>
<!-- Benefits -->
<?php
$benefits = null;
if (!empty($partnership['benefits'])) {
$benefits = is_string($partnership['benefits']) ? json_decode($partnership['benefits'], true) : $partnership['benefits'];
}
?>
<?php if (!empty($benefits) && is_array($benefits)): ?>
<div class="card mb-4">
<h3 style="margin-bottom: 12px;">الفوائد</h3>
<ul style="list-style: disc; padding-right: 20px;">
<?php foreach ($benefits as $benefit): ?>
<li style="padding: 4px 0;"><?= View::e(is_string($benefit) ? $benefit : json_encode($benefit)) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- Actions -->
<?php if ($status === 'pending' || $status === 'active'): ?>
<div class="card">
<h3 style="margin-bottom: 16px;">الإجراءات</h3>
<div class="flex gap-2 mt-4">
<?php if ($status === 'pending'): ?>
<form method="POST" action="/org-partnerships/<?= $partnership['id'] ?>/approve" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من قبول هذه الشراكة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-primary">قبول الشراكة</button>
</form>
<form method="POST" action="/org-partnerships/<?= $partnership['id'] ?>/reject" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من رفض هذه الشراكة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger">رفض الشراكة</button>
</form>
<?php elseif ($status === 'active'): ?>
<form method="POST" action="/org-partnerships/<?= $partnership['id'] ?>/dissolve" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من حل هذه الشراكة؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger">حل الشراكة</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
.featured-star {
color: #f59e0b;
font-size: 1.1rem;
}
.recruitment-requirements {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
font-size: 0.8rem;
}
.recruitment-requirements span {
background: var(--bg-secondary);
padding: 2px 8px;
border-radius: var(--radius-sm);
}
<?php
class OrgRecruitmentController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function list(array $params, string $method): void
{
$status = $_GET['status'] ?? '';
$queryParams = ['select' => '*', 'order' => 'is_featured.desc,created_at.desc'];
if ($status) {
$queryParams['status'] = "eq.{$status}";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_recruitment_posts', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$posts = $this->db->select('org_recruitment_posts', $queryParams);
$orgIds = array_column($posts, 'org_id');
$orgs = [];
if (!empty($orgIds)) {
$idList = '(' . implode(',', array_unique($orgIds)) . ')';
foreach ($this->db->select('el3ab_organizations', ['select' => 'id,name,name_ar,logo_url', 'id' => "in.{$idList}"]) as $o) {
$orgs[$o['id']] = $o;
}
}
$pageTitle = 'لوحة التوظيف';
$moduleCSS = 'org-recruitment';
View::render('org-recruitment/list', compact('posts', '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',
]);
$post = [];
$pageTitle = 'إنشاء إعلان توظيف';
$moduleCSS = 'org-recruitment';
View::render('org-recruitment/form', compact('post', 'organizations', 'pageTitle', 'moduleCSS'));
}
public function store(array $params, string $method): void
{
Auth::requireCsrf();
$title = trim($_POST['title'] ?? '');
$orgId = $_POST['org_id'] ?? '';
if (empty($title) || empty($orgId)) {
Response::error('العنوان والمنظمة مطلوبان', '/org-recruitment/create');
return;
}
$data = [
'org_id' => $orgId,
'title' => $title,
'title_ar' => trim($_POST['title_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'game_key' => $_POST['game_key'] ?? null,
'positions_available' => (int)($_POST['positions_available'] ?? 1),
'status' => 'open',
'requirements' => json_encode([
'min_elo' => (int)($_POST['min_elo'] ?? 0),
'min_level' => (int)($_POST['min_level'] ?? 0),
'country' => $_POST['required_country'] ?? '',
]),
];
if (!empty($_POST['expires_at'])) {
$data['expires_at'] = $_POST['expires_at'];
}
$result = $this->db->insert('org_recruitment_posts', $data);
AuditLog::log('create', 'org_recruitment', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء إعلان التوظيف', '/org-recruitment');
}
public function edit(array $params, string $method): void
{
$id = $params['id'];
$post = $this->db->selectOne('org_recruitment_posts', ['id' => "eq.{$id}"]);
if (!$post) {
Response::error('الإعلان غير موجود', '/org-recruitment');
return;
}
$organizations = $this->db->select('el3ab_organizations', [
'select' => 'id,name,name_ar',
'is_active' => 'eq.true',
'order' => 'name.asc',
]);
$pageTitle = 'تعديل إعلان التوظيف';
$moduleCSS = 'org-recruitment';
View::render('org-recruitment/form', compact('post', 'organizations', 'pageTitle', 'moduleCSS'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$data = [
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'description_ar' => trim($_POST['description_ar'] ?? ''),
'game_key' => $_POST['game_key'] ?? null,
'positions_available' => (int)($_POST['positions_available'] ?? 1),
'requirements' => json_encode([
'min_elo' => (int)($_POST['min_elo'] ?? 0),
'min_level' => (int)($_POST['min_level'] ?? 0),
'country' => $_POST['required_country'] ?? '',
]),
'updated_at' => date('c'),
];
if (!empty($_POST['expires_at'])) {
$data['expires_at'] = $_POST['expires_at'];
}
$this->db->update('org_recruitment_posts', ['id' => "eq.{$id}"], $data);
AuditLog::log('update', 'org_recruitment', $id, null, $data);
Response::success('تم تحديث الإعلان', '/org-recruitment');
}
public function close(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_recruitment_posts', ['id' => "eq.{$id}"], ['status' => 'closed', 'updated_at' => date('c')]);
AuditLog::log('close', 'org_recruitment', $id);
Response::success('تم إغلاق الإعلان', '/org-recruitment');
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->delete('org_recruitment_posts', ['id' => "eq.{$id}"]);
AuditLog::log('delete', 'org_recruitment', $id);
Response::success('تم حذف الإعلان', '/org-recruitment');
}
public function toggleFeatured(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$post = $this->db->selectOne('org_recruitment_posts', ['id' => "eq.{$id}"]);
if (!$post) {
Response::error('الإعلان غير موجود', '/org-recruitment');
return;
}
$newFeatured = !($post['is_featured'] ?? false);
$this->db->update('org_recruitment_posts', ['id' => "eq.{$id}"], ['is_featured' => $newFeatured, 'updated_at' => date('c')]);
Response::success($newFeatured ? 'تم تمييز الإعلان' : 'تم إلغاء تمييز الإعلان', '/org-recruitment');
}
}
<?php
$isEdit = !empty($post);
$formAction = $isEdit ? "/org-recruitment/{$post['id']}/update" : '/org-recruitment/store';
$requirements = [];
if ($isEdit && !empty($post['requirements'])) {
$requirements = is_string($post['requirements']) ? json_decode($post['requirements'], true) : $post['requirements'];
}
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/org-recruitment" class="btn btn-icon btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<h1><?= View::e($pageTitle) ?></h1>
</div>
</div>
<div class="card max-w-lg">
<form method="POST" action="<?= $formAction ?>" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group">
<label class="form-label">المنظمة *</label>
<select name="org_id" class="form-input" required>
<option value="">اختر المنظمة</option>
<?php foreach ($organizations as $org): ?>
<option value="<?= $org['id'] ?>" <?= ($isEdit && ($post['org_id'] ?? '') == $org['id']) ? 'selected' : '' ?>>
<?= View::e($org['name_ar'] ?? $org['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">العنوان (English) *</label>
<input type="text" name="title" class="form-input" dir="ltr" required value="<?= View::e($post['title'] ?? '') ?>" placeholder="Recruitment post title">
</div>
<div class="form-group">
<label class="form-label">العنوان (عربي)</label>
<input type="text" name="title_ar" class="form-input" value="<?= View::e($post['title_ar'] ?? '') ?>" placeholder="عنوان إعلان التوظيف">
</div>
<div class="form-group">
<label class="form-label">الوصف (English)</label>
<textarea name="description" class="form-input" rows="4" dir="ltr" placeholder="Post description..."><?= View::e($post['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">الوصف (عربي)</label>
<textarea name="description_ar" class="form-input" rows="4" placeholder="وصف الإعلان..."><?= View::e($post['description_ar'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">اللعبة</label>
<input type="text" name="game_key" class="form-input" dir="ltr" value="<?= View::e($post['game_key'] ?? '') ?>" placeholder="e.g. chess, ludo">
<span class="form-hint">مفتاح اللعبة المرتبطة بالإعلان.</span>
</div>
<div class="form-group">
<label class="form-label">عدد المقاعد المتاحة</label>
<input type="number" name="positions_available" class="form-input" min="1" value="<?= View::e($post['positions_available'] ?? '1') ?>">
</div>
<div class="form-group">
<label class="form-label">الحد الأدنى للتقييم (Elo)</label>
<input type="number" name="min_elo" class="form-input" min="0" value="<?= View::e($requirements['min_elo'] ?? '0') ?>" dir="ltr">
<span class="form-hint">اتركه 0 لعدم وجود حد أدنى.</span>
</div>
<div class="form-group">
<label class="form-label">الحد الأدنى للمستوى</label>
<input type="number" name="min_level" class="form-input" min="0" value="<?= View::e($requirements['min_level'] ?? '0') ?>" dir="ltr">
<span class="form-hint">اتركه 0 لعدم وجود حد أدنى.</span>
</div>
<div class="form-group">
<label class="form-label">الدولة المطلوبة</label>
<input type="text" name="required_country" class="form-input" value="<?= View::e($requirements['country'] ?? '') ?>" placeholder="اتركه فارغاً لقبول جميع الدول">
<span class="form-hint">كود الدولة (مثال: SA, EG). اتركه فارغاً للسماح لجميع الدول.</span>
</div>
<div class="form-group">
<label class="form-label">تاريخ انتهاء الصلاحية</label>
<input type="datetime-local" name="expires_at" class="form-input" dir="ltr" value="<?= !empty($post['expires_at']) ? date('Y-m-d\TH:i', strtotime($post['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="/org-recruitment" class="btn btn-ghost">إلغاء</a>
</div>
</form>
</div>
<div class="content-header">
<h1><?= View::e($pageTitle) ?></h1>
<a href="/org-recruitment/create" class="btn btn-primary">إعلان توظيف جديد</a>
</div>
<!-- Filter by status -->
<div class="card mb-5">
<form method="GET" class="flex gap-4 items-end flex-wrap">
<div class="filter-pills">
<a href="/org-recruitment" class="filter-pill <?= empty($status) ? 'active' : '' ?>">الكل</a>
<a href="/org-recruitment?status=open" class="filter-pill <?= $status === 'open' ? 'active' : '' ?>">مفتوح</a>
<a href="/org-recruitment?status=closed" class="filter-pill <?= $status === 'closed' ? 'active' : '' ?>">مغلق</a>
<a href="/org-recruitment?status=filled" class="filter-pill <?= $status === 'filled' ? 'active' : '' ?>">مكتمل</a>
<a href="/org-recruitment?status=expired" class="filter-pill <?= $status === 'expired' ? 'active' : '' ?>">منتهي</a>
</div>
</form>
</div>
<?php if (empty($posts)): ?>
<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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<h3 class="empty-state-title">لا توجد إعلانات توظيف</h3>
<p class="empty-state-text">لم يتم العثور على أي إعلانات<?= $status ? ' بهذه الحالة' : '' ?></p>
</div>
</div>
<?php else: ?>
<div class="card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>العنوان</th>
<th>المنظمة</th>
<th>اللعبة</th>
<th>المقاعد</th>
<th>الحالة</th>
<th>مميز</th>
<th>الطلبات</th>
<th>تاريخ الانتهاء</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php
$statusBadges = ['open' => 'success', 'closed' => 'default', 'filled' => 'info', 'expired' => 'warning'];
$statusLabels = ['open' => 'مفتوح', 'closed' => 'مغلق', 'filled' => 'مكتمل', 'expired' => 'منتهي'];
?>
<?php foreach ($posts as $post): ?>
<tr>
<td>
<div class="font-medium"><?= View::e($post['title_ar'] ?: $post['title']) ?></div>
<?php if ($post['title_ar'] && $post['title']): ?>
<div class="text-xs text-muted" dir="ltr"><?= View::e($post['title']) ?></div>
<?php endif; ?>
</td>
<td>
<?php $org = $orgs[$post['org_id']] ?? null; ?>
<?php if ($org): ?>
<?= View::e($org['name_ar'] ?? $org['name']) ?>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td><?= View::e($post['game_key'] ?? '-') ?></td>
<td><?= (int)($post['positions_available'] ?? 0) ?></td>
<td>
<span class="badge badge-<?= $statusBadges[$post['status']] ?? 'default' ?>">
<?= $statusLabels[$post['status']] ?? $post['status'] ?>
</span>
</td>
<td>
<?php if (!empty($post['is_featured'])): ?>
<span class="text-warning" title="مميز">&#9733;</span>
<?php else: ?>
<span class="text-muted">&#9734;</span>
<?php endif; ?>
</td>
<td>
<span class="badge badge-info"><?= (int)($post['applications_count'] ?? 0) ?></span>
</td>
<td class="text-xs tabular-nums">
<?= $post['expires_at'] ? date('Y-m-d H:i', strtotime($post['expires_at'])) : '-' ?>
</td>
<td>
<div class="flex items-center gap-2">
<a href="/org-recruitment/<?= $post['id'] ?>/edit" class="btn btn-ghost btn-sm" title="تعديل">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</a>
<?php if ($post['status'] === 'open'): ?>
<form method="POST" action="/org-recruitment/<?= $post['id'] ?>/close" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-warning btn-sm" title="إغلاق" onclick="return confirm('هل تريد إغلاق هذا الإعلان؟')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</button>
</form>
<?php endif; ?>
<form method="POST" action="/org-recruitment/<?= $post['id'] ?>/toggle-featured" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-ghost btn-sm" title="<?= !empty($post['is_featured']) ? 'إلغاء التمييز' : 'تمييز' ?>">
<?php if (!empty($post['is_featured'])): ?>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<?php else: ?>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<?php endif; ?>
</button>
</form>
<form method="POST" action="/org-recruitment/<?= $post['id'] ?>/delete" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-danger btn-sm" title="حذف" onclick="return confirm('هل أنت متأكد من حذف هذا الإعلان؟')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<a href="?page=<?= $pagination->page - 1 ?>&status=<?= urlencode($status) ?>" class="pagination-btn <?= !$pagination->hasPrev() ? 'disabled' : '' ?>" <?= !$pagination->hasPrev() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</a>
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&status=<?= urlencode($status) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
<a href="?page=<?= $pagination->page + 1 ?>&status=<?= urlencode($status) ?>" class="pagination-btn <?= !$pagination->hasNext() ? 'disabled' : '' ?>" <?= !$pagination->hasNext() ? 'disabled' : '' ?>>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</div>
</div>
<?php endif; ?>
.roster-position {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-weight: 600;
}
.jersey-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--bg-secondary);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.8rem;
}
<?php
class OrgRostersController
{
private Database $db;
public function __construct()
{
$this->db = Database::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;
}
$queryParams = [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'created_at.desc',
];
if (!empty($_GET['status'])) {
$queryParams['status'] = 'eq.' . $_GET['status'];
}
if (!empty($_GET['game_key'])) {
$queryParams['game_key'] = 'eq.' . $_GET['game_key'];
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_rosters', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$rosters = $this->db->select('org_rosters', $queryParams);
$pageTitle = 'القوائم - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-rosters/list', compact('org', 'rosters', '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;
}
$roster = [];
$pageTitle = 'إنشاء قائمة - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-rosters/form', compact('org', 'roster', '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;
}
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
Response::error('اسم القائمة مطلوب', "/organizations/{$orgId}/rosters/create");
return;
}
$data = [
'org_id' => $orgId,
'name' => $name,
'name_ar' => trim($_POST['name_ar'] ?? ''),
'game_key' => $_POST['game_key'] ?? '',
'status' => 'active',
];
$result = $this->db->insert('org_rosters', $data);
AuditLog::log('create', 'org_roster', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء القائمة بنجاح', "/organizations/{$orgId}/rosters");
}
public function show(array $params, string $method): void
{
$orgId = $params['id'];
$rosterId = $params['rosterId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$roster = $this->db->selectOne('org_rosters', [
'id' => "eq.{$rosterId}",
'org_id' => "eq.{$orgId}",
]);
if (!$roster) {
Response::error('القائمة غير موجودة', "/organizations/{$orgId}/rosters");
return;
}
$players = $this->db->select('org_roster_players', [
'select' => '*, profiles(id,username,display_name,avatar_url)',
'roster_id' => "eq.{$rosterId}",
'order' => 'jersey_number.asc',
]);
$pageTitle = 'القائمة: ' . ($roster['name_ar'] ?? $roster['name']);
View::render('org-rosters/show', compact('org', 'roster', 'players', 'pageTitle'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rosterId = $params['rosterId'];
$roster = $this->db->selectOne('org_rosters', [
'id' => "eq.{$rosterId}",
'org_id' => "eq.{$orgId}",
]);
if (!$roster) {
Response::error('القائمة غير موجودة', "/organizations/{$orgId}/rosters");
return;
}
$data = [
'name' => trim($_POST['name'] ?? ''),
'name_ar' => trim($_POST['name_ar'] ?? ''),
'game_key' => $_POST['game_key'] ?? $roster['game_key'],
'updated_at' => date('c'),
];
$this->db->update('org_rosters', ['id' => "eq.{$rosterId}"], $data);
AuditLog::log('update', 'org_roster', $rosterId, $roster, $data);
Response::success('تم تحديث القائمة', "/organizations/{$orgId}/rosters/{$rosterId}");
}
public function addPlayer(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rosterId = $params['rosterId'];
$roster = $this->db->selectOne('org_rosters', [
'id' => "eq.{$rosterId}",
'org_id' => "eq.{$orgId}",
]);
if (!$roster) {
Response::error('القائمة غير موجودة', "/organizations/{$orgId}/rosters");
return;
}
$playerId = $_POST['player_id'] ?? '';
if (empty($playerId)) {
Response::error('يجب تحديد اللاعب', "/organizations/{$orgId}/rosters/{$rosterId}");
return;
}
// Check if player already in roster
$existing = $this->db->selectOne('org_roster_players', [
'roster_id' => "eq.{$rosterId}",
'player_id' => "eq.{$playerId}",
]);
if ($existing) {
Response::error('اللاعب موجود في القائمة بالفعل', "/organizations/{$orgId}/rosters/{$rosterId}");
return;
}
$data = [
'roster_id' => $rosterId,
'player_id' => $playerId,
'position' => trim($_POST['position'] ?? ''),
'jersey_number' => !empty($_POST['jersey_number']) ? (int) $_POST['jersey_number'] : null,
'notes' => trim($_POST['notes'] ?? ''),
];
$this->db->insert('org_roster_players', $data);
AuditLog::log('add_player', 'org_roster_player', $rosterId, null, $data);
Response::success('تم إضافة اللاعب إلى القائمة', "/organizations/{$orgId}/rosters/{$rosterId}");
}
public function removePlayer(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rosterId = $params['rosterId'];
$playerId = $_POST['player_id'] ?? '';
if (empty($playerId)) {
Response::error('يجب تحديد اللاعب', "/organizations/{$orgId}/rosters/{$rosterId}");
return;
}
$existing = $this->db->selectOne('org_roster_players', [
'roster_id' => "eq.{$rosterId}",
'player_id' => "eq.{$playerId}",
]);
if (!$existing) {
Response::error('اللاعب غير موجود في القائمة', "/organizations/{$orgId}/rosters/{$rosterId}");
return;
}
$this->db->delete('org_roster_players', [
'roster_id' => "eq.{$rosterId}",
'player_id' => "eq.{$playerId}",
]);
AuditLog::log('remove_player', 'org_roster_player', $rosterId, $existing, ['player_id' => $playerId]);
Response::success('تم إزالة اللاعب من القائمة', "/organizations/{$orgId}/rosters/{$rosterId}");
}
public function archive(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$rosterId = $params['rosterId'];
$roster = $this->db->selectOne('org_rosters', [
'id' => "eq.{$rosterId}",
'org_id' => "eq.{$orgId}",
]);
if (!$roster) {
Response::error('القائمة غير موجودة', "/organizations/{$orgId}/rosters");
return;
}
$this->db->update('org_rosters', ['id' => "eq.{$rosterId}"], [
'status' => 'archived',
'updated_at' => date('c'),
]);
AuditLog::log('archive', 'org_roster', $rosterId, $roster, ['status' => 'archived']);
Response::success('تم أرشفة القائمة', "/organizations/{$orgId}/rosters");
}
}
<?php
$isEdit = !empty($roster);
$actionUrl = $isEdit
? "/organizations/{$org['id']}/rosters/{$roster['id']}/update"
: "/organizations/{$org['id']}/rosters/store";
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/rosters" 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="<?= $actionUrl ?>" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group">
<label class="form-label">المنظمة</label>
<input type="text" class="form-input" value="<?= View::e($org['name_ar'] ?? $org['name']) ?>" disabled>
<input type="hidden" name="org_id" value="<?= $org['id'] ?>">
</div>
<div class="form-group">
<label class="form-label">الاسم (English) *</label>
<input type="text" name="name" class="form-input" required value="<?= View::e($roster['name'] ?? '') ?>" placeholder="Roster name" dir="ltr">
</div>
<div class="form-group">
<label class="form-label">الاسم (عربي)</label>
<input type="text" name="name_ar" class="form-input" value="<?= View::e($roster['name_ar'] ?? '') ?>" placeholder="اسم القائمة">
</div>
<div class="form-group">
<label class="form-label">مفتاح اللعبة</label>
<input type="text" name="game_key" class="form-input" value="<?= View::e($roster['game_key'] ?? '') ?>" placeholder="chess, valorant, etc." dir="ltr">
<span class="form-hint">المعرف الخاص باللعبة المرتبطة بهذه القائمة</span>
</div>
<?php if ($isEdit): ?>
<div class="form-group">
<label class="form-label">الحالة</label>
<select name="status" class="form-input">
<option value="active" <?= ($roster['status'] ?? '') === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="archived" <?= ($roster['status'] ?? '') === 'archived' ? 'selected' : '' ?>>مؤرشف</option>
<option value="draft" <?= ($roster['status'] ?? '') === 'draft' ? 'selected' : '' ?>>مسودة</option>
</select>
</div>
<?php endif; ?>
<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'] ?>/rosters" 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'] ?>/rosters/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>
<input type="text" name="game_key" class="form-input" value="<?= View::e($_GET['game_key'] ?? '') ?>" placeholder="مفتاح اللعبة">
</div>
<div class="form-group mb-0">
<label class="form-label">الحالة</label>
<select name="status" class="form-input">
<option value="">الكل</option>
<option value="active" <?= ($_GET['status'] ?? '') === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="archived" <?= ($_GET['status'] ?? '') === 'archived' ? 'selected' : '' ?>>مؤرشف</option>
<option value="draft" <?= ($_GET['status'] ?? '') === 'draft' ? 'selected' : '' ?>>مسودة</option>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
<a href="/organizations/<?= $org['id'] ?>/rosters" class="btn btn-ghost">إعادة تعيين</a>
</form>
</div>
<div class="data-table-wrapper">
<?php if (empty($rosters)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<h3 class="empty-state-title">لا توجد قوائم</h3>
<p class="empty-state-text">لم يتم إنشاء أي قوائم لهذه المنظمة بعد</p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>الاسم</th>
<th>المنظمة</th>
<th>اللعبة</th>
<th>الحالة</th>
<th>عدد اللاعبين</th>
<th>تاريخ الإنشاء</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rosters as $roster): ?>
<tr>
<td>
<a href="/organizations/<?= $org['id'] ?>/rosters/<?= $roster['id'] ?>" class="font-medium">
<?= View::e($roster['name_ar'] ?? $roster['name']) ?>
</a>
</td>
<td><?= View::e($org['name_ar'] ?? $org['name']) ?></td>
<td><code><?= View::e($roster['game_key'] ?? '-') ?></code></td>
<td>
<?php
$statusBadges = ['active' => 'badge-success', 'archived' => 'badge-default', 'draft' => 'badge-warning'];
$statusLabels = ['active' => 'نشط', 'archived' => 'مؤرشف', 'draft' => 'مسودة'];
$status = $roster['status'] ?? 'draft';
?>
<span class="badge <?= $statusBadges[$status] ?? 'badge-default' ?>"><?= $statusLabels[$status] ?? $status ?></span>
</td>
<td><?= $roster['player_count'] ?? 0 ?></td>
<td class="tabular-nums"><?= date('Y-m-d', strtotime($roster['created_at'])) ?></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'] ?>/rosters/<?= $roster['id'] ?>" class="dropdown-item">عرض</a>
<a href="/organizations/<?= $org['id'] ?>/rosters/<?= $roster['id'] ?>/edit" class="dropdown-item">تعديل</a>
<?php if (($roster['status'] ?? '') !== 'archived'): ?>
<div class="dropdown-divider"></div>
<form method="POST" action="/organizations/<?= $org['id'] ?>/rosters/<?= $roster['id'] ?>/archive" style="margin:0;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="dropdown-item danger">أرشفة</button>
</form>
<?php endif; ?>
</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 ?>&status=<?= View::e($_GET['status'] ?? '') ?>&game_key=<?= View::e($_GET['game_key'] ?? '') ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/rosters" 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($roster['name_ar'] ?? $roster['name']) ?></h1>
<?php
$statusBadges = ['active' => 'badge-success', 'archived' => 'badge-default', 'draft' => 'badge-warning'];
$statusLabels = ['active' => 'نشط', 'archived' => 'مؤرشف', 'draft' => 'مسودة'];
$status = $roster['status'] ?? 'draft';
?>
<span class="badge <?= $statusBadges[$status] ?? 'badge-default' ?>"><?= $statusLabels[$status] ?? $status ?></span>
</div>
<a href="/organizations/<?= $org['id'] ?>/rosters/<?= $roster['id'] ?>/edit" class="btn btn-primary">تعديل</a>
</div>
<div class="card mb-5">
<div class="card-header"><h3 class="card-title">معلومات القائمة</h3></div>
<div class="grid grid-2 gap-4">
<div class="flex flex-col gap-1">
<span class="text-secondary text-sm">الاسم (English)</span>
<span class="font-medium"><?= View::e($roster['name'] ?? '-') ?></span>
</div>
<div class="flex flex-col gap-1">
<span class="text-secondary text-sm">الاسم (عربي)</span>
<span class="font-medium"><?= View::e($roster['name_ar'] ?? '-') ?></span>
</div>
<div class="flex flex-col gap-1">
<span class="text-secondary text-sm">المنظمة</span>
<a href="/organizations/<?= $org['id'] ?>" class="text-blue"><?= View::e($org['name_ar'] ?? $org['name']) ?></a>
</div>
<div class="flex flex-col gap-1">
<span class="text-secondary text-sm">اللعبة</span>
<code><?= View::e($roster['game_key'] ?? '-') ?></code>
</div>
<div class="flex flex-col gap-1">
<span class="text-secondary text-sm">الحالة</span>
<span class="badge <?= $statusBadges[$status] ?? 'badge-default' ?>"><?= $statusLabels[$status] ?? $status ?></span>
</div>
<div class="flex flex-col gap-1">
<span class="text-secondary text-sm">تاريخ الإنشاء</span>
<span class="tabular-nums"><?= date('Y-m-d H:i', strtotime($roster['created_at'])) ?></span>
</div>
</div>
</div>
<div class="card mb-5">
<div class="card-header">
<h3 class="card-title">اللاعبون (<?= count($players) ?>)</h3>
</div>
<?php if (empty($players)): ?>
<div class="empty-state">
<h3 class="empty-state-title">لا يوجد لاعبون</h3>
<p class="empty-state-text">لم يتم إضافة أي لاعبين إلى هذه القائمة بعد</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>اللاعب</th>
<th>المركز</th>
<th>رقم القميص</th>
<th>تاريخ الانضمام</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php
$positionBadges = [
'captain' => 'badge-warning',
'starter' => 'badge-success',
'substitute' => 'badge-info',
'reserve' => 'badge-default',
'coach' => 'badge-purple',
];
$positionLabels = [
'captain' => 'كابتن',
'starter' => 'أساسي',
'substitute' => 'بديل',
'reserve' => 'احتياطي',
'coach' => 'مدرب',
];
?>
<?php foreach ($players as $player): ?>
<tr>
<td>
<?php
$profile = $player['profiles'] ?? null;
$displayName = $profile['display_name'] ?? $profile['username'] ?? null;
?>
<?php if ($displayName): ?>
<span class="font-medium"><?= View::e($displayName) ?></span>
<?php else: ?>
<span class="text-muted text-xs"><?= View::e(substr($player['player_id'], 0, 8)) ?>...</span>
<?php endif; ?>
</td>
<td>
<?php $pos = $player['position'] ?? ''; ?>
<?php if ($pos): ?>
<span class="badge <?= $positionBadges[$pos] ?? 'badge-default' ?>"><?= $positionLabels[$pos] ?? $pos ?></span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td><?= $player['jersey_number'] !== null ? View::e($player['jersey_number']) : '-' ?></td>
<td class="tabular-nums"><?= !empty($player['joined_roster_at']) ? date('Y-m-d', strtotime($player['joined_roster_at'])) : date('Y-m-d', strtotime($player['created_at'] ?? 'now')) ?></td>
<td>
<form method="POST" action="/organizations/<?= $org['id'] ?>/rosters/<?= $roster['id'] ?>/remove-player" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من إزالة هذا اللاعب؟')">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="player_id" value="<?= View::e($player['player_id']) ?>">
<button type="submit" class="btn btn-ghost btn-sm text-danger">إزالة</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php if (($roster['status'] ?? '') !== 'archived'): ?>
<div class="card">
<div class="card-header"><h3 class="card-title">إضافة لاعب</h3></div>
<form method="POST" action="/organizations/<?= $org['id'] ?>/rosters/<?= $roster['id'] ?>/add-player" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">معرف اللاعب *</label>
<input type="text" name="player_id" class="form-input" required placeholder="UUID اللاعب" dir="ltr">
</div>
<div class="form-group">
<label class="form-label">المركز</label>
<select name="position" class="form-input">
<option value="">اختر المركز</option>
<option value="captain">كابتن</option>
<option value="starter">أساسي</option>
<option value="substitute">بديل</option>
<option value="reserve">احتياطي</option>
<option value="coach">مدرب</option>
</select>
</div>
<div class="form-group">
<label class="form-label">رقم القميص</label>
<input type="number" name="jersey_number" class="form-input" min="0" max="99" placeholder="رقم القميص">
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" class="form-input" placeholder="ملاحظات إضافية">
</div>
</div>
<div class="flex gap-3 mt-4">
<button type="submit" class="btn btn-primary">
<span class="btn-text">إضافة اللاعب</span>
<span class="btn-spinner"></span>
</button>
</div>
</form>
</div>
<?php endif; ?>
.spotlight-type {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-weight: 600;
text-transform: uppercase;
}
.spotlight-type.mvp { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
.spotlight-type.newcomer { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
.spotlight-type.most_improved { background: rgba(59, 130, 246, 0.15); color: #3b82f6; }
.spotlight-type.top_contributor { background: rgba(168, 85, 247, 0.15); color: #a855f7; }
.spotlight-type.custom { background: rgba(156, 163, 175, 0.15); color: #9ca3af; }
<?php
class OrgSpotlightsController
{
private Database $db;
public function __construct()
{
$this->db = Database::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' => '*, profiles(username, display_name)',
'org_id' => "eq.{$orgId}",
'order' => 'created_at.desc',
];
$total = $this->db->count('org_member_spotlights', $filters);
$pagination = Pagination::fromRequest($total);
$filters['offset'] = $pagination->offset;
$filters['limit'] = $pagination->perPage;
$spotlights = $this->db->select('org_member_spotlights', $filters);
$pageTitle = 'تسليط الضوء - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-spotlights/list', compact('org', 'spotlights', '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;
}
$members = $this->db->select('org_members', [
'select' => '*, profiles(username, display_name)',
'org_id' => "eq.{$orgId}",
'order' => 'joined_at.asc',
]);
$pageTitle = 'إضافة تسليط ضوء - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-spotlights/form', compact('org', 'members', '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();
$stats = [];
if (!empty($_POST['stats'])) {
$stats = is_array($_POST['stats']) ? $_POST['stats'] : json_decode($_POST['stats'], true) ?? [];
}
$data = [
'org_id' => $orgId,
'player_id' => $_POST['player_id'] ?? null,
'type' => $_POST['type'] ?? 'custom',
'period' => trim($_POST['period'] ?? ''),
'title' => trim($_POST['title'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'stats' => json_encode($stats),
'awarded_by' => $user['id'],
'is_active' => true,
];
$result = $this->db->insert('org_member_spotlights', $data);
AuditLog::log('create', 'org_member_spotlight', $result['id'] ?? null, null, $data);
Response::success('تم إضافة تسليط الضوء بنجاح', "/organizations/{$orgId}/spotlights");
}
public function toggle(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$spotId = $params['spotId'];
$spotlight = $this->db->selectOne('org_member_spotlights', [
'id' => "eq.{$spotId}",
'org_id' => "eq.{$orgId}",
]);
if (!$spotlight) {
Response::error('تسليط الضوء غير موجود', "/organizations/{$orgId}/spotlights");
return;
}
$newStatus = !($spotlight['is_active'] ?? false);
$this->db->update('org_member_spotlights', ['id' => "eq.{$spotId}"], [
'is_active' => $newStatus,
]);
AuditLog::log('toggle', 'org_member_spotlight', $spotId, $spotlight, ['is_active' => $newStatus]);
Response::success(
$newStatus ? 'تم تفعيل تسليط الضوء' : 'تم تعطيل تسليط الضوء',
"/organizations/{$orgId}/spotlights"
);
}
public function delete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$spotId = $params['spotId'];
$spotlight = $this->db->selectOne('org_member_spotlights', [
'id' => "eq.{$spotId}",
'org_id' => "eq.{$orgId}",
]);
if (!$spotlight) {
Response::error('تسليط الضوء غير موجود', "/organizations/{$orgId}/spotlights");
return;
}
$this->db->delete('org_member_spotlights', ['id' => "eq.{$spotId}"]);
AuditLog::log('delete', 'org_member_spotlight', $spotId, $spotlight, null);
Response::success('تم حذف تسليط الضوء', "/organizations/{$orgId}/spotlights");
}
}
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/spotlights" 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>
<div class="card max-w-lg">
<form method="POST" action="/organizations/<?= $org['id'] ?>/spotlights/store" data-validate>
<?= Auth::csrfField() ?>
<div class="form-group">
<label class="form-label">اللاعب <span style="color:var(--danger);">*</span></label>
<?php if (!empty($members)): ?>
<select name="player_id" class="form-input" required>
<option value="">اختر لاعب...</option>
<?php foreach ($members as $member): ?>
<option value="<?= View::e($member['user_id'] ?? $member['id']) ?>" <?= ($_POST['player_id'] ?? '') === ($member['user_id'] ?? $member['id']) ? 'selected' : '' ?>>
<?= View::e($member['profiles']['display_name'] ?? $member['profiles']['username'] ?? substr($member['user_id'] ?? $member['id'], 0, 8) . '...') ?>
</option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="text" name="player_id" class="form-input" required placeholder="UUID اللاعب" value="<?= View::e($_POST['player_id'] ?? '') ?>" dir="ltr">
<?php endif; ?>
<span class="form-hint">اختر اللاعب الذي سيتم تسليط الضوء عليه.</span>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:var(--danger);">*</span></label>
<select name="type" class="form-input" required>
<option value="">اختر النوع...</option>
<option value="mvp" <?= ($_POST['type'] ?? '') === 'mvp' ? 'selected' : '' ?>>MVP</option>
<option value="newcomer" <?= ($_POST['type'] ?? '') === 'newcomer' ? 'selected' : '' ?>>وافد جديد</option>
<option value="most_improved" <?= ($_POST['type'] ?? '') === 'most_improved' ? 'selected' : '' ?>>الأكثر تحسناً</option>
<option value="top_contributor" <?= ($_POST['type'] ?? '') === 'top_contributor' ? 'selected' : '' ?>>أفضل مساهم</option>
<option value="custom" <?= ($_POST['type'] ?? '') === 'custom' ? 'selected' : '' ?>>مخصص</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الفترة</label>
<input type="text" name="period" class="form-input" placeholder="مثال: 2024-01" value="<?= View::e($_POST['period'] ?? '') ?>" dir="ltr">
<span class="form-hint">الفترة الزمنية للتكريم (مثال: 2024-01 لشهر يناير 2024).</span>
</div>
<div class="form-group">
<label class="form-label">العنوان</label>
<input type="text" name="title" class="form-input" placeholder="عنوان تسليط الضوء" value="<?= View::e($_POST['title'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-input" rows="4" placeholder="وصف الإنجاز أو سبب التكريم..."><?= View::e($_POST['description'] ?? '') ?></textarea>
</div>
<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="/organizations/<?= $org['id'] ?>/spotlights" 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'] ?>/spotlights/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" style="margin-bottom:0;">
<label class="form-label">النوع</label>
<select name="type" class="form-input">
<option value="">الكل</option>
<option value="mvp" <?= ($_GET['type'] ?? '') === 'mvp' ? 'selected' : '' ?>>MVP</option>
<option value="newcomer" <?= ($_GET['type'] ?? '') === 'newcomer' ? 'selected' : '' ?>>وافد جديد</option>
<option value="most_improved" <?= ($_GET['type'] ?? '') === 'most_improved' ? 'selected' : '' ?>>الأكثر تحسناً</option>
<option value="top_contributor" <?= ($_GET['type'] ?? '') === 'top_contributor' ? 'selected' : '' ?>>أفضل مساهم</option>
<option value="custom" <?= ($_GET['type'] ?? '') === 'custom' ? 'selected' : '' ?>>مخصص</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm">تصفية</button>
<a href="/organizations/<?= $org['id'] ?>/spotlights" class="btn btn-ghost btn-sm">إعادة تعيين</a>
</form>
</div>
<div class="data-table-wrapper">
<?php if (empty($spotlights)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<h3 class="empty-state-title">لا يوجد تسليط ضوء</h3>
<p class="empty-state-text">لم يتم إضافة أي تسليط ضوء لأعضاء هذه المنظمة بعد</p>
<a href="/organizations/<?= $org['id'] ?>/spotlights/create" class="btn btn-primary">إضافة تسليط ضوء</a>
</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 ($spotlights as $spotlight): ?>
<tr>
<td>
<?php
$playerName = $spotlight['profiles']['display_name'] ?? $spotlight['profiles']['username'] ?? null;
?>
<?php if ($playerName): ?>
<span class="font-semibold"><?= View::e($playerName) ?></span>
<?php else: ?>
<span class="text-muted text-xs"><?= View::e(substr($spotlight['player_id'], 0, 8)) ?>...</span>
<?php endif; ?>
</td>
<td><?= View::e($org['name_ar'] ?? $org['name']) ?></td>
<td>
<?php
$typeBadges = [
'mvp' => 'badge-success',
'newcomer' => 'badge-info',
'most_improved' => 'badge-warning',
'top_contributor' => 'badge-purple',
'custom' => 'badge-default',
];
$typeLabels = [
'mvp' => 'MVP',
'newcomer' => 'وافد جديد',
'most_improved' => 'الأكثر تحسناً',
'top_contributor' => 'أفضل مساهم',
'custom' => 'مخصص',
];
$type = $spotlight['type'] ?? 'custom';
$badgeClass = $typeBadges[$type] ?? 'badge-default';
$typeLabel = $typeLabels[$type] ?? $type;
?>
<span class="badge <?= $badgeClass ?>"><?= $typeLabel ?></span>
</td>
<td class="text-xs tabular-nums"><?= View::e($spotlight['period'] ?? '-') ?></td>
<td><?= View::e($spotlight['title'] ?? '-') ?></td>
<td>
<?php if ($spotlight['is_active'] ?? false): ?>
<span class="badge badge-success badge-dot">نشط</span>
<?php else: ?>
<span class="badge badge-danger badge-dot">معطل</span>
<?php endif; ?>
</td>
<td class="text-xs tabular-nums"><?= date('Y-m-d', strtotime($spotlight['created_at'])) ?></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">
<form method="POST" action="/organizations/<?= $org['id'] ?>/spotlights/<?= $spotlight['id'] ?>/toggle" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="dropdown-item">
<?= ($spotlight['is_active'] ?? false) ? 'تعطيل' : 'تفعيل' ?>
</button>
</form>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger" onclick="confirmDelete('/organizations/<?= $org['id'] ?>/spotlights/<?= $spotlight['id'] ?>/delete', '<?= View::e($spotlight['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 ?><?= !empty($_GET['type']) ? '&type=' . urlencode($_GET['type']) : '' ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
.skill-level-badge {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: var(--radius-sm);
}
.training-topics {
display: flex;
gap: var(--space-1);
flex-wrap: wrap;
}
.training-topics span {
font-size: 0.7rem;
padding: 1px 6px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
}
<?php
class OrgTrainingController
{
private Database $db;
public function __construct()
{
$this->db = Database::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' => 'starts_at.desc',
];
if (!empty($_GET['status'])) {
$filters['status'] = "eq.{$_GET['status']}";
}
$total = $this->db->count('org_training_sessions', $filters);
$pagination = Pagination::fromRequest($total);
$filters['offset'] = $pagination->offset;
$filters['limit'] = $pagination->perPage;
$sessions = $this->db->select('org_training_sessions', $filters);
$pageTitle = 'جلسات التدريب - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-training/list', compact('org', 'sessions', '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;
}
$members = $this->db->select('org_members', [
'select' => '*, profiles(username, display_name)',
'org_id' => "eq.{$orgId}",
'order' => 'joined_at.asc',
]);
$pageTitle = 'إنشاء جلسة تدريب - ' . ($org['name_ar'] ?? $org['name']);
View::render('org-training/form', compact('org', 'members', '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;
}
$topics = [];
if (!empty($_POST['topics']) && is_array($_POST['topics'])) {
$topics = $_POST['topics'];
}
$data = [
'org_id' => $orgId,
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'game_key' => $_POST['game_key'] ?? null,
'coach_id' => $_POST['coach_id'] ?? null,
'starts_at' => $_POST['starts_at'] ?? null,
'duration_minutes' => !empty($_POST['duration_minutes']) ? (int) $_POST['duration_minutes'] : null,
'max_participants' => !empty($_POST['max_participants']) ? (int) $_POST['max_participants'] : null,
'skill_level' => $_POST['skill_level'] ?? null,
'topics' => json_encode($topics),
'status' => 'scheduled',
];
$result = $this->db->insert('org_training_sessions', $data);
AuditLog::log('create', 'org_training_session', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء جلسة التدريب بنجاح', "/organizations/{$orgId}/training");
}
public function edit(array $params, string $method): void
{
$orgId = $params['id'];
$sessionId = $params['sessionId'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
http_response_code(404);
View::render('errors/404');
return;
}
$session = $this->db->selectOne('org_training_sessions', [
'id' => "eq.{$sessionId}",
'org_id' => "eq.{$orgId}",
]);
if (!$session) {
Response::error('الجلسة غير موجودة', "/organizations/{$orgId}/training");
return;
}
$members = $this->db->select('org_members', [
'select' => '*, profiles(username, display_name)',
'org_id' => "eq.{$orgId}",
'order' => 'joined_at.asc',
]);
$pageTitle = 'تعديل جلسة التدريب - ' . ($session['title_ar'] ?? $session['title']);
View::render('org-training/form', compact('org', 'session', 'members', 'pageTitle'));
}
public function update(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$sessionId = $params['sessionId'];
$old = $this->db->selectOne('org_training_sessions', [
'id' => "eq.{$sessionId}",
'org_id' => "eq.{$orgId}",
]);
if (!$old) {
Response::error('الجلسة غير موجودة', "/organizations/{$orgId}/training");
return;
}
$topics = [];
if (!empty($_POST['topics']) && is_array($_POST['topics'])) {
$topics = $_POST['topics'];
}
$data = [
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'game_key' => $_POST['game_key'] ?? null,
'coach_id' => $_POST['coach_id'] ?? null,
'starts_at' => $_POST['starts_at'] ?? null,
'duration_minutes' => !empty($_POST['duration_minutes']) ? (int) $_POST['duration_minutes'] : null,
'max_participants' => !empty($_POST['max_participants']) ? (int) $_POST['max_participants'] : null,
'skill_level' => $_POST['skill_level'] ?? null,
'topics' => json_encode($topics),
];
$this->db->update('org_training_sessions', ['id' => "eq.{$sessionId}"], $data);
AuditLog::log('update', 'org_training_session', $sessionId, $old, $data);
Response::success('تم تحديث جلسة التدريب بنجاح', "/organizations/{$orgId}/training");
}
public function cancel(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$sessionId = $params['sessionId'];
$session = $this->db->selectOne('org_training_sessions', [
'id' => "eq.{$sessionId}",
'org_id' => "eq.{$orgId}",
]);
if (!$session) {
Response::error('الجلسة غير موجودة', "/organizations/{$orgId}/training");
return;
}
$this->db->update('org_training_sessions', ['id' => "eq.{$sessionId}"], [
'status' => 'cancelled',
]);
AuditLog::log('cancel', 'org_training_session', $sessionId, $session, ['status' => 'cancelled']);
Response::success('تم إلغاء جلسة التدريب', "/organizations/{$orgId}/training");
}
public function complete(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$sessionId = $params['sessionId'];
$session = $this->db->selectOne('org_training_sessions', [
'id' => "eq.{$sessionId}",
'org_id' => "eq.{$orgId}",
]);
if (!$session) {
Response::error('الجلسة غير موجودة', "/organizations/{$orgId}/training");
return;
}
$data = [
'status' => 'completed',
];
if (!empty($_POST['recording_url'])) {
$data['recording_url'] = trim($_POST['recording_url']);
}
if (!empty($_POST['materials_url'])) {
$data['materials_url'] = trim($_POST['materials_url']);
}
$this->db->update('org_training_sessions', ['id' => "eq.{$sessionId}"], $data);
AuditLog::log('complete', 'org_training_session', $sessionId, $session, $data);
Response::success('تم إكمال جلسة التدريب', "/organizations/{$orgId}/training");
}
}
<?php
$isEdit = !empty($session);
$actionUrl = $isEdit
? "/organizations/{$org['id']}/training/{$session['id']}/update"
: "/organizations/{$org['id']}/training/store";
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>/training" 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="<?= $actionUrl ?>" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group">
<label class="form-label">المنظمة</label>
<input type="text" class="form-input" value="<?= View::e($org['name_ar'] ?? $org['name']) ?>" disabled>
<input type="hidden" name="org_id" value="<?= $org['id'] ?>">
</div>
<div class="form-group">
<label class="form-label">العنوان (English) *</label>
<input type="text" name="title" class="form-input" required value="<?= View::e($session['title'] ?? '') ?>" placeholder="Training session title" dir="ltr">
</div>
<div class="form-group">
<label class="form-label">العنوان (عربي)</label>
<input type="text" name="title_ar" class="form-input" value="<?= View::e($session['title_ar'] ?? '') ?>" placeholder="عنوان جلسة التدريب">
</div>
<div class="form-group">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-input" rows="4" placeholder="وصف الجلسة التدريبية..."><?= View::e($session['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">مفتاح اللعبة</label>
<input type="text" name="game_key" class="form-input" value="<?= View::e($session['game_key'] ?? '') ?>" placeholder="chess, valorant, etc." dir="ltr">
</div>
<div class="form-group">
<label class="form-label">معرف المدرب</label>
<input type="text" name="coach_id" class="form-input" value="<?= View::e($session['coach_id'] ?? '') ?>" placeholder="UUID المدرب" dir="ltr">
<span class="form-hint">معرف اللاعب الذي سيقوم بتدريب الجلسة</span>
</div>
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">يبدأ في</label>
<input type="datetime-local" name="starts_at" class="form-input" dir="ltr" value="<?= !empty($session['starts_at']) ? date('Y-m-d\TH:i', strtotime($session['starts_at'])) : '' ?>">
</div>
<div class="form-group">
<label class="form-label">المدة (بالدقائق)</label>
<input type="number" name="duration_minutes" class="form-input" min="1" value="<?= View::e($session['duration_minutes'] ?? '') ?>" placeholder="60">
</div>
</div>
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">الحد الأقصى للمشاركين</label>
<input type="number" name="max_participants" class="form-input" min="1" value="<?= View::e($session['max_participants'] ?? '') ?>" placeholder="اختياري">
</div>
<div class="form-group">
<label class="form-label">مستوى المهارة</label>
<select name="skill_level" class="form-input">
<option value="">اختر المستوى</option>
<option value="beginner" <?= ($session['skill_level'] ?? '') === 'beginner' ? 'selected' : '' ?>>مبتدئ</option>
<option value="intermediate" <?= ($session['skill_level'] ?? '') === 'intermediate' ? 'selected' : '' ?>>متوسط</option>
<option value="advanced" <?= ($session['skill_level'] ?? '') === 'advanced' ? 'selected' : '' ?>>متقدم</option>
<option value="all" <?= ($session['skill_level'] ?? '') === 'all' ? 'selected' : '' ?>>الكل</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">المواضيع</label>
<?php
$topics = '';
if ($isEdit && !empty($session['topics'])) {
$topicsArr = is_string($session['topics']) ? json_decode($session['topics'], true) : $session['topics'];
$topics = is_array($topicsArr) ? implode(', ', $topicsArr) : '';
}
?>
<input type="text" name="topics[]" class="form-input" value="<?= View::e($topics) ?>" placeholder="موضوع1, موضوع2, موضوع3" dir="ltr">
<span class="form-hint">أدخل المواضيع مفصولة بفواصل</span>
</div>
<div class="form-group">
<label class="form-label">رابط التسجيل</label>
<input type="text" name="recording_url" class="form-input" value="<?= View::e($session['recording_url'] ?? '') ?>" placeholder="https://..." dir="ltr">
</div>
<div class="form-group">
<label class="form-label">رابط المواد</label>
<input type="text" name="materials_url" class="form-input" value="<?= View::e($session['materials_url'] ?? '') ?>" placeholder="https://..." dir="ltr">
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="is_recurring" value="1" <?= !empty($session['is_recurring']) ? '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'] ?>/training" 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'] ?>/training/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>
<input type="text" name="game_key" class="form-input" value="<?= View::e($_GET['game_key'] ?? '') ?>" placeholder="مفتاح اللعبة">
</div>
<div class="form-group mb-0">
<label class="form-label">الحالة</label>
<select name="status" class="form-input">
<option value="">الكل</option>
<option value="scheduled" <?= ($_GET['status'] ?? '') === 'scheduled' ? 'selected' : '' ?>>مجدولة</option>
<option value="in_progress" <?= ($_GET['status'] ?? '') === 'in_progress' ? 'selected' : '' ?>>جارية</option>
<option value="completed" <?= ($_GET['status'] ?? '') === 'completed' ? 'selected' : '' ?>>مكتملة</option>
<option value="cancelled" <?= ($_GET['status'] ?? '') === 'cancelled' ? 'selected' : '' ?>>ملغاة</option>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
<a href="/organizations/<?= $org['id'] ?>/training" class="btn btn-ghost">إعادة تعيين</a>
</form>
</div>
<div class="data-table-wrapper">
<?php if (empty($sessions)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 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>المشاركون</th>
<th>المستوى</th>
<th>الحالة</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php
$statusBadges = ['scheduled' => 'badge-info', 'in_progress' => 'badge-warning', 'completed' => 'badge-success', 'cancelled' => 'badge-default'];
$statusLabels = ['scheduled' => 'مجدولة', 'in_progress' => 'جارية', 'completed' => 'مكتملة', 'cancelled' => 'ملغاة'];
$levelBadges = ['beginner' => 'badge-success', 'intermediate' => 'badge-info', 'advanced' => 'badge-warning', 'all' => 'badge-default'];
$levelLabels = ['beginner' => 'مبتدئ', 'intermediate' => 'متوسط', 'advanced' => 'متقدم', 'all' => 'الكل'];
?>
<?php foreach ($sessions as $session): ?>
<tr>
<td class="font-medium"><?= View::e($session['title_ar'] ?? $session['title'] ?? '-') ?></td>
<td><?= View::e($org['name_ar'] ?? $org['name']) ?></td>
<td><code><?= View::e($session['game_key'] ?? '-') ?></code></td>
<td>
<?php if (!empty($session['coach_id'])): ?>
<span class="text-xs"><?= View::e(substr($session['coach_id'], 0, 8)) ?>...</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td class="tabular-nums"><?= !empty($session['starts_at']) ? date('Y-m-d H:i', strtotime($session['starts_at'])) : '-' ?></td>
<td><?= $session['duration_minutes'] ? $session['duration_minutes'] . ' د' : '-' ?></td>
<td>
<?php
$current = $session['participants_count'] ?? 0;
$max = $session['max_participants'] ?? null;
?>
<?= $current ?><?= $max ? " / {$max}" : '' ?>
</td>
<td>
<?php $level = $session['skill_level'] ?? ''; ?>
<?php if ($level): ?>
<span class="badge <?= $levelBadges[$level] ?? 'badge-default' ?>"><?= $levelLabels[$level] ?? $level ?></span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<?php $sessionStatus = $session['status'] ?? 'scheduled'; ?>
<span class="badge <?= $statusBadges[$sessionStatus] ?? 'badge-default' ?>"><?= $statusLabels[$sessionStatus] ?? $sessionStatus ?></span>
</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'] ?>/training/<?= $session['id'] ?>/edit" class="dropdown-item">تعديل</a>
<?php if ($sessionStatus === 'scheduled' || $sessionStatus === 'in_progress'): ?>
<form method="POST" action="/organizations/<?= $org['id'] ?>/training/<?= $session['id'] ?>/complete" style="margin:0;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="dropdown-item">إكمال</button>
</form>
<form method="POST" action="/organizations/<?= $org['id'] ?>/training/<?= $session['id'] ?>/cancel" style="margin:0;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="dropdown-item danger">إلغاء</button>
</form>
<?php endif; ?>
</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 ?>&status=<?= View::e($_GET['status'] ?? '') ?>&game_key=<?= View::e($_GET['game_key'] ?? '') ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php
class OrgTransfersController
{
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_player_transfers', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$transfers = $this->db->select('org_player_transfers', $queryParams);
$playerIds = array_column($transfers, 'player_id');
$orgIds = array_unique(array_merge(
array_filter(array_column($transfers, 'from_org_id')),
array_column($transfers, 'to_org_id')
));
$players = [];
if (!empty($playerIds)) {
$idList = '(' . implode(',', $playerIds) . ')';
foreach ($this->db->select('profiles', ['select' => 'id,username,display_name', 'id' => "in.{$idList}"]) as $p) {
$players[$p['id']] = $p;
}
}
$orgs = [];
if (!empty($orgIds)) {
$idList = '(' . implode(',', $orgIds) . ')';
foreach ($this->db->select('el3ab_organizations', ['select' => 'id,name,name_ar', 'id' => "in.{$idList}"]) as $o) {
$orgs[$o['id']] = $o;
}
}
$pageTitle = 'انتقالات اللاعبين';
$moduleCSS = 'org-transfers';
View::render('org-transfers/list', compact('transfers', 'players', 'orgs', 'pagination', 'status', 'pageTitle', 'moduleCSS'));
}
public function show(array $params, string $method): void
{
$id = $params['id'];
$transfer = $this->db->selectOne('org_player_transfers', ['id' => "eq.{$id}"]);
if (!$transfer) {
Response::error('الانتقال غير موجود', '/org-transfers');
return;
}
$player = $this->db->selectOne('profiles', ['id' => "eq.{$transfer['player_id']}"]);
$fromOrg = $transfer['from_org_id'] ? $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$transfer['from_org_id']}"]) : null;
$toOrg = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$transfer['to_org_id']}"]);
$pageTitle = 'تفاصيل الانتقال';
$moduleCSS = 'org-transfers';
View::render('org-transfers/show', compact('transfer', 'player', 'fromOrg', 'toOrg', 'pageTitle', 'moduleCSS'));
}
public function initiate(array $params, string $method): void
{
Auth::requireCsrf();
$playerId = $_POST['player_id'] ?? '';
$toOrgId = $_POST['to_org_id'] ?? '';
$fromOrgId = $_POST['from_org_id'] ?? null;
if (empty($playerId) || empty($toOrgId)) {
Response::error('يرجى تحديد اللاعب والمنظمة المستقبلة', '/org-transfers');
return;
}
if (empty($fromOrgId)) {
$currentMembership = $this->db->selectOne('org_members', [
'user_id' => "eq.{$playerId}",
'status' => 'eq.active',
]);
$fromOrgId = $currentMembership['org_id'] ?? null;
}
$data = [
'player_id' => $playerId,
'from_org_id' => $fromOrgId,
'to_org_id' => $toOrgId,
'transfer_type' => $_POST['transfer_type'] ?? 'request',
'transfer_fee_coins' => (int)($_POST['transfer_fee_coins'] ?? 0),
'transfer_fee_gems' => (int)($_POST['transfer_fee_gems'] ?? 0),
'initiated_by' => Auth::user()['id'],
'admin_override' => isset($_POST['admin_override']),
'notes' => trim($_POST['notes'] ?? ''),
'status' => 'pending',
];
if (isset($_POST['admin_override'])) {
$data['status'] = 'completed';
$data['player_consent'] = true;
$data['from_org_consent'] = true;
$data['to_org_consent'] = true;
$data['completed_at'] = date('c');
if ($fromOrgId) {
$this->db->delete('org_members', [
'org_id' => "eq.{$fromOrgId}",
'user_id' => "eq.{$playerId}",
]);
}
$this->db->insert('org_members', [
'org_id' => $toOrgId,
'user_id' => $playerId,
'role' => 'member',
'status' => 'active',
'joined_at' => date('c'),
]);
}
$result = $this->db->insert('org_player_transfers', $data);
AuditLog::log('initiate_transfer', 'org_transfer', $result['id'] ?? null, null, $data);
Response::success('تم إنشاء طلب الانتقال', '/org-transfers');
}
public function approve(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$step = $_POST['step'] ?? '';
$transfer = $this->db->selectOne('org_player_transfers', ['id' => "eq.{$id}"]);
if (!$transfer) {
Response::error('الانتقال غير موجود', '/org-transfers');
return;
}
$update = ['updated_at' => date('c')];
switch ($step) {
case 'player':
$update['player_consent'] = true;
$update['status'] = 'player_accepted';
break;
case 'from_org':
$update['from_org_consent'] = true;
$update['status'] = 'from_org_approved';
break;
case 'to_org':
$update['to_org_consent'] = true;
$update['status'] = 'to_org_approved';
break;
default:
$update['player_consent'] = true;
$update['from_org_consent'] = true;
$update['to_org_consent'] = true;
$update['status'] = 'to_org_approved';
$update['admin_override'] = true;
}
$this->db->update('org_player_transfers', ['id' => "eq.{$id}"], $update);
AuditLog::log('approve_transfer', 'org_transfer', $id, null, ['step' => $step]);
Response::success('تم الموافقة', "/org-transfers/{$id}");
}
public function reject(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_player_transfers', ['id' => "eq.{$id}"], [
'status' => 'rejected',
'notes' => trim($_POST['notes'] ?? ''),
'updated_at' => date('c'),
]);
AuditLog::log('reject_transfer', 'org_transfer', $id);
Response::success('تم رفض الانتقال', '/org-transfers');
}
public function complete(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$transfer = $this->db->selectOne('org_player_transfers', ['id' => "eq.{$id}"]);
if (!$transfer) {
Response::error('الانتقال غير موجود', '/org-transfers');
return;
}
if ($transfer['from_org_id']) {
$this->db->delete('org_members', [
'org_id' => "eq.{$transfer['from_org_id']}",
'user_id' => "eq.{$transfer['player_id']}",
]);
}
$existing = $this->db->selectOne('org_members', [
'org_id' => "eq.{$transfer['to_org_id']}",
'user_id' => "eq.{$transfer['player_id']}",
]);
if (!$existing) {
$this->db->insert('org_members', [
'org_id' => $transfer['to_org_id'],
'user_id' => $transfer['player_id'],
'role' => 'member',
'status' => 'active',
'joined_at' => date('c'),
]);
}
$this->db->update('org_player_transfers', ['id' => "eq.{$id}"], [
'status' => 'completed',
'completed_at' => date('c'),
'updated_at' => date('c'),
]);
$this->db->insert('notifications', [
'user_id' => $transfer['player_id'],
'title' => 'تم إتمام انتقالك',
'body' => 'تم نقلك بنجاح إلى المنظمة الجديدة',
'type' => 'social',
]);
AuditLog::log('complete_transfer', 'org_transfer', $id);
Response::success('تم إتمام الانتقال', "/org-transfers/{$id}");
}
public function cancel(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$this->db->update('org_player_transfers', ['id' => "eq.{$id}"], [
'status' => 'cancelled',
'updated_at' => date('c'),
]);
AuditLog::log('cancel_transfer', 'org_transfer', $id);
Response::success('تم إلغاء الانتقال', '/org-transfers');
}
}
<div class="content-header">
<h1>انتقالات اللاعبين</h1>
</div>
<!-- Filters -->
<div class="card mb-4">
<form method="GET" action="/transfers" 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="initiated" <?= ($status ?? '') === 'initiated' ? 'selected' : '' ?>>بدأ</option>
<option value="player_consent" <?= ($status ?? '') === 'player_consent' ? 'selected' : '' ?>>موافقة اللاعب</option>
<option value="from_org_approved" <?= ($status ?? '') === 'from_org_approved' ? 'selected' : '' ?>>موافقة المنظمة المرسلة</option>
<option value="to_org_approved" <?= ($status ?? '') === 'to_org_approved' ? 'selected' : '' ?>>موافقة المنظمة المستقبلة</option>
<option value="completed" <?= ($status ?? '') === 'completed' ? 'selected' : '' ?>>مكتمل</option>
<option value="rejected" <?= ($status ?? '') === 'rejected' ? 'selected' : '' ?>>مرفوض</option>
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>ملغي</option>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
</form>
</div>
<!-- Transfers Table -->
<div class="data-table-wrapper">
<?php if (empty($transfers)): ?>
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/></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>التاريخ</th>
<th style="width: 80px;">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($transfers as $transfer): ?>
<?php
$player = $players[$transfer['player_id']] ?? null;
$fromOrg = $orgs[$transfer['from_org_id']] ?? null;
$toOrg = $orgs[$transfer['to_org_id']] ?? null;
$tStatus = $transfer['status'] ?? 'initiated';
$statusLabels = [
'initiated' => 'بدأ',
'player_consent' => 'موافقة اللاعب',
'from_org_approved' => 'المرسلة وافقت',
'to_org_approved' => 'المستقبلة وافقت',
'completed' => 'مكتمل',
'rejected' => 'مرفوض',
'cancelled' => 'ملغي',
];
$statusBadges = [
'initiated' => 'badge-default',
'player_consent' => 'badge-info',
'from_org_approved' => 'badge-purple',
'to_org_approved' => 'badge-purple',
'completed' => 'badge-success',
'rejected' => 'badge-danger',
'cancelled' => 'badge-default',
];
$typeLabels = ['transfer' => 'انتقال', 'loan' => 'إعارة', 'free' => 'حر'];
?>
<tr>
<td>
<strong><?= View::e($player['username'] ?? $player['name'] ?? '-') ?></strong>
</td>
<td><?= View::e($fromOrg['name_ar'] ?? $fromOrg['name'] ?? '-') ?></td>
<td class="text-center text-muted">&larr;</td>
<td><?= View::e($toOrg['name_ar'] ?? $toOrg['name'] ?? '-') ?></td>
<td><?= $typeLabels[$transfer['type'] ?? 'transfer'] ?? ($transfer['type'] ?? '-') ?></td>
<td>
<span class="badge <?= $statusBadges[$tStatus] ?? 'badge-default' ?>"><?= $statusLabels[$tStatus] ?? $tStatus ?></span>
</td>
<td><?= isset($transfer['fee']) ? number_format($transfer['fee']) : '-' ?></td>
<td><?= date('Y-m-d', strtotime($transfer['created_at'])) ?></td>
<td>
<a href="/transfers/<?= $transfer['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
$tStatus = $transfer['status'] ?? 'initiated';
$statusLabels = [
'initiated' => 'بدأ',
'player_consent' => 'موافقة اللاعب',
'from_org_approved' => 'المرسلة وافقت',
'to_org_approved' => 'المستقبلة وافقت',
'completed' => 'مكتمل',
'rejected' => 'مرفوض',
'cancelled' => 'ملغي',
];
$steps = ['initiated', 'player_consent', 'from_org_approved', 'to_org_approved', 'completed'];
$stepLabels = [
'initiated' => 'بدء الطلب',
'player_consent' => 'موافقة اللاعب',
'from_org_approved' => 'المنظمة المرسلة',
'to_org_approved' => 'المنظمة المستقبلة',
'completed' => 'مكتمل',
];
$currentStepIndex = array_search($tStatus, $steps);
if ($currentStepIndex === false) $currentStepIndex = -1;
?>
<div class="content-header">
<div class="flex items-center gap-4">
<a href="/transfers" 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 <?= in_array($tStatus, ['completed']) ? 'badge-success' : (in_array($tStatus, ['rejected', 'cancelled']) ? 'badge-danger' : 'badge-info') ?>">
<?= $statusLabels[$tStatus] ?? $tStatus ?>
</span>
</div>
</div>
<!-- Player Card -->
<div class="card mb-4">
<div class="flex items-center gap-4">
<div class="stat-icon blue" style="width: 48px; height: 48px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div>
<h3><?= View::e($player['username'] ?? $player['name'] ?? '-') ?></h3>
<p class="text-sm text-muted"><?= View::e($player['email'] ?? '') ?></p>
</div>
</div>
</div>
<!-- Transfer Pipeline -->
<div class="card mb-4">
<h3 style="margin-bottom: 20px;">مراحل الانتقال</h3>
<div class="flex items-center justify-between" style="padding: 0 16px;">
<?php foreach ($steps as $i => $step): ?>
<?php
$isCompleted = $currentStepIndex >= $i;
$isCurrent = $currentStepIndex === $i;
$dotColor = $isCompleted ? 'var(--success)' : 'var(--border-color)';
$dotBorder = $isCurrent ? '3px solid var(--primary)' : 'none';
?>
<div style="text-align: center; flex: 1;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: <?= $dotColor ?>; margin: 0 auto 8px; display: flex; align-items: center; justify-content: center; border: <?= $dotBorder ?>;">
<?php if ($isCompleted): ?>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
<?php endif; ?>
</div>
<p class="text-xs <?= $isCurrent ? 'font-bold' : 'text-muted' ?>"><?= $stepLabels[$step] ?></p>
</div>
<?php if ($i < count($steps) - 1): ?>
<div style="flex: 0.5; height: 2px; background: <?= $currentStepIndex > $i ? 'var(--success)' : 'var(--border-color)' ?>;"></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<!-- From/To Orgs -->
<div class="grid grid-2 gap-4 mb-4">
<div class="card">
<p class="text-sm text-muted mb-2">من منظمة</p>
<div class="flex items-center gap-3">
<?php if (!empty($fromOrg['logo_url'])): ?>
<img src="<?= View::e($fromOrg['logo_url']) ?>" alt="" style="width:40px;height:40px;border-radius:6px;">
<?php endif; ?>
<h3><?= View::e($fromOrg['name_ar'] ?? $fromOrg['name'] ?? '-') ?></h3>
</div>
</div>
<div class="card">
<p class="text-sm text-muted mb-2">إلى منظمة</p>
<div class="flex items-center gap-3">
<?php if (!empty($toOrg['logo_url'])): ?>
<img src="<?= View::e($toOrg['logo_url']) ?>" alt="" style="width:40px;height:40px;border-radius:6px;">
<?php endif; ?>
<h3><?= View::e($toOrg['name_ar'] ?? $toOrg['name'] ?? '-') ?></h3>
</div>
</div>
</div>
<!-- Transfer Details -->
<div class="card mb-4">
<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;"><?php $typeLabels = ['transfer' => 'انتقال', 'loan' => 'إعارة', 'free' => 'حر']; echo $typeLabels[$transfer['type'] ?? 'transfer'] ?? ($transfer['type'] ?? '-'); ?></td></tr>
<tr><td class="text-muted" style="padding:4px 0;">الرسوم</td><td style="padding:4px 0;"><?= isset($transfer['fee']) ? number_format($transfer['fee']) : '-' ?></td></tr>
<tr><td class="text-muted" style="padding:4px 0;">تاريخ الطلب</td><td style="padding:4px 0;"><?= date('Y-m-d H:i', strtotime($transfer['created_at'])) ?></td></tr>
<?php if (!empty($transfer['completed_at'])): ?>
<tr><td class="text-muted" style="padding:4px 0;">تاريخ الإكمال</td><td style="padding:4px 0;"><?= date('Y-m-d H:i', strtotime($transfer['completed_at'])) ?></td></tr>
<?php endif; ?>
<?php if (!empty($transfer['notes'])): ?>
<tr><td class="text-muted" style="padding:4px 0;">ملاحظات</td><td style="padding:4px 0;"><?= View::e($transfer['notes']) ?></td></tr>
<?php endif; ?>
</table>
</div>
<!-- Actions -->
<?php if (!in_array($tStatus, ['completed', 'rejected', 'cancelled'])): ?>
<div class="card">
<h3 style="margin-bottom: 16px;">الإجراءات</h3>
<!-- Approve with step -->
<form method="POST" action="/transfers/<?= $transfer['id'] ?>/approve" class="mb-4" data-validate>
<?= Auth::csrfField() ?>
<div class="grid grid-2 gap-4">
<div class="form-group">
<label class="form-label">الموافقة على مرحلة</label>
<select name="step" class="form-input" required>
<option value="player_consent">موافقة اللاعب</option>
<option value="from_org_approved">المنظمة المرسلة</option>
<option value="to_org_approved">المنظمة المستقبلة</option>
</select>
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" class="form-input" placeholder="ملاحظات اختيارية">
</div>
</div>
<button type="submit" class="btn btn-primary">موافقة</button>
</form>
<div class="flex gap-3">
<!-- Complete -->
<form method="POST" action="/transfers/<?= $transfer['id'] ?>/complete" style="margin:0;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-primary">إكمال الانتقال</button>
</form>
<!-- Reject -->
<form method="POST" action="/transfers/<?= $transfer['id'] ?>/reject" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من رفض هذا الانتقال؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger">رفض</button>
</form>
<!-- Cancel -->
<form method="POST" action="/transfers/<?= $transfer['id'] ?>/cancel" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من إلغاء هذا الانتقال؟')">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-ghost" style="color: var(--danger);">إلغاء</button>
</form>
</div>
</div>
<?php endif; ?>
<?php
class OrgTreasuryController
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function index(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$treasury = $this->db->selectOne('org_treasury', ['org_id' => "eq.{$orgId}"]);
if (!$treasury) {
$treasury = $this->db->insert('org_treasury', ['org_id' => $orgId]);
}
$recentTransactions = $this->db->select('org_treasury_transactions', [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'created_at.desc',
'limit' => 10,
]);
$playerIds = array_filter(array_column($recentTransactions, 'player_id'));
$players = [];
if (!empty($playerIds)) {
$idList = '(' . implode(',', $playerIds) . ')';
$profiles = $this->db->select('profiles', [
'select' => 'id,username,display_name',
'id' => "in.{$idList}",
]);
foreach ($profiles as $p) {
$players[$p['id']] = $p;
}
}
$pageTitle = "خزينة المنظمة - {$org['name']}";
$moduleCSS = 'org-treasury';
$moduleJS = 'org-treasury';
View::render('org-treasury/index', compact(
'org', 'treasury', 'recentTransactions', 'players',
'pageTitle', 'moduleCSS', 'moduleJS'
));
}
public function transactions(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$type = $_GET['type'] ?? '';
$currency = $_GET['currency'] ?? '';
$queryParams = [
'select' => '*',
'org_id' => "eq.{$orgId}",
'order' => 'created_at.desc',
];
if ($type) {
$queryParams['type'] = "eq.{$type}";
}
if ($currency) {
$queryParams['currency'] = "eq.{$currency}";
}
$countParams = $queryParams;
unset($countParams['select'], $countParams['order']);
$total = $this->db->count('org_treasury_transactions', $countParams);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset;
$queryParams['limit'] = $pagination->perPage;
$transactions = $this->db->select('org_treasury_transactions', $queryParams);
$playerIds = array_filter(array_column($transactions, 'player_id'));
$players = [];
if (!empty($playerIds)) {
$idList = '(' . implode(',', $playerIds) . ')';
$profiles = $this->db->select('profiles', [
'select' => 'id,username,display_name',
'id' => "in.{$idList}",
]);
foreach ($profiles as $p) {
$players[$p['id']] = $p;
}
}
$pageTitle = "سجل المعاملات - {$org['name']}";
$moduleCSS = 'org-treasury';
View::render('org-treasury/transactions', compact(
'org', 'transactions', 'players', 'pagination', 'type', 'currency',
'pageTitle', 'moduleCSS'
));
}
public function deposit(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$currency = $_POST['currency'] ?? 'coins';
$amount = (int)($_POST['amount'] ?? 0);
$reason = trim($_POST['reason'] ?? '');
if ($amount <= 0) {
Response::error('يجب إدخال مبلغ صحيح', "/organizations/{$orgId}/treasury");
return;
}
if (!in_array($currency, ['coins', 'gems'])) {
Response::error('عملة غير صالحة', "/organizations/{$orgId}/treasury");
return;
}
$treasury = $this->db->selectOne('org_treasury', ['org_id' => "eq.{$orgId}"]);
if (!$treasury) {
$treasury = $this->db->insert('org_treasury', ['org_id' => $orgId]);
}
$balanceField = "balance_{$currency}";
$totalField = "total_deposited_{$currency}";
$newBalance = ($treasury[$balanceField] ?? 0) + $amount;
$this->db->update('org_treasury', ['org_id' => "eq.{$orgId}"], [
$balanceField => $newBalance,
$totalField => ($treasury[$totalField] ?? 0) + $amount,
'updated_at' => date('c'),
]);
$this->db->insert('org_treasury_transactions', [
'org_id' => $orgId,
'type' => 'admin_grant',
'currency' => $currency,
'amount' => $amount,
'balance_after' => $newBalance,
'reason' => $reason ?: 'إيداع بواسطة الإدارة',
'approved_by' => Auth::user()['id'],
]);
AuditLog::log('treasury_deposit', 'org_treasury', $orgId, null, [
'currency' => $currency,
'amount' => $amount,
]);
Response::success("تم إيداع {$amount} {$currency}", "/organizations/{$orgId}/treasury");
}
public function withdraw(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$currency = $_POST['currency'] ?? 'coins';
$amount = (int)($_POST['amount'] ?? 0);
$reason = trim($_POST['reason'] ?? '');
if ($amount <= 0) {
Response::error('يجب إدخال مبلغ صحيح', "/organizations/{$orgId}/treasury");
return;
}
$treasury = $this->db->selectOne('org_treasury', ['org_id' => "eq.{$orgId}"]);
if (!$treasury) {
Response::error('لا توجد خزينة', "/organizations/{$orgId}/treasury");
return;
}
$balanceField = "balance_{$currency}";
$currentBalance = $treasury[$balanceField] ?? 0;
if ($amount > $currentBalance) {
Response::error('الرصيد غير كاف', "/organizations/{$orgId}/treasury");
return;
}
$totalField = "total_withdrawn_{$currency}";
$newBalance = $currentBalance - $amount;
$this->db->update('org_treasury', ['org_id' => "eq.{$orgId}"], [
$balanceField => $newBalance,
$totalField => ($treasury[$totalField] ?? 0) + $amount,
'updated_at' => date('c'),
]);
$this->db->insert('org_treasury_transactions', [
'org_id' => $orgId,
'type' => 'admin_revoke',
'currency' => $currency,
'amount' => $amount,
'balance_after' => $newBalance,
'reason' => $reason ?: 'سحب بواسطة الإدارة',
'approved_by' => Auth::user()['id'],
]);
AuditLog::log('treasury_withdraw', 'org_treasury', $orgId, null, [
'currency' => $currency,
'amount' => $amount,
]);
Response::success("تم سحب {$amount} {$currency}", "/organizations/{$orgId}/treasury");
}
public function settings(array $params, string $method): void
{
$orgId = $params['id'];
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]);
if (!$org) {
Response::error('المنظمة غير موجودة', '/organizations');
return;
}
$treasury = $this->db->selectOne('org_treasury', ['org_id' => "eq.{$orgId}"]);
$settings = json_decode($treasury['settings'] ?? '{}', true);
$pageTitle = "إعدادات الخزينة - {$org['name']}";
$moduleCSS = 'org-treasury';
View::render('org-treasury/settings', compact('org', 'treasury', 'settings', 'pageTitle', 'moduleCSS'));
}
public function updateSettings(array $params, string $method): void
{
Auth::requireCsrf();
$orgId = $params['id'];
$settings = [
'auto_collect_rate' => (float)($_POST['auto_collect_rate'] ?? 0),
'max_deposit_per_day' => (int)($_POST['max_deposit_per_day'] ?? 0),
'withdrawal_approval_required' => isset($_POST['withdrawal_approval_required']),
'min_withdrawal_amount' => (int)($_POST['min_withdrawal_amount'] ?? 0),
];
$this->db->update('org_treasury', ['org_id' => "eq.{$orgId}"], [
'settings' => json_encode($settings),
'updated_at' => date('c'),
]);
AuditLog::log('update_treasury_settings', 'org_treasury', $orgId, null, $settings);
Response::success('تم تحديث إعدادات الخزينة', "/organizations/{$orgId}/treasury/settings");
}
}
<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'] ?>/treasury/transactions" class="btn btn-ghost">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
كل المعاملات
</a>
</div>
<!-- Balance Cards -->
<div class="grid grid-2 gap-4 mb-4">
<div class="card">
<div class="flex items-center gap-3 mb-2">
<div class="stat-icon blue" style="width: 48px; height: 48px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v12M8 10h8M8 14h8"/></svg>
</div>
<div>
<p class="text-sm text-muted">رصيد العملات</p>
<p class="text-2xl font-bold"><?= number_format($treasury['coins_balance'] ?? 0) ?></p>
</div>
</div>
</div>
<div class="card">
<div class="flex items-center gap-3 mb-2">
<div class="stat-icon purple" style="width: 48px; height: 48px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<p class="text-sm text-muted">رصيد الجواهر</p>
<p class="text-2xl font-bold"><?= number_format($treasury['gems_balance'] ?? 0) ?></p>
</div>
</div>
</div>
</div>
<!-- Stats Row -->
<div class="grid grid-2 gap-4 mb-4">
<div class="card" style="background: var(--bg-secondary);">
<p class="text-sm text-muted">إجمالي الإيداعات</p>
<p class="text-lg font-semibold" style="color: var(--success);"><?= number_format($treasury['total_deposited'] ?? 0) ?></p>
</div>
<div class="card" style="background: var(--bg-secondary);">
<p class="text-sm text-muted">إجمالي السحب</p>
<p class="text-lg font-semibold" style="color: var(--danger);"><?= number_format($treasury['total_withdrawn'] ?? 0) ?></p>
</div>
</div>
<!-- Deposit Form -->
<div class="card mb-4">
<h3 style="margin-bottom: 16px;">إيداع</h3>
<form method="POST" action="/organizations/<?= $org['id'] ?>/treasury/deposit" data-validate>
<?= Auth::csrfField() ?>
<div class="grid grid-3 gap-4">
<div class="form-group">
<label class="form-label">العملة</label>
<select name="currency" class="form-input" required>
<option value="coins">عملات</option>
<option value="gems">جواهر</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المبلغ</label>
<input type="number" name="amount" class="form-input" min="1" required placeholder="0">
</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>
<!-- Withdraw Form -->
<div class="card mb-4">
<h3 style="margin-bottom: 16px;">سحب</h3>
<form method="POST" action="/organizations/<?= $org['id'] ?>/treasury/withdraw" data-validate>
<?= Auth::csrfField() ?>
<div class="grid grid-3 gap-4">
<div class="form-group">
<label class="form-label">العملة</label>
<select name="currency" class="form-input" required>
<option value="coins">عملات</option>
<option value="gems">جواهر</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المبلغ</label>
<input type="number" name="amount" class="form-input" min="1" required placeholder="0">
</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-danger" style="margin-top: 8px;">سحب</button>
</form>
</div>
<!-- Recent Transactions -->
<div class="card">
<div class="flex items-center justify-between mb-4">
<h3>آخر المعاملات</h3>
<a href="/organizations/<?= $org['id'] ?>/treasury/transactions" class="btn btn-ghost btn-sm">عرض الكل</a>
</div>
<?php if (empty($recentTransactions)): ?>
<p class="text-muted">لا توجد معاملات بعد</p>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>النوع</th>
<th>العملة</th>
<th>المبلغ</th>
<th>الرصيد بعد</th>
<th>اللاعب</th>
<th>السبب</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentTransactions as $tx): ?>
<tr>
<td>
<?php
$typeLabels = ['deposit' => 'إيداع', 'withdrawal' => 'سحب', 'transfer' => 'تحويل', 'fee' => 'رسوم'];
$typeBadges = ['deposit' => 'badge-success', 'withdrawal' => 'badge-danger', 'transfer' => 'badge-info', 'fee' => 'badge-warning'];
$txType = $tx['type'] ?? 'deposit';
?>
<span class="badge <?= $typeBadges[$txType] ?? 'badge-default' ?>"><?= $typeLabels[$txType] ?? $txType ?></span>
</td>
<td><?= ($tx['currency'] ?? 'coins') === 'gems' ? 'جواهر' : 'عملات' ?></td>
<td style="color: <?= in_array($txType, ['deposit', 'transfer']) ? 'var(--success)' : 'var(--danger)' ?>; font-weight: 600;">
<?= in_array($txType, ['deposit', 'transfer']) ? '+' : '-' ?><?= number_format($tx['amount'] ?? 0) ?>
</td>
<td><?= number_format($tx['balance_after'] ?? 0) ?></td>
<td><?= View::e($players[$tx['player_id'] ?? '']['name'] ?? '-') ?></td>
<td><?= View::e($tx['reason'] ?? '-') ?></td>
<td><?= date('Y-m-d H:i', strtotime($tx['created_at'])) ?></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'] ?>/treasury" 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>
<!-- Filters -->
<div class="card mb-4">
<form method="GET" action="/organizations/<?= $org['id'] ?>/treasury/transactions" class="flex gap-4 items-end">
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">النوع</label>
<select name="type" class="form-input">
<option value="">الكل</option>
<option value="deposit" <?= ($type ?? '') === 'deposit' ? 'selected' : '' ?>>إيداع</option>
<option value="withdrawal" <?= ($type ?? '') === 'withdrawal' ? 'selected' : '' ?>>سحب</option>
<option value="transfer" <?= ($type ?? '') === 'transfer' ? 'selected' : '' ?>>تحويل</option>
<option value="fee" <?= ($type ?? '') === 'fee' ? 'selected' : '' ?>>رسوم</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">العملة</label>
<select name="currency" class="form-input">
<option value="">الكل</option>
<option value="coins" <?= ($currency ?? '') === 'coins' ? 'selected' : '' ?>>عملات</option>
<option value="gems" <?= ($currency ?? '') === 'gems' ? 'selected' : '' ?>>جواهر</option>
</select>
</div>
<button type="submit" class="btn btn-primary">تصفية</button>
</form>
</div>
<!-- Transactions Table -->
<div class="data-table-wrapper">
<?php if (empty($transactions)): ?>
<div class="empty-state">
<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>
</tr>
</thead>
<tbody>
<?php foreach ($transactions as $tx): ?>
<?php
$typeLabels = ['deposit' => 'إيداع', 'withdrawal' => 'سحب', 'transfer' => 'تحويل', 'fee' => 'رسوم'];
$typeBadges = ['deposit' => 'badge-success', 'withdrawal' => 'badge-danger', 'transfer' => 'badge-info', 'fee' => 'badge-warning'];
$txType = $tx['type'] ?? 'deposit';
$isCredit = in_array($txType, ['deposit', 'transfer']);
?>
<tr>
<td>
<span class="badge <?= $typeBadges[$txType] ?? 'badge-default' ?>"><?= $typeLabels[$txType] ?? $txType ?></span>
</td>
<td><?= ($tx['currency'] ?? 'coins') === 'gems' ? 'جواهر' : 'عملات' ?></td>
<td style="color: <?= $isCredit ? 'var(--success)' : 'var(--danger)' ?>; font-weight: 600;">
<?= $isCredit ? '+' : '-' ?><?= number_format($tx['amount'] ?? 0) ?>
</td>
<td><?= number_format($tx['balance_after'] ?? 0) ?></td>
<td><?= View::e($players[$tx['player_id'] ?? '']['name'] ?? '-') ?></td>
<td><?= View::e($tx['reason'] ?? '-') ?></td>
<td><?= date('Y-m-d H:i', strtotime($tx['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 ?>&type=<?= urlencode($type ?? '') ?>&currency=<?= urlencode($currency ?? '') ?>" 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 ?>&type=<?= urlencode($type ?? '') ?>&currency=<?= urlencode($currency ?? '') ?>" class="btn btn-ghost btn-sm">التالي</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
...@@ -278,4 +278,147 @@ class PlayersController ...@@ -278,4 +278,147 @@ class PlayersController
AuditLog::log('revoke', 'player', $id, null, ['currency' => $currency, 'amount' => $amount]); AuditLog::log('revoke', 'player', $id, null, ['currency' => $currency, 'amount' => $amount]);
Response::success("تم سحب {$amount} {$currency}", "/players/{$id}"); Response::success("تم سحب {$amount} {$currency}", "/players/{$id}");
} }
public function uploadAvatar(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$storage = SupabaseStorage::getInstance();
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$maxSize = 2 * 1024 * 1024;
$error = $storage->validateFile($_FILES['avatar'] ?? [], $allowedMimes, $maxSize);
if ($error) {
Response::error($error, "/players/{$id}");
return;
}
$player = $this->db->selectOne('profiles', ['id' => "eq.{$id}"]);
if (!$player) {
Response::error('اللاعب غير موجود', '/players');
return;
}
if (!empty($player['avatar_url']) && str_contains($player['avatar_url'], '/storage/v1/object/public/avatars/')) {
$oldPath = str_replace(SUPABASE_URL . '/storage/v1/object/public/avatars/', '', $player['avatar_url']);
$storage->delete('avatars', $oldPath);
}
$path = $storage->generatePath("players/{$id}", $_FILES['avatar']['name']);
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['avatar']['tmp_name']);
$url = $storage->upload('avatars', $path, $_FILES['avatar']['tmp_name'], $mime);
$this->db->update('profiles', ['id' => "eq.{$id}"], ['avatar_url' => $url, 'updated_at' => date('c')]);
AuditLog::log('upload_avatar', 'player', $id);
Response::success('تم رفع الصورة الشخصية', "/players/{$id}");
}
public function removeAvatar(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$player = $this->db->selectOne('profiles', ['id' => "eq.{$id}"]);
if (!$player) {
Response::error('اللاعب غير موجود', '/players');
return;
}
if (!empty($player['avatar_url']) && str_contains($player['avatar_url'], '/storage/v1/object/public/avatars/')) {
$storage = SupabaseStorage::getInstance();
$oldPath = str_replace(SUPABASE_URL . '/storage/v1/object/public/avatars/', '', $player['avatar_url']);
$storage->delete('avatars', $oldPath);
}
$this->db->update('profiles', ['id' => "eq.{$id}"], ['avatar_url' => null, 'updated_at' => date('c')]);
AuditLog::log('remove_avatar', 'player', $id);
Response::success('تم إزالة الصورة الشخصية', "/players/{$id}");
}
public function setFrame(array $params, string $method): void
{
Auth::requireCsrf();
$id = $params['id'];
$frameId = $_POST['frame_id'] ?? null;
$player = $this->db->selectOne('profiles', ['id' => "eq.{$id}"]);
if (!$player) {
Response::error('اللاعب غير موجود', '/players');
return;
}
if (empty($frameId) || $frameId === 'none') {
$this->db->update('player_frames', [
'player_id' => "eq.{$id}",
'is_equipped' => 'eq.true',
], ['is_equipped' => false]);
$this->db->update('profiles', ['id' => "eq.{$id}"], [
'avatar_frame_id' => null,
'active_org_frame_id' => null,
'updated_at' => date('c'),
]);
Response::success('تم إزالة الإطار', "/players/{$id}");
return;
}
$frame = $this->db->selectOne('profile_frames', ['id' => "eq.{$frameId}"]);
if (!$frame) {
Response::error('الإطار غير موجود', "/players/{$id}");
return;
}
$owned = $this->db->selectOne('player_frames', [
'player_id' => "eq.{$id}",
'frame_id' => "eq.{$frameId}",
]);
if (!$owned) {
$this->db->insert('player_frames', [
'player_id' => $id,
'frame_id' => $frameId,
'acquisition_type' => 'admin_grant',
'is_equipped' => true,
]);
} else {
$this->db->update('player_frames', [
'player_id' => "eq.{$id}",
'is_equipped' => 'eq.true',
], ['is_equipped' => false]);
$this->db->update('player_frames', [
'player_id' => "eq.{$id}",
'frame_id' => "eq.{$frameId}",
], ['is_equipped' => true]);
}
$updateData = ['avatar_frame_id' => $frameId, 'updated_at' => date('c')];
if ($frame['category'] === 'org' && !empty($frame['org_id'])) {
$updateData['active_org_frame_id'] = $frameId;
}
$this->db->update('profiles', ['id' => "eq.{$id}"], $updateData);
AuditLog::log('set_frame', 'player', $id, null, ['frame_id' => $frameId]);
Response::success('تم تعيين الإطار', "/players/{$id}");
}
public function apiSearch(array $params, string $method): void
{
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
header('Content-Type: application/json');
echo json_encode([]);
return;
}
$players = $this->db->select('profiles', [
'select' => 'id,username,display_name,avatar_url',
'or' => "(username.ilike.*{$q}*,display_name.ilike.*{$q}*)",
'limit' => 20,
'order' => 'username.asc',
]);
header('Content-Type: application/json');
echo json_encode($players);
}
} }
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