Commit 3a5ae9a4 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(org-leaderboards): rank from what the game writes

The pages read org_leaderboards and org_seasonal_rankings, which nothing in
the game writes (both empty), and "recalculate" counted match_players and
tournament_participants, which do not exist, so every board was empty.

The boards are now computed live for each org's active members from
player_game_ratings (games, wins, losses, draws, best streak, best rating per
game) and tournament_registrations (entered, won = final_standing 1): a
cross-org table ranked by wins, and a per-org board ranked by wins or, for
one game, by rating. There is no seasonal org ranking in the game, so that
page and the recalculate action are gone (the old URL redirects).
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 38add483
...@@ -382,7 +382,6 @@ return [ ...@@ -382,7 +382,6 @@ return [
// Org Leaderboards // Org Leaderboards
'org-leaderboards' => ['module' => 'org-leaderboards', 'action' => 'index'], 'org-leaderboards' => ['module' => 'org-leaderboards', 'action' => 'index'],
'org-leaderboards/seasonal' => ['module' => 'org-leaderboards', 'action' => 'seasonal'], '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'], 'org-leaderboards/{orgId}' => ['module' => 'org-leaderboards', 'action' => 'orgBoard'],
'api/org-leaderboards/{orgId}' => ['module' => 'org-leaderboards', 'action' => 'apiLeaderboard'], 'api/org-leaderboards/{orgId}' => ['module' => 'org-leaderboards', 'action' => 'apiLeaderboard'],
......
<?php <?php
/**
* Org leaderboards, computed live from what the game actually writes:
* player_game_ratings (rating, games, wins, losses, draws per game and mode)
* and tournament_registrations (tournaments entered, final_standing 1 = won),
* for each org's active members (org_members.status = 'active').
*
* This used to read org_leaderboards / org_seasonal_rankings, which nothing in
* the game writes (both empty), and its "recalculate" counted match_players
* and tournament_participants, tables that do not exist. Those are gone; there
* is no seasonal org ranking in the game, so that page is gone too.
*/
class OrgLeaderboardsController class OrgLeaderboardsController
{ {
private Database $db; private Database $db;
public const GAMES = ['all' => 'كل الألعاب', 'chess' => 'شطرنج', 'ludo' => 'لودو', 'domino' => 'دومينو', 'backgammon' => 'طاولة'];
public function __construct() public function __construct()
{ {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
public function index(array $params, string $method): void private static function game(): string
{ {
$queryParams = [ $g = (string)($_GET['game'] ?? 'all');
'select' => '*, el3ab_organizations(id,name,name_ar,logo_url)', return isset(self::GAMES[$g]) ? $g : 'all';
'order' => 'total_points.desc', }
];
$total = $this->db->count('org_seasonal_rankings', []);
$pagination = Pagination::fromRequest($total);
$queryParams['offset'] = $pagination->offset; /** Orgs ranked by their active members' wins (or, for one game, also by average rating). */
$queryParams['limit'] = $pagination->perPage; public function index(array $params, string $method): void
{
$game = self::game();
$q = ['select' => 'id,name,name_ar,logo_url,tag,member_count', 'is_active' => 'eq.true', 'order' => 'member_count.desc', 'limit' => 1000];
OrgScope::filter($q, ['id']);
$orgs = $this->db->select('el3ab_organizations', $q);
$members = $this->activeMembers(array_column($orgs, 'id'));
$stats = $this->playerStats(array_values(array_unique(array_merge(...array_values($members ?: [[]])))), $game);
$rows = [];
foreach ($orgs as $o) {
$r = ['org' => $o, 'members' => 0, 'players' => 0, 'games' => 0, 'wins' => 0, 'tournaments' => 0, 'tournaments_won' => 0, 'rating_sum' => 0, 'rated' => 0];
foreach ($members[$o['id']] ?? [] as $pid) {
$r['members']++;
$s = $stats[$pid] ?? null;
if (!$s) continue;
if ($s['games'] > 0) $r['players']++;
$r['games'] += $s['games'];
$r['wins'] += $s['wins'];
$r['tournaments'] += $s['tournaments'];
$r['tournaments_won'] += $s['tournaments_won'];
if ($s['rating'] !== null) { $r['rating_sum'] += $s['rating']; $r['rated']++; }
}
$r['avg_rating'] = $r['rated'] ? (int)round($r['rating_sum'] / $r['rated']) : null;
$rows[] = $r;
}
usort($rows, fn($a, $b) => [$b['wins'], $b['tournaments_won'], $b['games']] <=> [$a['wins'], $a['tournaments_won'], $a['games']]);
$rankings = $this->db->select('org_seasonal_rankings', $queryParams); $pageTitle = 'متصدرو المنظمات';
View::render('org-leaderboards/index', compact('rows', 'game', 'pageTitle'));
}
$pageTitle = 'لوحة المتصدرين العامة'; /** One org's active members, ranked by rating for one game, or by wins across all games. */
public function orgBoard(array $params, string $method): void
{
$org = $this->org($params['orgId'] ?? '');
$game = self::game();
$leaderboard = $this->board($org['id'], $game);
$pageTitle = 'المتصدرين - ' . ((($org['name_ar'] ?? '') ?: $org['name']));
View::render('org-leaderboards/org-board', compact('org', 'leaderboard', 'game', 'pageTitle'));
}
View::render('org-leaderboards/index', compact('rankings', 'pagination', 'pageTitle')); /** There is no seasonal org ranking in the game; the page that showed an empty table is gone. */
public function seasonal(array $params, string $method): void
{
header('Location: /org-leaderboards');
exit;
} }
public function orgBoard(array $params, string $method): void public function apiLeaderboard(array $params, string $method): void
{ {
$orgId = $params['orgId']; $org = $this->org($params['orgId'] ?? '');
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]); header('Content-Type: application/json');
echo json_encode(array_map(fn($r) => [
'player_id' => $r['player_id'], 'username' => $r['player']['username'] ?? null, 'display_name' => $r['player']['display_name'] ?? null,
'role' => $r['role'], 'rating' => $r['rating'], 'games' => $r['games'], 'wins' => $r['wins'], 'losses' => $r['losses'], 'draws' => $r['draws'],
'tournaments' => $r['tournaments'], 'tournaments_won' => $r['tournaments_won'],
], array_slice($this->board($org['id'], self::game()), 0, 50)), JSON_UNESCAPED_UNICODE);
}
// ─── Internals ──────────────────────────────────────────────────────
private function org(string $id): array
{
$org = preg_match('/^[0-9a-f-]{36}$/i', $id) ? $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$id}"]) : null;
if (!$org) { if (!$org) {
http_response_code(404); http_response_code(404);
View::render('errors/404'); View::render('errors/404');
return; exit;
} }
return $org;
$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 private function board(string $orgId, string $game): array
{ {
$season = $_GET['season'] ?? ''; $rows = $this->db->select('org_members', ['select' => 'player_id,role,joined_at', 'org_id' => "eq.{$orgId}", 'status' => 'eq.active', 'limit' => 10000]);
$ids = array_column($rows, 'player_id');
$queryParams = [ $stats = $this->playerStats($ids, $game);
'select' => '*, el3ab_organizations(id,name,name_ar,logo_url)', $profiles = [];
'order' => 'total_points.desc', foreach (array_chunk($ids, 150) as $chunk) {
]; foreach ($this->db->select('profiles', ['select' => 'id,username,display_name,avatar_url', 'id' => 'in.(' . implode(',', $chunk) . ')']) as $p) $profiles[$p['id']] = $p;
if ($season) {
$queryParams['season'] = "eq.{$season}";
} }
$empty = ['rating' => null, 'games' => 0, 'wins' => 0, 'losses' => 0, 'draws' => 0, 'best_streak' => 0, 'tournaments' => 0, 'tournaments_won' => 0];
$countParams = $queryParams; $out = [];
unset($countParams['select'], $countParams['order']); foreach ($rows as $m) {
$total = $this->db->count('org_seasonal_rankings', $countParams); $out[] = ['player_id' => $m['player_id'], 'role' => $m['role'], 'player' => $profiles[$m['player_id']] ?? null] + ($stats[$m['player_id']] ?? $empty);
$pagination = Pagination::fromRequest($total); }
usort($out, $game === 'all'
$queryParams['offset'] = $pagination->offset; ? fn($a, $b) => [$b['wins'], $b['tournaments_won'], $b['games']] <=> [$a['wins'], $a['tournaments_won'], $a['games']]
$queryParams['limit'] = $pagination->perPage; : fn($a, $b) => [$b['rating'] ?? -1, $b['wins'], $b['games']] <=> [$a['rating'] ?? -1, $a['wins'], $a['games']]);
return $out;
$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 /** org id => [player ids] of active members. */
private function activeMembers(array $orgIds): array
{ {
Auth::requireCsrf(); $out = [];
foreach (array_chunk($orgIds, 100) as $chunk) {
$orgId = $params['orgId']; $offset = 0;
$org = $this->db->selectOne('el3ab_organizations', ['id' => "eq.{$orgId}"]); do {
$rows = $this->db->select('org_members', [
if (!$org) { 'select' => 'org_id,player_id', 'org_id' => 'in.(' . implode(',', $chunk) . ')', 'status' => 'eq.active',
Response::error('المنظمة غير موجودة', '/org-leaderboards'); 'order' => 'id.asc', 'offset' => $offset, 'limit' => 1000,
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,
]); ]);
} foreach ($rows as $r) $out[$r['org_id']][] = $r['player_id'];
$offset += 1000;
} while (count($rows) === 1000);
} }
return $out;
AuditLog::log('recalculate', 'org_leaderboard', $orgId, null, ['members_count' => count($members)]);
Response::success('تم إعادة حساب النقاط بنجاح', "/org-leaderboards/{$orgId}");
} }
public function apiLeaderboard(array $params, string $method): void /**
* player id => totals. For one game, rating is the player's best rating
* across that game's modes; for all games it is null (ratings differ per game).
*/
private function playerStats(array $playerIds, string $game): array
{ {
$orgId = $params['orgId']; $out = [];
$new = fn() => ['rating' => null, 'games' => 0, 'wins' => 0, 'losses' => 0, 'draws' => 0, 'best_streak' => 0, 'tournaments' => 0, 'tournaments_won' => 0];
$leaderboard = $this->db->select('org_leaderboards', [ foreach (array_chunk(array_values(array_unique(array_filter($playerIds))), 150) as $chunk) {
'select' => '*, profiles(id,username,display_name,avatar_url)', $in = 'in.(' . implode(',', $chunk) . ')';
'org_id' => "eq.{$orgId}", $q = ['select' => 'player_id,game_key,rating,games_played,wins,losses,draws,best_win_streak', 'player_id' => $in, 'limit' => 5000];
'order' => 'points.desc', if ($game !== 'all') $q['game_key'] = "eq.{$game}";
'limit' => 50, foreach ($this->db->select('player_game_ratings', $q) as $r) {
]); $s = $out[$r['player_id']] ?? $new();
$s['games'] += (int)$r['games_played'];
header('Content-Type: application/json'); $s['wins'] += (int)$r['wins'];
echo json_encode($leaderboard); $s['losses'] += (int)$r['losses'];
$s['draws'] += (int)$r['draws'];
$s['best_streak'] = max($s['best_streak'], (int)$r['best_win_streak']);
if ($game !== 'all' && $r['rating'] !== null && (int)$r['games_played'] > 0) $s['rating'] = max($s['rating'] ?? 0, (int)$r['rating']);
$out[$r['player_id']] = $s;
}
foreach ($this->db->select('tournament_registrations', ['select' => 'player_id,final_standing,withdrawn_at', 'player_id' => $in, 'is_bot' => 'is.false', 'limit' => 5000]) as $t) {
if (!empty($t['withdrawn_at'])) continue;
$s = $out[$t['player_id']] ?? $new();
$s['tournaments']++;
if ((int)($t['final_standing'] ?? 0) === 1) $s['tournaments_won']++;
$out[$t['player_id']] = $s;
}
}
return $out;
} }
} }
<?php
/** Expects: $rows (per org: org, members, players, games, wins, tournaments, tournaments_won, avg_rating), $game */
$medal = fn($rank) => $rank <= 3 ? '<span class="badge ' . ($rank === 1 ? 'badge-warning' : ($rank === 2 ? 'badge-default' : 'badge-info')) . '">#' . $rank . '</span>' : '<span class="text-muted">#' . $rank . '</span>';
?>
<div class="content-header"> <div class="content-header">
<h1>لوحة المتصدرين العامة</h1> <h1>متصدرو المنظمات</h1>
<a href="/org-leaderboards/seasonal" class="btn btn-ghost">التصنيف الموسمي</a>
</div> </div>
<div class="filter-pills mb-4">
<?php foreach (OrgLeaderboardsController::GAMES as $k => $l): ?>
<a href="/org-leaderboards?game=<?= $k ?>" class="filter-pill <?= $game === $k ? 'active' : '' ?>"><?= $l ?></a>
<?php endforeach; ?>
</div>
<p class="text-sm text-muted mb-4">محسوبة الآن من نتائج اللعب الفعلية (تقييمات اللاعبين في كل لعبة وتسجيلات البطولات) لأعضاء كل منظمة النشطين. الترتيب بعدد الانتصارات ثم البطولات المحسومة.</p>
<div class="data-table-wrapper"> <div class="data-table-wrapper">
<?php if (empty($rankings)): ?> <?php if (empty($rows)): ?>
<div class="empty-state"> <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> <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> <h3 class="empty-state-title">لا توجد منظمات نشطة</h3>
<p class="empty-state-text">لم يتم تسجيل أي تصنيفات بعد</p>
</div> </div>
<?php else: ?> <?php else: ?>
<table class="data-table"> <table class="data-table">
...@@ -16,57 +26,40 @@ ...@@ -16,57 +26,40 @@
<tr> <tr>
<th style="width: 60px;">المركز</th> <th style="width: 60px;">المركز</th>
<th>المنظمة</th> <th>المنظمة</th>
<th>النقاط</th> <th>الأعضاء</th>
<th>فوز مباريات</th> <th>لعبوا</th>
<th>المباريات</th>
<th>الانتصارات</th>
<th>البطولات</th>
<th>فوز بطولات</th> <th>فوز بطولات</th>
<th>فوز تحديات</th> <?php if ($game !== 'all'): ?><th>متوسط التقييم</th><?php endif; ?>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($rankings as $index => $ranking): ?> <?php foreach ($rows as $i => $r): $org = $r['org']; ?>
<?php
$rank = $pagination->offset + $index + 1;
$org = $ranking['el3ab_organizations'] ?? null;
$orgName = $org['name_ar'] ?? $org['name'] ?? '-';
?>
<tr> <tr>
<td> <td><?= $medal($i + 1) ?></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> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<?php if (!empty($org['logo_url'])): ?> <?php if (!empty($org['logo_url'])): ?>
<img src="<?= View::e($org['logo_url']) ?>" alt="" style="width:24px;height:24px;border-radius:4px;"> <img src="<?= View::e($org['logo_url']) ?>" alt="" style="width:24px;height:24px;border-radius:4px;">
<?php endif; ?> <?php endif; ?>
<span><?= View::e($orgName) ?></span> <span><?= View::e(($org['name_ar'] ?? '') ?: $org['name']) ?></span>
<?php if (!empty($org['tag'])): ?><span class="text-xs text-muted" dir="ltr"><?= View::e($org['tag']) ?></span><?php endif; ?>
</div> </div>
</td> </td>
<td><strong class="tabular-nums"><?= number_format($ranking['total_points'] ?? 0) ?></strong></td> <td class="tabular-nums"><?= number_format($r['members']) ?></td>
<td class="tabular-nums"><?= number_format($ranking['matches_won'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($r['players']) ?></td>
<td class="tabular-nums"><?= number_format($ranking['tournaments_won'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($r['games']) ?></td>
<td class="tabular-nums"><?= number_format($ranking['challenges_won'] ?? 0) ?></td> <td><strong class="tabular-nums"><?= number_format($r['wins']) ?></strong></td>
<td> <td class="tabular-nums"><?= number_format($r['tournaments']) ?></td>
<?php if ($org): ?> <td class="tabular-nums"><?= number_format($r['tournaments_won']) ?></td>
<a href="/org-leaderboards/<?= $org['id'] ?>" class="btn btn-sm btn-ghost">التفاصيل</a> <?php if ($game !== 'all'): ?><td class="tabular-nums"><?= $r['avg_rating'] === null ? '-' : number_format($r['avg_rating']) ?></td><?php endif; ?>
<?php endif; ?> <td><a href="/org-leaderboards/<?= $org['id'] ?>?game=<?= $game ?>" class="btn btn-sm btn-ghost">التفاصيل</a></td>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php
/** Expects: $org, $leaderboard (per active member), $game */
$roles = ['owner' => 'مالك', 'admin' => 'مدير', 'arbiter' => 'حكم', 'moderator' => 'مشرف'];
?>
<div class="content-header"> <div class="content-header">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<a href="/organizations/<?= $org['id'] ?>" class="btn btn-icon btn-ghost"> <a href="/organizations/<?= $org['id'] ?>" class="btn btn-icon btn-ghost" aria-label="رجوع">
<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> <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> </a>
<h1>المتصدرين - <?= View::e($org['name_ar'] ?? $org['name']) ?></h1> <h1>المتصدرين - <?= View::e(($org['name_ar'] ?? '') ?: $org['name']) ?></h1>
</div> </div>
<form method="POST" action="/org-leaderboards/recalculate/<?= $org['id'] ?>" style="margin:0;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<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> </div>
<!-- Filters --> <div class="filter-pills mb-4">
<div class="card mb-5"> <?php foreach (OrgLeaderboardsController::GAMES as $k => $l): ?>
<form method="GET" action="/org-leaderboards/<?= $org['id'] ?>" class="flex gap-4 items-end flex-wrap"> <a href="/org-leaderboards/<?= $org['id'] ?>?game=<?= $k ?>" class="filter-pill <?= $game === $k ? 'active' : '' ?>"><?= $l ?></a>
<div class="form-group" style="margin-bottom:0;"> <?php endforeach; ?>
<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> </div>
<!-- Leaderboard Table --> <p class="text-sm text-muted mb-4"><?= $game === 'all'
? 'الأعضاء النشطون مرتبون بالانتصارات في كل الألعاب، من نتائج اللعب الفعلية.'
: 'الأعضاء النشطون مرتبون بأعلى تقييم لهم في ' . View::e(OrgLeaderboardsController::GAMES[$game]) . '، من نتائج اللعب الفعلية.' ?></p>
<div class="data-table-wrapper"> <div class="data-table-wrapper">
<?php if (empty($leaderboard)): ?> <?php if (empty($leaderboard)): ?>
<div class="empty-state"> <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> <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> <h3 class="empty-state-title">لا يوجد أعضاء نشطون</h3>
<p class="empty-state-text">لم يتم تسجيل أي نقاط بعد. استخدم زر "إعادة الحساب" لتحديث البيانات.</p>
</div> </div>
<?php else: ?> <?php else: ?>
<table class="data-table"> <table class="data-table">
...@@ -39,25 +33,21 @@ ...@@ -39,25 +33,21 @@
<tr> <tr>
<th style="width: 60px;">المركز</th> <th style="width: 60px;">المركز</th>
<th>اللاعب</th> <th>اللاعب</th>
<th>النقاط</th> <?php if ($game !== 'all'): ?><th>التقييم</th><?php endif; ?>
<th>المباريات</th> <th>المباريات</th>
<th>الفوز</th> <th>فوز</th>
<th>خسارة</th>
<th>تعادل</th>
<th>أفضل سلسلة</th>
<th>البطولات</th> <th>البطولات</th>
<th>فوز بطولات</th> <th>فوز بطولات</th>
<th>سلسلة حالية</th>
<th>أفضل سلسلة</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($leaderboard as $index => $entry): ?> <?php foreach ($leaderboard as $i => $e): $rank = $i + 1; $p = $e['player']; ?>
<?php
$rank = $pagination->offset + $index + 1;
$player = $entry['profiles'] ?? null;
$playerName = $player['display_name'] ?? $player['username'] ?? substr($entry['player_id'], 0, 8) . '...';
?>
<tr> <tr>
<td> <td>
<?php if ($rank <= 3): ?> <?php if ($rank <= 3 && $e['games'] > 0): ?>
<span class="badge <?= $rank === 1 ? 'badge-warning' : ($rank === 2 ? 'badge-default' : 'badge-info') ?>">#<?= $rank ?></span> <span class="badge <?= $rank === 1 ? 'badge-warning' : ($rank === 2 ? 'badge-default' : 'badge-info') ?>">#<?= $rank ?></span>
<?php else: ?> <?php else: ?>
<span class="text-muted">#<?= $rank ?></span> <span class="text-muted">#<?= $rank ?></span>
...@@ -65,31 +55,24 @@ ...@@ -65,31 +55,24 @@
</td> </td>
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<?php if (!empty($player['avatar_url'])): ?> <?php if (!empty($p['avatar_url'])): ?>
<img src="<?= View::e($player['avatar_url']) ?>" alt="" style="width:24px;height:24px;border-radius:50%;"> <img src="<?= View::e($p['avatar_url']) ?>" alt="" style="width:24px;height:24px;border-radius:50%;">
<?php endif; ?> <?php endif; ?>
<span><?= View::e($playerName) ?></span> <span><?= View::e($p ? ($p['display_name'] ?: $p['username']) : substr($e['player_id'], 0, 8)) ?></span>
<?php if (($e['role'] ?? 'member') !== 'member'): ?><span class="text-xs text-muted"><?= View::e($roles[$e['role']] ?? $e['role']) ?></span><?php endif; ?>
</div> </div>
</td> </td>
<td><strong class="tabular-nums"><?= number_format($entry['points'] ?? 0) ?></strong></td> <?php if ($game !== 'all'): ?><td><strong class="tabular-nums"><?= $e['rating'] === null ? '-' : number_format($e['rating']) ?></strong></td><?php endif; ?>
<td class="tabular-nums"><?= number_format($entry['matches_played'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($e['games']) ?></td>
<td class="tabular-nums"><?= number_format($entry['matches_won'] ?? 0) ?></td> <td class="tabular-nums"><?php if ($game === 'all'): ?><strong><?= number_format($e['wins']) ?></strong><?php else: ?><?= number_format($e['wins']) ?><?php endif; ?></td>
<td class="tabular-nums"><?= number_format($entry['tournaments_played'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($e['losses']) ?></td>
<td class="tabular-nums"><?= number_format($entry['tournaments_won'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($e['draws']) ?></td>
<td class="tabular-nums"><?= number_format($entry['streak_current'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($e['best_streak']) ?></td>
<td class="tabular-nums"><?= number_format($entry['streak_best'] ?? 0) ?></td> <td class="tabular-nums"><?= number_format($e['tournaments']) ?></td>
<td class="tabular-nums"><?= number_format($e['tournaments_won']) ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </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; ?> <?php endif; ?>
</div> </div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment