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);
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