Commit 0a0a15dc authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add 3-tier sports dashboard with time filters, metrics, and PDF export

- Club level: total revenue, player demographics, discipline breakdown, utilization
- Sport level: discipline-specific KPIs, coach performance, academy links
- Facility level: bookings, revenue, utilization rate, popular time slots
- Reusable time filter partial (day/week/month/year/3year/5year/custom)
- PDF export via PdfExportService
- DashboardMetricsService for centralized metric calculations
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 90e3df69
<?php
declare(strict_types=1);
namespace App\Modules\SportsDashboard\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsDashboard\Services\DashboardMetricsService;
use App\Shared\Services\PdfExportService;
class SportsDashboardController extends Controller
{
public function clubLevel(Request $request): Response
{
$this->authorize('sports_dashboard.view');
$period = trim((string) $request->get('period', 'month'));
$from = trim((string) $request->get('from', ''));
$to = trim((string) $request->get('to', ''));
$range = DashboardMetricsService::getDateRange($period, $from ?: null, $to ?: null);
$start = $range['start'];
$end = $range['end'];
// Revenue with comparison
$revenueComparison = DashboardMetricsService::getPreviousPeriodComparison($start, $end, 'revenue');
// Player demographics
$demographics = DashboardMetricsService::getPlayerDemographics();
// Disciplines breakdown
$disciplines = DashboardMetricsService::getDisciplineBreakdown($start, $end);
// Facility utilization
$facilities = DashboardMetricsService::getFacilityUtilization($start, $end);
$avgUtilization = 0;
if (!empty($facilities)) {
$avgUtilization = round(array_sum(array_column($facilities, 'utilization')) / count($facilities), 1);
}
return $this->view('SportsDashboard.Views.club_dashboard', [
'period' => $period,
'from' => $from,
'to' => $to,
'start' => $start,
'end' => $end,
'revenueComparison' => $revenueComparison,
'demographics' => $demographics,
'disciplines' => $disciplines,
'facilities' => $facilities,
'avgUtilization' => $avgUtilization,
'baseUrl' => '/sports-dashboard',
'export' => false,
]);
}
public function sportLevel(Request $request, string $id): Response
{
$this->authorize('sports_dashboard.view');
$db = App::getInstance()->db();
$discipline = $db->selectOne("SELECT * FROM disciplines WHERE id = ? AND is_archived = 0", [(int) $id]);
if (!$discipline) {
return $this->redirect('/sports-dashboard')->withError('الرياضة غير موجودة');
}
$period = trim((string) $request->get('period', 'month'));
$from = trim((string) $request->get('from', ''));
$to = trim((string) $request->get('to', ''));
$range = DashboardMetricsService::getDateRange($period, $from ?: null, $to ?: null);
$start = $range['start'];
$end = $range['end'];
// Revenue for this discipline
$revenueComparison = DashboardMetricsService::getPreviousPeriodComparison($start, $end, 'revenue', null, (int) $id);
// Players for this discipline
$demographics = DashboardMetricsService::getPlayerDemographics((int) $id);
// Coaches for this discipline
$coaches = DashboardMetricsService::getCoachesForDiscipline((int) $id, $start, $end);
// Session count
$sessionCount = $db->selectOne("
SELECT COUNT(*) AS cnt FROM facility_zone_schedules
WHERE discipline_id = ? AND schedule_date BETWEEN ? AND ?
", [(int) $id, $start, $end]);
// Linked academies
$academies = $db->select("
SELECT a.id, a.name_ar, COUNT(DISTINCT ae.player_id) AS player_count
FROM academies a
LEFT JOIN academy_enrollments ae ON ae.academy_id = a.id AND ae.status = 'active'
WHERE a.discipline_id = ? AND a.is_archived = 0
GROUP BY a.id, a.name_ar
", [(int) $id]);
return $this->view('SportsDashboard.Views.sport_dashboard', [
'discipline' => $discipline,
'period' => $period,
'from' => $from,
'to' => $to,
'start' => $start,
'end' => $end,
'revenueComparison' => $revenueComparison,
'demographics' => $demographics,
'coaches' => $coaches,
'sessionCount' => (int) ($sessionCount['cnt'] ?? 0),
'academies' => $academies,
'baseUrl' => '/sports-dashboard/sport/' . $id,
'export' => false,
]);
}
public function facilityLevel(Request $request, string $id): Response
{
$this->authorize('sports_dashboard.view');
$db = App::getInstance()->db();
$facility = $db->selectOne("SELECT * FROM facilities WHERE id = ? AND is_archived = 0", [(int) $id]);
if (!$facility) {
return $this->redirect('/sports-dashboard')->withError('المرفق غير موجود');
}
$period = trim((string) $request->get('period', 'month'));
$from = trim((string) $request->get('from', ''));
$to = trim((string) $request->get('to', ''));
$range = DashboardMetricsService::getDateRange($period, $from ?: null, $to ?: null);
$start = $range['start'];
$end = $range['end'];
// Revenue for this facility
$revenueComparison = DashboardMetricsService::getPreviousPeriodComparison($start, $end, 'revenue', (int) $id);
// Bookings count
$bookings = $db->selectOne("
SELECT COUNT(*) AS cnt FROM reservations
WHERE facility_id = ? AND reservation_date BETWEEN ? AND ? AND status NOT IN ('cancelled')
", [(int) $id, $start, $end]);
// Utilization
$utilization = DashboardMetricsService::getFacilityUtilization($start, $end, (int) $id);
$utilizationRate = !empty($utilization) ? $utilization[0]['utilization'] : 0;
// Popular time slots
$popularSlots = DashboardMetricsService::getPopularTimeSlots((int) $id, $start, $end);
return $this->view('SportsDashboard.Views.facility_dashboard', [
'facility' => $facility,
'period' => $period,
'from' => $from,
'to' => $to,
'start' => $start,
'end' => $end,
'revenueComparison' => $revenueComparison,
'bookingsCount' => (int) ($bookings['cnt'] ?? 0),
'utilizationRate' => $utilizationRate,
'popularSlots' => $popularSlots,
'baseUrl' => '/sports-dashboard/facility/' . $id,
'export' => false,
]);
}
public function export(Request $request): Response
{
$this->authorize('sports_dashboard.export');
$level = trim((string) $request->get('level', 'club'));
$referenceId = (int) $request->get('reference_id', '0');
$period = trim((string) $request->get('period', 'month'));
$from = trim((string) $request->get('from', ''));
$to = trim((string) $request->get('to', ''));
$range = DashboardMetricsService::getDateRange($period, $from ?: null, $to ?: null);
$start = $range['start'];
$end = $range['end'];
$db = App::getInstance()->db();
if ($level === 'sport' && $referenceId > 0) {
$discipline = $db->selectOne("SELECT * FROM disciplines WHERE id = ?", [$referenceId]);
$revenueComparison = DashboardMetricsService::getPreviousPeriodComparison($start, $end, 'revenue', null, $referenceId);
$demographics = DashboardMetricsService::getPlayerDemographics($referenceId);
$coaches = DashboardMetricsService::getCoachesForDiscipline($referenceId, $start, $end);
$title = 'تقرير الرياضة: ' . ($discipline['name_ar'] ?? '');
} elseif ($level === 'facility' && $referenceId > 0) {
$facility = $db->selectOne("SELECT * FROM facilities WHERE id = ?", [$referenceId]);
$revenueComparison = DashboardMetricsService::getPreviousPeriodComparison($start, $end, 'revenue', $referenceId);
$bookings = $db->selectOne("SELECT COUNT(*) AS cnt FROM reservations WHERE facility_id = ? AND reservation_date BETWEEN ? AND ? AND status NOT IN ('cancelled')", [$referenceId, $start, $end]);
$popularSlots = DashboardMetricsService::getPopularTimeSlots($referenceId, $start, $end);
$title = 'تقرير المرفق: ' . ($facility['name_ar'] ?? '');
} else {
$revenueComparison = DashboardMetricsService::getPreviousPeriodComparison($start, $end, 'revenue');
$demographics = DashboardMetricsService::getPlayerDemographics();
$disciplines = DashboardMetricsService::getDisciplineBreakdown($start, $end);
$facilities = DashboardMetricsService::getFacilityUtilization($start, $end);
$title = 'تقرير لوحة المعلومات الرياضية';
}
// Build export HTML
$html = $this->buildExportHtml($level, $title, $start, $end, $revenueComparison ?? [], $demographics ?? [], $disciplines ?? [], $facilities ?? [], $coaches ?? [], $bookings ?? [], $popularSlots ?? []);
$filename = 'sports_dashboard_' . $level . '_' . date('Ymd_His') . '.pdf';
return PdfExportService::renderToPdf($html, $filename);
}
private function buildExportHtml(string $level, string $title, string $start, string $end, array $revenueComparison, array $demographics, array $disciplines, array $facilities, array $coaches, array $bookings, array $popularSlots): string
{
$html = '<!DOCTYPE html><html dir="rtl" lang="ar"><head><meta charset="utf-8"><title>' . htmlspecialchars($title) . '</title>';
$html .= '<style>body{font-family:Arial,sans-serif;padding:30px;direction:rtl;} table{width:100%;border-collapse:collapse;margin:15px 0;} th,td{border:1px solid #ddd;padding:8px;text-align:right;font-size:13px;} th{background:#f5f5f5;} .header{text-align:center;margin-bottom:30px;} .stat{display:inline-block;width:22%;text-align:center;padding:15px;margin:5px;border:1px solid #ddd;border-radius:8px;}</style>';
$html .= '</head><body>';
$html .= '<div class="header"><h2>' . htmlspecialchars($title) . '</h2>';
$html .= '<p>الفترة: ' . $start . ' إلى ' . $end . ' | تاريخ التقرير: ' . date('Y-m-d H:i') . '</p></div>';
// Revenue KPI
if (!empty($revenueComparison)) {
$arrow = $revenueComparison['direction'] === 'up' ? '&#x25B2;' : '&#x25BC;';
$html .= '<div style="text-align:center;margin:20px 0;">';
$html .= '<div class="stat"><strong>' . number_format($revenueComparison['current'], 2) . ' ج.م</strong><br>الإيرادات الحالية</div>';
$html .= '<div class="stat"><strong>' . number_format($revenueComparison['previous'], 2) . ' ج.م</strong><br>الفترة السابقة</div>';
$html .= '<div class="stat"><strong>' . $arrow . ' ' . $revenueComparison['change_pct'] . '%</strong><br>نسبة التغيير</div>';
$html .= '</div>';
}
// Demographics
if (!empty($demographics) && ($demographics['total'] ?? 0) > 0) {
$html .= '<div style="text-align:center;margin:20px 0;">';
$html .= '<div class="stat"><strong>' . $demographics['total'] . '</strong><br>إجمالي اللاعبين</div>';
$html .= '<div class="stat"><strong>' . $demographics['members'] . '</strong><br>أعضاء</div>';
$html .= '<div class="stat"><strong>' . $demographics['non_members'] . '</strong><br>غير أعضاء</div>';
$html .= '</div>';
}
// Disciplines table
if (!empty($disciplines)) {
$html .= '<h3>الرياضات</h3><table><tr><th>الرياضة</th><th>الإيرادات</th><th>عدد اللاعبين</th></tr>';
foreach ($disciplines as $d) {
$html .= '<tr><td>' . htmlspecialchars($d['name_ar'] ?? '') . '</td><td>' . number_format((float) ($d['revenue'] ?? 0), 2) . '</td><td>' . (int) ($d['player_count'] ?? 0) . '</td></tr>';
}
$html .= '</table>';
}
// Facilities table
if (!empty($facilities)) {
$html .= '<h3>المرافق</h3><table><tr><th>المرفق</th><th>الحجوزات</th><th>نسبة الإشغال</th></tr>';
foreach ($facilities as $f) {
$html .= '<tr><td>' . htmlspecialchars($f['name_ar'] ?? '') . '</td><td>' . (int) ($f['booked_slots'] ?? 0) . '</td><td>' . ($f['utilization'] ?? 0) . '%</td></tr>';
}
$html .= '</table>';
}
// Coaches table
if (!empty($coaches)) {
$html .= '<h3>المدربين</h3><table><tr><th>المدرب</th><th>عدد الحصص</th></tr>';
foreach ($coaches as $c) {
$html .= '<tr><td>' . htmlspecialchars($c['name_ar'] ?? '') . '</td><td>' . (int) ($c['session_count'] ?? 0) . '</td></tr>';
}
$html .= '</table>';
}
// Popular slots
if (!empty($popularSlots)) {
$html .= '<h3>الأوقات الأكثر حجزاً</h3><table><tr><th>الساعة</th><th>عدد الحجوزات</th><th>الإيرادات</th></tr>';
foreach ($popularSlots as $slot) {
$html .= '<tr><td>' . (int) $slot['hour_slot'] . ':00</td><td>' . (int) $slot['booking_count'] . '</td><td>' . number_format((float) ($slot['revenue'] ?? 0), 2) . '</td></tr>';
}
$html .= '</table>';
}
$html .= '</body></html>';
return $html;
}
}
<?php
declare(strict_types=1);
return [
['GET', '/sports-dashboard', 'SportsDashboard\Controllers\SportsDashboardController@clubLevel', ['auth'], 'sports_dashboard.view'],
['GET', '/sports-dashboard/sport/{id:\d+}', 'SportsDashboard\Controllers\SportsDashboardController@sportLevel', ['auth'], 'sports_dashboard.view'],
['GET', '/sports-dashboard/facility/{id:\d+}', 'SportsDashboard\Controllers\SportsDashboardController@facilityLevel', ['auth'], 'sports_dashboard.view'],
['GET', '/sports-dashboard/export', 'SportsDashboard\Controllers\SportsDashboardController@export', ['auth'], 'sports_dashboard.export'],
];
<?php
declare(strict_types=1);
namespace App\Modules\SportsDashboard\Services;
use App\Core\App;
final class DashboardMetricsService
{
public static function getDateRange(string $period, ?string $from = null, ?string $to = null): array
{
$end = $to ?? date('Y-m-d');
$start = match ($period) {
'day' => $end,
'week' => date('Y-m-d', strtotime('-7 days', strtotime($end))),
'month' => date('Y-m-d', strtotime('-1 month', strtotime($end))),
'year' => date('Y-m-d', strtotime('-1 year', strtotime($end))),
'3year' => date('Y-m-d', strtotime('-3 years', strtotime($end))),
'5year' => date('Y-m-d', strtotime('-5 years', strtotime($end))),
'custom' => $from ?? date('Y-m-d', strtotime('-1 month', strtotime($end))),
default => date('Y-m-d', strtotime('-1 month', strtotime($end))),
};
return ['start' => $start, 'end' => $end];
}
public static function getRevenue(string $start, string $end, ?int $facilityId = null, ?int $disciplineId = null): array
{
$db = App::getInstance()->db();
$where = "p.payment_date BETWEEN ? AND ? AND p.payment_type IN ('activity_booking','pool_booking','sports_subscription') AND p.is_voided = 0";
$params = [$start, $end];
if ($facilityId) {
$where .= " AND p.related_entity_type = 'facility' AND p.related_entity_id = ?";
$params[] = $facilityId;
}
if ($disciplineId) {
$where .= " AND p.related_entity_type = 'discipline' AND p.related_entity_id = ?";
$params[] = $disciplineId;
}
$total = $db->selectOne("SELECT COALESCE(SUM(p.amount), 0) as total FROM payments p WHERE {$where}", $params);
return ['total' => $total['total'] ?? '0.00'];
}
public static function getPlayerDemographics(?int $disciplineId = null): array
{
$db = App::getInstance()->db();
$where = "is_archived = 0";
$params = [];
if ($disciplineId) {
$where .= " AND id IN (SELECT player_id FROM activity_subscriptions WHERE discipline_id = ? AND status = 'active')";
$params[] = $disciplineId;
}
$members = $db->selectOne("SELECT COUNT(*) as cnt FROM players WHERE {$where} AND player_type = 'member'", $params);
$nonMembers = $db->selectOne("SELECT COUNT(*) as cnt FROM players WHERE {$where} AND player_type = 'non_member'", $params);
return [
'members' => (int) ($members['cnt'] ?? 0),
'non_members' => (int) ($nonMembers['cnt'] ?? 0),
'total' => (int) ($members['cnt'] ?? 0) + (int) ($nonMembers['cnt'] ?? 0),
];
}
public static function getDisciplineBreakdown(string $start, string $end): array
{
$db = App::getInstance()->db();
return $db->select("
SELECT d.id, d.name_ar, d.name_en,
COALESCE(SUM(p.amount), 0) AS revenue,
COUNT(DISTINCT asub.player_id) AS player_count
FROM disciplines d
LEFT JOIN activity_subscriptions asub ON asub.discipline_id = d.id AND asub.status = 'active'
LEFT JOIN payments p ON p.related_entity_type = 'discipline' AND p.related_entity_id = d.id
AND p.payment_date BETWEEN ? AND ? AND p.is_voided = 0
WHERE d.is_archived = 0
GROUP BY d.id, d.name_ar, d.name_en
ORDER BY revenue DESC
", [$start, $end]);
}
public static function getFacilityUtilization(string $start, string $end, ?int $facilityId = null): array
{
$db = App::getInstance()->db();
$params = [$start, $end];
$facilityWhere = '';
if ($facilityId) {
$facilityWhere = ' AND f.id = ?';
$params[] = $facilityId;
}
$facilities = $db->select("
SELECT f.id, f.name_ar,
(SELECT COUNT(*) FROM reservations r WHERE r.facility_id = f.id AND r.reservation_date BETWEEN ? AND ? AND r.status NOT IN ('cancelled')) AS booked_slots
FROM facilities f
WHERE f.is_archived = 0 {$facilityWhere}
", $params);
foreach ($facilities as &$fac) {
$slots = $db->select("SELECT COUNT(*) as cnt FROM facility_time_slots WHERE facility_id = ?", [(int) $fac['id']]);
$slotsPerDay = (int) ($slots[0]['cnt'] ?? 1);
$days = max(1, (int) ((strtotime($end) - strtotime($start)) / 86400) + 1);
$totalSlots = $slotsPerDay * $days;
$fac['total_slots'] = $totalSlots;
$fac['utilization'] = $totalSlots > 0 ? round(((int) $fac['booked_slots'] / $totalSlots) * 100, 1) : 0;
}
return $facilities;
}
public static function getPreviousPeriodComparison(string $currentStart, string $currentEnd, string $metric, ?int $facilityId = null, ?int $disciplineId = null): array
{
$duration = strtotime($currentEnd) - strtotime($currentStart);
$prevEnd = date('Y-m-d', strtotime($currentStart) - 86400);
$prevStart = date('Y-m-d', strtotime($prevEnd) - $duration);
$current = self::getRevenue($currentStart, $currentEnd, $facilityId, $disciplineId);
$previous = self::getRevenue($prevStart, $prevEnd, $facilityId, $disciplineId);
$currentVal = (float) $current['total'];
$previousVal = (float) $previous['total'];
$change = $previousVal > 0 ? round((($currentVal - $previousVal) / $previousVal) * 100, 1) : 0;
return [
'current' => $currentVal,
'previous' => $previousVal,
'change_pct' => $change,
'direction' => $change >= 0 ? 'up' : 'down',
];
}
public static function getCoachesForDiscipline(int $disciplineId, string $start, string $end): array
{
$db = App::getInstance()->db();
return $db->select("
SELECT DISTINCT c.id, c.name_ar, c.name_en,
(SELECT COUNT(*) FROM facility_zone_schedules fzs2 WHERE fzs2.coach_id = c.id AND fzs2.schedule_date BETWEEN ? AND ?) AS session_count
FROM coaches c
INNER JOIN facility_zone_schedules fzs ON fzs.coach_id = c.id
WHERE fzs.discipline_id = ? AND fzs.schedule_date BETWEEN ? AND ?
ORDER BY session_count DESC
", [$start, $end, $disciplineId, $start, $end]);
}
public static function getPopularTimeSlots(int $facilityId, string $start, string $end): array
{
$db = App::getInstance()->db();
return $db->select("
SELECT HOUR(start_time) AS hour_slot, COUNT(*) AS booking_count,
COALESCE(SUM(total_amount), 0) AS revenue
FROM reservations
WHERE facility_id = ? AND reservation_date BETWEEN ? AND ? AND status NOT IN ('cancelled')
GROUP BY hour_slot
ORDER BY booking_count DESC
LIMIT 10
", [$facilityId, $start, $end]);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>لوحة المعلومات الرياضية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="margin-bottom:20px;">
<h2 style="margin:0 0 5px;font-size:22px;">لوحة المعلومات الرياضية</h2>
<p style="color:#6B7280;font-size:14px;margin:0;">نظرة شاملة على أداء الأنشطة الرياضية بالنادي</p>
</div>
<?php $__template->include('Shared.Views._partials.time_filter', [
'period' => $period,
'from' => $from,
'to' => $to,
'baseUrl' => $baseUrl,
'export' => $export,
]); ?>
<!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:15px;margin-bottom:25px;">
<!-- Revenue -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #059669;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= money($revenueComparison['current'] ?? 0) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">إجمالي الإيرادات</div>
<?php if (($revenueComparison['change_pct'] ?? 0) != 0): ?>
<div style="margin-top:6px;font-size:12px;color:<?= $revenueComparison['direction'] === 'up' ? '#059669' : '#DC2626' ?>;">
<i data-lucide="<?= $revenueComparison['direction'] === 'up' ? 'trending-up' : 'trending-down' ?>" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
<?= $revenueComparison['change_pct'] ?>% مقارنة بالفترة السابقة
</div>
<?php endif; ?>
</div>
<!-- Total Players -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #2563EB;">
<div style="font-size:26px;font-weight:700;color:#2563EB;"><?= number_format($demographics['total'] ?? 0) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">إجمالي اللاعبين النشطين</div>
</div>
<!-- Member/Non-member ratio -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #7C3AED;">
<div style="font-size:26px;font-weight:700;color:#7C3AED;">
<?= $demographics['members'] ?? 0 ?> / <?= $demographics['non_members'] ?? 0 ?>
</div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">أعضاء / غير أعضاء</div>
</div>
<!-- Utilization -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #D97706;">
<div style="font-size:26px;font-weight:700;color:#D97706;"><?= $avgUtilization ?>%</div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">متوسط نسبة الإشغال</div>
</div>
</div>
<!-- Disciplines Breakdown -->
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;font-size:17px;">الرياضات والأنشطة</h3>
<?php if (!empty($disciplines)): ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>الرياضة</th>
<th>الإيرادات</th>
<th>عدد اللاعبين</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($disciplines as $disc): ?>
<tr>
<td style="font-weight:600;"><?= e($disc['name_ar'] ?? '') ?></td>
<td><?= money((float) ($disc['revenue'] ?? 0)) ?></td>
<td><?= (int) ($disc['player_count'] ?? 0) ?></td>
<td>
<a href="/sports-dashboard/sport/<?= (int) $disc['id'] ?>?period=<?= e($period) ?>&from=<?= e($from) ?>&to=<?= e($to) ?>" class="btn btn-outline" style="padding:4px 10px;font-size:12px;">
تفاصيل
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#6B7280;text-align:center;">لا توجد بيانات رياضات للفترة المحددة</p>
<?php endif; ?>
</div>
<!-- Facilities Utilization -->
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;font-size:17px;">إشغال المرافق</h3>
<?php if (!empty($facilities)): ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>المرفق</th>
<th>الحجوزات</th>
<th>نسبة الإشغال</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($facilities as $fac): ?>
<tr>
<td style="font-weight:600;"><?= e($fac['name_ar'] ?? '') ?></td>
<td><?= (int) ($fac['booked_slots'] ?? 0) ?></td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;max-width:120px;">
<div style="width:<?= min(100, $fac['utilization'] ?? 0) ?>%;background:<?= ($fac['utilization'] ?? 0) > 75 ? '#059669' : (($fac['utilization'] ?? 0) > 40 ? '#D97706' : '#DC2626') ?>;height:100%;border-radius:4px;"></div>
</div>
<span style="font-size:12px;font-weight:600;"><?= $fac['utilization'] ?? 0 ?>%</span>
</div>
</td>
<td>
<a href="/sports-dashboard/facility/<?= (int) $fac['id'] ?>?period=<?= e($period) ?>&from=<?= e($from) ?>&to=<?= e($to) ?>" class="btn btn-outline" style="padding:4px 10px;font-size:12px;">
تفاصيل
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#6B7280;text-align:center;">لا توجد بيانات مرافق للفترة المحددة</p>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>لوحة المرفق — <?= e($facility['name_ar'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="margin-bottom:20px;display:flex;align-items:center;gap:15px;">
<a href="/sports-dashboard?period=<?= e($period) ?>&from=<?= e($from) ?>&to=<?= e($to) ?>" class="btn btn-outline" style="padding:6px 12px;font-size:13px;">
<i data-lucide="arrow-right" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
العودة للوحة الرئيسية
</a>
<div>
<h2 style="margin:0 0 5px;font-size:22px;"><?= e($facility['name_ar'] ?? '') ?></h2>
<p style="color:#6B7280;font-size:14px;margin:0;">لوحة معلومات المرفق</p>
</div>
</div>
<?php $__template->include('Shared.Views._partials.time_filter', [
'period' => $period,
'from' => $from,
'to' => $to,
'baseUrl' => $baseUrl,
'export' => $export,
]); ?>
<!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:15px;margin-bottom:25px;">
<!-- Revenue -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #059669;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= money($revenueComparison['current'] ?? 0) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">الإيرادات</div>
<?php if (($revenueComparison['change_pct'] ?? 0) != 0): ?>
<div style="margin-top:6px;font-size:12px;color:<?= $revenueComparison['direction'] === 'up' ? '#059669' : '#DC2626' ?>;">
<i data-lucide="<?= $revenueComparison['direction'] === 'up' ? 'trending-up' : 'trending-down' ?>" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
<?= $revenueComparison['change_pct'] ?>% مقارنة بالفترة السابقة
</div>
<?php endif; ?>
</div>
<!-- Bookings -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #2563EB;">
<div style="font-size:26px;font-weight:700;color:#2563EB;"><?= number_format($bookingsCount) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">إجمالي الحجوزات</div>
</div>
<!-- Utilization Rate -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #7C3AED;">
<div style="font-size:26px;font-weight:700;color:#7C3AED;"><?= $utilizationRate ?>%</div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">نسبة الإشغال</div>
<div style="margin-top:8px;">
<div style="background:#E5E7EB;border-radius:4px;height:10px;max-width:180px;margin:0 auto;">
<div style="width:<?= min(100, $utilizationRate) ?>%;background:<?= $utilizationRate > 75 ? '#059669' : ($utilizationRate > 40 ? '#D97706' : '#DC2626') ?>;height:100%;border-radius:4px;"></div>
</div>
</div>
</div>
</div>
<!-- Popular Time Slots -->
<?php if (!empty($popularSlots)): ?>
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;font-size:17px;">الأوقات الأكثر حجزاً</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>الوقت</th>
<th>عدد الحجوزات</th>
<th>الإيرادات</th>
<th>الحصة</th>
</tr>
</thead>
<tbody>
<?php
$maxBookings = max(array_column($popularSlots, 'booking_count') ?: [1]);
foreach ($popularSlots as $slot):
$pct = $maxBookings > 0 ? round(((int) $slot['booking_count'] / $maxBookings) * 100) : 0;
?>
<tr>
<td style="font-weight:600;"><?= sprintf('%02d:00 - %02d:00', (int) $slot['hour_slot'], (int) $slot['hour_slot'] + 1) ?></td>
<td><?= (int) $slot['booking_count'] ?></td>
<td><?= money((float) ($slot['revenue'] ?? 0)) ?></td>
<td style="width:200px;">
<div style="background:#E5E7EB;border-radius:4px;height:8px;">
<div style="width:<?= $pct ?>%;background:#2563EB;height:100%;border-radius:4px;"></div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:30px;text-align:center;color:#6B7280;margin-bottom:25px;">
<i data-lucide="calendar-x" style="width:40px;height:40px;margin-bottom:10px;opacity:0.5;"></i>
<p>لا توجد حجوزات للفترة المحددة</p>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>لوحة الرياضة — <?= e($discipline['name_ar'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="margin-bottom:20px;display:flex;align-items:center;gap:15px;">
<a href="/sports-dashboard?period=<?= e($period) ?>&from=<?= e($from) ?>&to=<?= e($to) ?>" class="btn btn-outline" style="padding:6px 12px;font-size:13px;">
<i data-lucide="arrow-right" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
العودة للوحة الرئيسية
</a>
<div>
<h2 style="margin:0 0 5px;font-size:22px;"><?= e($discipline['name_ar'] ?? '') ?></h2>
<p style="color:#6B7280;font-size:14px;margin:0;">لوحة معلومات الرياضة</p>
</div>
</div>
<?php $__template->include('Shared.Views._partials.time_filter', [
'period' => $period,
'from' => $from,
'to' => $to,
'baseUrl' => $baseUrl,
'export' => $export,
]); ?>
<!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:15px;margin-bottom:25px;">
<!-- Revenue -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #059669;">
<div style="font-size:24px;font-weight:700;color:#059669;"><?= money($revenueComparison['current'] ?? 0) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">الإيرادات</div>
<?php if (($revenueComparison['change_pct'] ?? 0) != 0): ?>
<div style="margin-top:6px;font-size:12px;color:<?= $revenueComparison['direction'] === 'up' ? '#059669' : '#DC2626' ?>;">
<i data-lucide="<?= $revenueComparison['direction'] === 'up' ? 'trending-up' : 'trending-down' ?>" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
<?= $revenueComparison['change_pct'] ?>%
</div>
<?php endif; ?>
</div>
<!-- Players -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #2563EB;">
<div style="font-size:24px;font-weight:700;color:#2563EB;"><?= number_format($demographics['total'] ?? 0) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">اللاعبين المسجلين</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;"><?= $demographics['members'] ?? 0 ?> أعضاء | <?= $demographics['non_members'] ?? 0 ?> غير أعضاء</div>
</div>
<!-- Coaches -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #7C3AED;">
<div style="font-size:24px;font-weight:700;color:#7C3AED;"><?= count($coaches) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">المدربين</div>
</div>
<!-- Sessions -->
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #D97706;">
<div style="font-size:24px;font-weight:700;color:#D97706;"><?= number_format($sessionCount) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">الحصص التدريبية</div>
</div>
</div>
<!-- Coach Performance Table -->
<?php if (!empty($coaches)): ?>
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;font-size:17px;">أداء المدربين</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>#</th>
<th>المدرب</th>
<th>عدد الحصص</th>
</tr>
</thead>
<tbody>
<?php foreach ($coaches as $i => $coach): ?>
<tr>
<td><?= $i + 1 ?></td>
<td style="font-weight:600;"><?= e($coach['name_ar'] ?? '') ?></td>
<td><?= (int) ($coach['session_count'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Academies -->
<?php if (!empty($academies)): ?>
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;font-size:17px;">الأكاديميات المرتبطة</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>الأكاديمية</th>
<th>عدد اللاعبين</th>
</tr>
</thead>
<tbody>
<?php foreach ($academies as $academy): ?>
<tr>
<td style="font-weight:600;"><?= e($academy['name_ar'] ?? '') ?></td>
<td><?= (int) ($academy['player_count'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('sports_dashboard', [
'sports_dashboard.view' => ['ar' => 'عرض لوحة المعلومات الرياضية', 'en' => 'View Sports Dashboard'],
'sports_dashboard.export' => ['ar' => 'تصدير التقارير', 'en' => 'Export Reports'],
]);
MenuRegistry::register('sports_dashboard', [
'label_ar' => 'لوحة المعلومات الرياضية',
'icon' => 'chart-bar',
'route' => '/sports-dashboard',
'permission' => 'sports_dashboard.view',
'parent' => 'sports_activities',
'order' => 700,
]);
<?php
/**
* Reusable time period filter bar.
*
* @var string $period Current selected period (day|week|month|year|3year|5year|custom)
* @var string $from Custom start date (Y-m-d)
* @var string $to Custom end date (Y-m-d)
* @var string $baseUrl Base URL for navigation links
* @var bool $export If true, hide the export button
*/
$currentPeriod = $period ?? 'month';
$currentFrom = $from ?? '';
$currentTo = $to ?? '';
$baseUrl = $baseUrl ?? '';
$export = $export ?? false;
?>
<div class="card" style="padding:12px 20px;margin-bottom:20px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<?php
$periods = [
'day' => 'اليوم',
'week' => 'الأسبوع',
'month' => 'الشهر',
'year' => 'السنة',
'3year' => '3 سنوات',
'5year' => '5 سنوات',
'custom' => 'مخصص',
];
foreach ($periods as $key => $label): ?>
<a href="<?= e($baseUrl) ?>?period=<?= $key ?><?= $key === 'custom' && $currentFrom ? '&from=' . e($currentFrom) . '&to=' . e($currentTo) : '' ?>"
class="btn <?= $currentPeriod === $key ? 'btn-primary' : 'btn-outline' ?>"
style="padding:6px 14px;font-size:13px;"
<?= $key === 'custom' ? 'id="custom-period-btn"' : '' ?>>
<?= $label ?>
</a>
<?php endforeach; ?>
<div id="custom-range" style="display:<?= $currentPeriod === 'custom' ? 'flex' : 'none' ?>;gap:8px;align-items:center;margin-right:10px;">
<input type="date" id="filter-from" value="<?= e($currentFrom) ?>" class="form-input" style="width:150px;padding:5px;">
<span></span>
<input type="date" id="filter-to" value="<?= e($currentTo) ?>" class="form-input" style="width:150px;padding:5px;">
<button onclick="applyCustomRange()" class="btn btn-primary" style="padding:5px 12px;font-size:12px;">تطبيق</button>
</div>
<?php if (!$export): ?>
<a href="<?= e($baseUrl) ?>/export?level=club&period=<?= e($currentPeriod) ?>&from=<?= e($currentFrom) ?>&to=<?= e($currentTo) ?>" class="btn btn-outline" style="margin-right:auto;padding:6px 14px;font-size:13px;">
<i data-lucide="download" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
تصدير PDF
</a>
<?php endif; ?>
</div>
<script>
function applyCustomRange() {
var from = document.getElementById('filter-from').value;
var to = document.getElementById('filter-to').value;
if (!from || !to) { alert('يرجى تحديد تاريخ البداية والنهاية'); return; }
window.location.href = '<?= e($baseUrl) ?>?period=custom&from=' + from + '&to=' + to;
}
(function() {
var btn = document.getElementById('custom-period-btn');
if (btn) {
btn.addEventListener('click', function(e) {
if ('<?= $currentPeriod ?>' !== 'custom') {
e.preventDefault();
document.getElementById('custom-range').style.display = 'flex';
}
});
}
})();
</script>
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$db->raw("
CREATE TABLE dashboard_snapshots (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
dashboard_type ENUM('club','sport','facility') NOT NULL,
reference_id BIGINT UNSIGNED NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
metrics_json JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_dash_type_period (dashboard_type, period_start, period_end)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
};
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