Commit 40f1c0c0 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(sa-reports, discounts): sports activity reports + membership discount fixes

Sports Activity Reports: player reports with filters (discipline/program/group/
player type/medical/payment status/branch) and finance reports (revenue/costs/
profit with daily/weekly/monthly/yearly/3yr/5yr/custom periods). CSV and PDF
export for both. Role-based access with 3 new permissions.

Membership Discounts: fix BillingService to include regulatory discount as bill
line item, add regulatory discount section to edit page, add FYI discount guide
to show page covering all 3 discount types (special, regulatory, board offers),
handle regulatory discount in update controller with document upload.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1f9a209d
...@@ -420,6 +420,10 @@ class MemberController extends Controller ...@@ -420,6 +420,10 @@ class MemberController extends Controller
: null, : null,
'installInterestRate' => (float) ($instRateData['percentage'] ?? 22), 'installInterestRate' => (float) ($instRateData['percentage'] ?? 22),
'installMaxMonths' => (int) ($instMonthsData['months'] ?? 30), 'installMaxMonths' => (int) ($instMonthsData['months'] ?? 30),
'regulatoryDiscount' => $member->regulatory_discount_id
? $db->selectOne("SELECT * FROM regulatory_discounts WHERE id = ?", [(int) $member->regulatory_discount_id])
: null,
'regulatoryDiscounts' => $db->select("SELECT * FROM regulatory_discounts WHERE is_active = 1 ORDER BY article_number"),
'boardOffers' => $boardOffers, 'boardOffers' => $boardOffers,
'bestOffer' => $bestOffer, 'bestOffer' => $bestOffer,
'cashDiscount' => $cashDiscount, 'cashDiscount' => $cashDiscount,
...@@ -843,6 +847,7 @@ class MemberController extends Controller ...@@ -843,6 +847,7 @@ class MemberController extends Controller
'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"), 'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"),
'isSuperAdmin' => self::isSuperAdmin(), 'isSuperAdmin' => self::isSuperAdmin(),
'specialDiscounts' => SpecialDiscount::allActive(), 'specialDiscounts' => SpecialDiscount::allActive(),
'regulatoryDiscounts' => $db->select("SELECT * FROM regulatory_discounts WHERE is_active = 1 ORDER BY article_number"),
'salesReps' => $salesReps, 'salesReps' => $salesReps,
]); ]);
} }
...@@ -968,6 +973,41 @@ class MemberController extends Controller ...@@ -968,6 +973,41 @@ class MemberController extends Controller
} }
} }
// ── Regulatory Discount handling ──
if (array_key_exists('regulatory_discount_id', $data)) {
$regDiscountId = trim((string) ($data['regulatory_discount_id'] ?? ''));
if ($regDiscountId === '' || $regDiscountId === '0') {
$update['regulatory_discount_id'] = null;
$update['regulatory_discount_amount'] = null;
} else {
$regRule = $db->selectOne("SELECT * FROM regulatory_discounts WHERE id = ? AND is_active = 1", [(int) $regDiscountId]);
if ($regRule) {
$update['regulatory_discount_id'] = (int) $regDiscountId;
$membershipValue = $member->membership_value ?? '0.00';
$pct = $regRule['discount_percentage'] ?? '0';
$maxPct = $regRule['max_discount_percentage'] ?? null;
$effectivePct = ($maxPct !== null && bccomp((string) $pct, (string) $maxPct, 2) > 0) ? $maxPct : $pct;
$update['regulatory_discount_amount'] = bcdiv(bcmul((string) $membershipValue, (string) $effectivePct, 4), '100', 2);
if (!empty($_FILES['regulatory_discount_document']['tmp_name'])) {
$file = $_FILES['regulatory_discount_document'];
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (in_array($mimeType, $allowedTypes) && $file['size'] <= 10485760) {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$storedName = 'reg_discount_' . (int) $id . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$uploadDir = App::getInstance()->basePath() . '/storage/uploads/discounts/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if (move_uploaded_file($file['tmp_name'], $uploadDir . $storedName)) {
$update['regulatory_discount_document'] = 'storage/uploads/discounts/' . $storedName;
}
}
}
}
}
}
if (!empty($_FILES['photo']) && PhotoUploadService::isUploaded($_FILES['photo'])) { if (!empty($_FILES['photo']) && PhotoUploadService::isUploaded($_FILES['photo'])) {
$photoValidation = PhotoUploadService::validate($_FILES['photo']); $photoValidation = PhotoUploadService::validate($_FILES['photo']);
if ($photoValidation['valid']) { if ($photoValidation['valid']) {
......
...@@ -472,6 +472,28 @@ final class BillingService ...@@ -472,6 +472,28 @@ final class BillingService
} }
} }
// ── 2c. Regulatory Discount ──
if (!empty($member['regulatory_discount_id'])) {
$regRow = $db->selectOne(
"SELECT * FROM regulatory_discounts WHERE id = ? AND is_active = 1",
[(int) $member['regulatory_discount_id']]
);
if ($regRow) {
$regAmount = $member['regulatory_discount_amount'] ?? '0.00';
if (bccomp((string) $regAmount, '0', 2) > 0) {
$regLabel = 'خصم لائحي: ' . $regRow['name_ar'] . ' (مادة ' . $regRow['article_number'] . ' — ' . $regRow['discount_percentage'] . '%)';
$items[] = [
'type' => 'regulatory_discount',
'label' => $regLabel,
'amount' => '-' . $regAmount,
'paid' => false,
'included' => false,
'category' => 'discount',
];
}
}
}
// ── 3. Spouses ── // ── 3. Spouses ──
$spouses = []; $spouses = [];
try { try {
......
...@@ -139,6 +139,76 @@ ...@@ -139,6 +139,76 @@
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Regulatory Discount -->
<?php if (!empty($regulatoryDiscounts)): ?>
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #0D7377;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="scale" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">خصم لائحي (مواد 97 — 102، 110)</h3>
</div>
<!-- FYI Table -->
<div style="margin-bottom:15px;background:#F0FDF4;border-radius:8px;padding:10px 14px;">
<p style="font-size:11px;color:#065F46;margin:0 0 6px;font-weight:600;">الخصومات اللائحية المتاحة (للعلم):</p>
<table style="width:100%;font-size:10px;border-collapse:collapse;">
<thead>
<tr>
<th style="padding:3px 6px;text-align:right;color:#065F46;border-bottom:1px solid #D1FAE5;">المادة</th>
<th style="padding:3px 6px;text-align:right;color:#065F46;border-bottom:1px solid #D1FAE5;">الوصف</th>
<th style="padding:3px 6px;text-align:center;color:#065F46;border-bottom:1px solid #D1FAE5;">النسبة</th>
</tr>
</thead>
<tbody>
<?php foreach ($regulatoryDiscounts as $rd): ?>
<tr>
<td style="padding:2px 6px;border-bottom:1px solid #ECFDF5;">مادة <?= e($rd['article_number']) ?></td>
<td style="padding:2px 6px;border-bottom:1px solid #ECFDF5;"><?= e($rd['name_ar']) ?></td>
<td style="padding:2px 6px;border-bottom:1px solid #ECFDF5;text-align:center;font-weight:600;color:#059669;"><?= e($rd['discount_percentage']) ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">الخصم اللائحي</label>
<select name="regulatory_discount_id" id="reg_discount_select" class="form-select" onchange="updateRegDiscountInfo()">
<option value="">— بدون خصم لائحي —</option>
<?php foreach ($regulatoryDiscounts as $rd): ?>
<option value="<?= (int) $rd['id'] ?>"
data-pct="<?= e($rd['discount_percentage']) ?>"
data-article="<?= e($rd['article_number']) ?>"
<?= ((int) ($member->regulatory_discount_id ?? 0)) === (int) $rd['id'] ? 'selected' : '' ?>>
مادة <?= e($rd['article_number']) ?><?= e($rd['name_ar']) ?> (<?= e($rd['discount_percentage']) ?>%)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" id="reg-discount-info" style="display:<?= $member->regulatory_discount_id ? 'block' : 'none' ?>;">
<label class="form-label">قيمة الخصم</label>
<div id="reg-discount-amount" style="font-size:18px;font-weight:700;color:#0D7377;padding:8px 0;">
<?php if ($member->regulatory_discount_amount && bccomp((string) $member->regulatory_discount_amount, '0', 2) > 0): ?>
<?= money($member->regulatory_discount_amount) ?>
<?php else: ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="form-group" style="margin-top:10px;">
<label class="form-label">مستند إثبات الخصم اللائحي (PDF, JPG, PNG)</label>
<input type="file" name="regulatory_discount_document" class="form-input" accept=".pdf,.jpg,.jpeg,.png" style="padding:8px;">
<?php if (!empty($member->regulatory_discount_document)): ?>
<small style="color:#059669;font-size:12px;display:block;margin-top:4px;">
<i data-lucide="file-check" style="width:12px;height:12px;vertical-align:middle;"></i>
مستند مرفق بالفعل — <a href="/<?= e($member->regulatory_discount_document) ?>" target="_blank" style="color:#0D7377;">عرض</a>
</small>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Profile Photo --> <!-- Profile Photo -->
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #7C3AED;"> <div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #7C3AED;">
<h3 style="color:#7C3AED;margin-bottom:15px;">الصورة الشخصية</h3> <h3 style="color:#7C3AED;margin-bottom:15px;">الصورة الشخصية</h3>
...@@ -179,5 +249,22 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -179,5 +249,22 @@ document.addEventListener('DOMContentLoaded', function() {
sel.addEventListener('change', updateDiscount); sel.addEventListener('change', updateDiscount);
updateDiscount(); updateDiscount();
}); });
function updateRegDiscountInfo() {
var regSel = document.getElementById('reg_discount_select');
var regInfo = document.getElementById('reg-discount-info');
var regAmount = document.getElementById('reg-discount-amount');
if (!regSel || !regInfo) return;
var opt = regSel.options[regSel.selectedIndex];
if (!opt || !opt.value) {
regInfo.style.display = 'none';
return;
}
var pct = parseFloat(opt.getAttribute('data-pct') || '0');
var membershipValue = parseFloat(<?= json_encode($member->membership_value ?? '0.00') ?>) || 0;
var amt = (membershipValue * pct / 100).toFixed(2);
regAmount.textContent = amt + ' ج.م';
regInfo.style.display = 'block';
}
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
\ No newline at end of file
...@@ -409,6 +409,94 @@ $_memberInitials = mb_substr($member->full_name_ar, 0, 2); ...@@ -409,6 +409,94 @@ $_memberInitials = mb_substr($member->full_name_ar, 0, 2);
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Regulatory Discount Status -->
<?php if (!empty($member->regulatory_discount_id) && !empty($regulatoryDiscount)): ?>
<div style="padding:15px 20px;border-top:1px solid #E5E7EB;">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<span style="background:#ECFDF5;color:#065F46;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600;">خصم لائحي</span>
<span style="color:#0D7377;font-weight:600;font-size:13px;">
<?= e($regulatoryDiscount['name_ar']) ?> (مادة <?= e($regulatoryDiscount['article_number']) ?><?= e($regulatoryDiscount['discount_percentage']) ?>%)
= -<?= money($member->regulatory_discount_amount ?? '0') ?>
</span>
<?php if (!empty($member->regulatory_discount_document)): ?>
<a href="/<?= e($member->regulatory_discount_document) ?>" target="_blank" style="color:#0D7377;font-size:12px;text-decoration:underline;">عرض المستند</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- FYI: All Available Discounts Guide -->
<?php if ($isInitialPhase): ?>
<div style="border-top:1px solid #E5E7EB;">
<div style="padding:12px 20px;cursor:pointer;display:flex;align-items:center;gap:8px;background:#F8FAFC;" onclick="var el=document.getElementById('fyi-discounts');el.style.display=el.style.display==='none'?'block':'none';this.querySelector('.fyi-arrow').textContent=el.style.display==='none'?'▼':'▲';">
<i data-lucide="info" style="width:16px;height:16px;color:#6366F1;"></i>
<span style="font-weight:600;font-size:13px;color:#6366F1;">دليل الخصومات المتاحة (للعلم)</span>
<span class="fyi-arrow" style="margin-right:auto;color:#6366F1;font-size:11px;">&#x25BC;</span>
</div>
<div id="fyi-discounts" style="display:none;padding:0 20px 20px;">
<!-- 1. Special Discounts -->
<div style="margin-bottom:16px;">
<h4 style="font-size:13px;color:#D97706;margin:12px 0 8px;border-bottom:1px solid #FEF3C7;padding-bottom:4px;">1. الخصومات الخاصة</h4>
<p style="font-size:12px;color:#6B7280;margin:0 0 6px;">خصومات نسبة أو مبلغ ثابت تُطبَّق من صفحة التعديل أو صفحة ملء الاستمارة. يمكن أن تتطلب مستند إثبات.</p>
<?php if (!empty($availableDiscounts)): ?>
<div style="font-size:12px;color:#374151;">
<strong>المتاح حالياً:</strong>
<?php foreach ($availableDiscounts as $ad): ?>
<span style="background:#FEF3C7;padding:2px 8px;border-radius:4px;margin:0 2px;"><?= e($ad['name_ar']) ?> (<?= e($ad['discount_percentage']) ?>%)</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- 2. Regulatory Discounts -->
<div style="margin-bottom:16px;">
<h4 style="font-size:13px;color:#0D7377;margin:0 0 8px;border-bottom:1px solid #D1FAE5;padding-bottom:4px;">2. الخصومات اللائحية (مواد 97 — 102، 110)</h4>
<p style="font-size:12px;color:#6B7280;margin:0 0 6px;">خصومات منصوص عليها في لائحة النادي — تُطبَّق أثناء ملء الاستمارة بعد التحقق من الأهلية واعتماد المسؤول.</p>
<?php if (!empty($regulatoryDiscounts)): ?>
<table style="width:100%;font-size:11px;border-collapse:collapse;margin-top:4px;">
<thead>
<tr style="background:#F0FDF4;">
<th style="padding:5px 8px;text-align:right;color:#065F46;border:1px solid #D1FAE5;">المادة</th>
<th style="padding:5px 8px;text-align:right;color:#065F46;border:1px solid #D1FAE5;">الوصف</th>
<th style="padding:5px 8px;text-align:center;color:#065F46;border:1px solid #D1FAE5;">النسبة</th>
<th style="padding:5px 8px;text-align:right;color:#065F46;border:1px solid #D1FAE5;">الإثبات المطلوب</th>
</tr>
</thead>
<tbody>
<?php foreach ($regulatoryDiscounts as $rd): ?>
<tr>
<td style="padding:4px 8px;border:1px solid #E5E7EB;font-weight:600;">مادة <?= e($rd['article_number']) ?></td>
<td style="padding:4px 8px;border:1px solid #E5E7EB;"><?= e($rd['name_ar']) ?></td>
<td style="padding:4px 8px;border:1px solid #E5E7EB;text-align:center;font-weight:600;color:#059669;"><?= e($rd['discount_percentage']) ?>%</td>
<td style="padding:4px 8px;border:1px solid #E5E7EB;font-size:10px;color:#6B7280;"><?= e($rd['proof_requirements'] ?? 'حسب نوع الخصم') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- 3. Board Offers -->
<div>
<h4 style="font-size:13px;color:#7C3AED;margin:0 0 8px;border-bottom:1px solid #EDE9FE;padding-bottom:4px;">3. عروض مجلس الإدارة</h4>
<p style="font-size:12px;color:#6B7280;margin:0 0 6px;">عروض سعرية بقرار مجلس الإدارة (خصم كاش أو شروط تقسيط). تظهر تلقائياً في قسم الدفع عندما تكون مفعّلة.</p>
<?php if (!empty($boardOffers)): ?>
<div style="font-size:12px;color:#374151;">
<strong>العروض المتاحة:</strong>
<?php foreach ($boardOffers as $bo): ?>
<span style="background:#EDE9FE;padding:2px 8px;border-radius:4px;margin:0 2px;"><?= e($bo['name_ar']) ?></span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="font-size:12px;color:#9CA3AF;margin:0;">لا توجد عروض نشطة حالياً.</p>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Type-specific registration CTA (seasonal/foreign/sports) --> <!-- Type-specific registration CTA (seasonal/foreign/sports) -->
<?php <?php
$typeDetailExists = match ($member->membership_type ?? 'working') { $typeDetailExists = match ($member->membership_type ?? 'working') {
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\SaPlayerReportService;
use App\Modules\SportsActivity\Services\SaFinanceReportService;
use App\Modules\Reports\Services\ReportExporter;
use App\Shared\Services\PdfExportService;
class SaReportController extends Controller
{
public function playerReport(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'discipline_id' => $request->get('discipline_id', ''),
'program_id' => $request->get('program_id', ''),
'group_id' => $request->get('group_id', ''),
'player_id' => $request->get('player_id', ''),
'player_type' => $request->get('player_type', ''),
'medical_status' => $request->get('medical_status', ''),
'payment_status' => $request->get('payment_status', ''),
'branch_id' => $request->get('branch_id', ''),
];
$hasFilters = array_filter($filters, fn($v) => $v !== '' && $v !== null);
$report = !empty($hasFilters) ? SaPlayerReportService::generate($filters) : null;
$disciplines = $db->select("SELECT id, name_ar FROM sa_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar");
$programs = $db->select("SELECT id, name_ar, discipline_id FROM sa_programs WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar");
$groups = $db->select("SELECT id, name_ar, program_id FROM sa_groups WHERE is_archived = 0 AND status = 'active' ORDER BY name_ar");
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1");
return $this->view('SportsActivity.Views.reports.players', [
'filters' => $filters,
'report' => $report,
'disciplines' => $disciplines,
'programs' => $programs,
'groups' => $groups,
'branches' => $branches,
]);
}
public function financeReport(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'discipline_id' => $request->get('discipline_id', ''),
'period_preset' => $request->get('period_preset', 'monthly'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'branch_id' => $request->get('branch_id', ''),
];
$report = SaFinanceReportService::generate($filters);
$disciplines = $db->select("SELECT id, name_ar FROM sa_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar");
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1");
return $this->view('SportsActivity.Views.reports.finance', [
'filters' => $filters,
'report' => $report,
'disciplines' => $disciplines,
'branches' => $branches,
]);
}
public function exportPlayerCsv(Request $request): Response
{
$filters = [
'discipline_id' => $request->get('discipline_id', ''),
'program_id' => $request->get('program_id', ''),
'group_id' => $request->get('group_id', ''),
'player_id' => $request->get('player_id', ''),
'player_type' => $request->get('player_type', ''),
'medical_status' => $request->get('medical_status', ''),
'payment_status' => $request->get('payment_status', ''),
'branch_id' => $request->get('branch_id', ''),
];
$report = SaPlayerReportService::generate($filters);
$csv = ReportExporter::toCsv($report, ['name' => 'player_report']);
$filename = 'تقرير_اللاعبين_' . date('Y-m-d_His') . '.csv';
$response = new Response();
return $response->html("\xEF\xBB\xBF" . $csv, 200)->withHeaders([
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}
public function exportPlayerPdf(Request $request): Response
{
$filters = [
'discipline_id' => $request->get('discipline_id', ''),
'program_id' => $request->get('program_id', ''),
'group_id' => $request->get('group_id', ''),
'player_id' => $request->get('player_id', ''),
'player_type' => $request->get('player_type', ''),
'medical_status' => $request->get('medical_status', ''),
'payment_status' => $request->get('payment_status', ''),
'branch_id' => $request->get('branch_id', ''),
];
$report = SaPlayerReportService::generate($filters);
$html = self::buildPlayerPdfHtml($report);
$filename = 'تقرير_اللاعبين_' . date('Y-m-d_His') . '.pdf';
return PdfExportService::renderToPdf($html, $filename);
}
public function exportFinanceCsv(Request $request): Response
{
$filters = [
'discipline_id' => $request->get('discipline_id', ''),
'period_preset' => $request->get('period_preset', 'monthly'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'branch_id' => $request->get('branch_id', ''),
];
$report = SaFinanceReportService::generate($filters);
$csv = ReportExporter::toCsv($report, ['name' => 'finance_report']);
$filename = 'التقرير_المالي_' . date('Y-m-d_His') . '.csv';
$response = new Response();
return $response->html("\xEF\xBB\xBF" . $csv, 200)->withHeaders([
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}
public function exportFinancePdf(Request $request): Response
{
$filters = [
'discipline_id' => $request->get('discipline_id', ''),
'period_preset' => $request->get('period_preset', 'monthly'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'branch_id' => $request->get('branch_id', ''),
];
$report = SaFinanceReportService::generate($filters);
$html = self::buildFinancePdfHtml($report);
$filename = 'التقرير_المالي_' . date('Y-m-d_His') . '.pdf';
return PdfExportService::renderToPdf($html, $filename);
}
private static function buildPlayerPdfHtml(array $report): string
{
$rows = $report['rows'] ?? [];
$columns = $report['columns'] ?? [];
$total = $report['total'] ?? 0;
$tableRows = '';
foreach ($rows as $row) {
$tableRows .= '<tr>';
foreach (array_keys($columns) as $key) {
$tableRows .= '<td>' . htmlspecialchars((string) ($row[$key] ?? '—'), ENT_QUOTES, 'UTF-8') . '</td>';
}
$tableRows .= '</tr>';
}
$headers = '';
foreach ($columns as $label) {
$headers .= '<th>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</th>';
}
return '<!DOCTYPE html><html dir="rtl" lang="ar"><head><meta charset="utf-8">
<style>
body{font-family:Arial,"Tahoma",sans-serif;font-size:11px;direction:rtl;margin:20px;}
h1{font-size:18px;text-align:center;margin-bottom:4px;color:#0D7377;}
.meta{text-align:center;font-size:10px;color:#6B7280;margin-bottom:16px;}
table{width:100%;border-collapse:collapse;margin-top:10px;}
th{background:#0D7377;color:#fff;padding:6px 8px;text-align:right;font-size:10px;white-space:nowrap;}
td{padding:5px 8px;border-bottom:1px solid #E5E7EB;font-size:10px;}
tr:nth-child(even){background:#F9FAFB;}
.total{margin-top:10px;font-weight:bold;font-size:12px;}
</style></head><body>
<h1>تقرير اللاعبين — الأنشطة الرياضية</h1>
<div class="meta">تاريخ التقرير: ' . date('Y-m-d H:i') . ' | إجمالي: ' . $total . ' لاعب</div>
<table><thead><tr>' . $headers . '</tr></thead><tbody>' . $tableRows . '</tbody></table>
<div class="total">إجمالي اللاعبين: ' . $total . '</div>
</body></html>';
}
private static function buildFinancePdfHtml(array $report): string
{
$summary = $report['summary'] ?? [];
$revenueBreakdown = $report['revenue_breakdown'] ?? [];
$costBreakdown = $report['cost_breakdown'] ?? [];
$trend = $report['monthly_trend'] ?? [];
$dateFrom = $report['date_from'] ?? '';
$dateTo = $report['date_to'] ?? '';
$revRows = '';
foreach ($revenueBreakdown as $item) {
$revRows .= '<tr><td>' . htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8') . '</td><td style="text-align:left;direction:ltr;">' . number_format((float) $item['amount'], 2) . '</td><td style="text-align:center;">' . (int) ($item['count'] ?? 0) . '</td></tr>';
}
$costRows = '';
foreach ($costBreakdown as $item) {
$costRows .= '<tr><td>' . htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8') . '</td><td style="text-align:left;direction:ltr;">' . number_format((float) $item['amount'], 2) . '</td><td></td></tr>';
}
$trendRows = '';
foreach ($trend as $t) {
$trendRows .= '<tr><td>' . htmlspecialchars($t['month'], ENT_QUOTES, 'UTF-8') . '</td><td style="text-align:left;direction:ltr;">' . number_format((float) $t['revenue'], 2) . '</td></tr>';
}
$totalRevenue = number_format((float) ($summary['total_revenue'] ?? 0), 2);
$totalCosts = number_format((float) ($summary['total_costs'] ?? 0), 2);
$netProfit = number_format((float) ($summary['net_profit'] ?? 0), 2);
$profitColor = bccomp($summary['net_profit'] ?? '0', '0', 2) >= 0 ? '#059669' : '#DC2626';
return '<!DOCTYPE html><html dir="rtl" lang="ar"><head><meta charset="utf-8">
<style>
body{font-family:Arial,"Tahoma",sans-serif;font-size:11px;direction:rtl;margin:20px;}
h1{font-size:18px;text-align:center;margin-bottom:4px;color:#0D7377;}
h2{font-size:14px;margin:16px 0 8px;color:#374151;border-bottom:1px solid #E5E7EB;padding-bottom:4px;}
.meta{text-align:center;font-size:10px;color:#6B7280;margin-bottom:16px;}
.cards{display:flex;gap:16px;margin-bottom:16px;}
.card{flex:1;padding:12px;border-radius:8px;text-align:center;}
.card-rev{background:#ECFDF5;color:#065F46;}
.card-cost{background:#FEF2F2;color:#991B1B;}
.card-profit{background:#EFF6FF;color:#1E40AF;}
.card .num{font-size:20px;font-weight:700;}
.card .lbl{font-size:10px;margin-top:2px;}
table{width:100%;border-collapse:collapse;}
th{background:#F3F4F6;padding:6px 8px;text-align:right;font-size:10px;font-weight:600;color:#374151;}
td{padding:5px 8px;border-bottom:1px solid #E5E7EB;font-size:10px;}
</style></head><body>
<h1>التقرير المالي — الأنشطة الرياضية</h1>
<div class="meta">الفترة: ' . htmlspecialchars($dateFrom, ENT_QUOTES, 'UTF-8') . ' إلى ' . htmlspecialchars($dateTo, ENT_QUOTES, 'UTF-8') . ' | تاريخ التقرير: ' . date('Y-m-d H:i') . '</div>
<div class="cards">
<div class="card card-rev"><div class="num">' . $totalRevenue . '</div><div class="lbl">إجمالي الإيرادات</div></div>
<div class="card card-cost"><div class="num">' . $totalCosts . '</div><div class="lbl">إجمالي التكاليف</div></div>
<div class="card card-profit"><div class="num" style="color:' . $profitColor . ';">' . $netProfit . '</div><div class="lbl">صافي الربح</div></div>
</div>
<h2>تفصيل الإيرادات</h2>
<table><thead><tr><th>البند</th><th style="text-align:left;">المبلغ (ج.م)</th><th style="text-align:center;">العدد</th></tr></thead><tbody>' . $revRows . '</tbody></table>
<h2>التكاليف</h2>
<table><thead><tr><th>البند</th><th style="text-align:left;">المبلغ (ج.م)</th><th></th></tr></thead><tbody>' . $costRows . '</tbody></table>
' . (!empty($trendRows) ? '<h2>الاتجاه الشهري</h2><table><thead><tr><th>الشهر</th><th style="text-align:left;">الإيرادات (ج.م)</th></tr></thead><tbody>' . $trendRows . '</tbody></table>' : '') . '
</body></html>';
}
}
...@@ -383,4 +383,12 @@ return [ ...@@ -383,4 +383,12 @@ return [
['POST', '/sa/players/{pid:\d+}/groups/{gid:\d+}/pause', 'SportsActivity\Controllers\PlayerLifecycleController@pause', ['auth', 'csrf'], 'sa.enrollment.manage'], ['POST', '/sa/players/{pid:\d+}/groups/{gid:\d+}/pause', 'SportsActivity\Controllers\PlayerLifecycleController@pause', ['auth', 'csrf'], 'sa.enrollment.manage'],
['POST', '/sa/players/{pid:\d+}/groups/{gid:\d+}/resume', 'SportsActivity\Controllers\PlayerLifecycleController@resume', ['auth', 'csrf'], 'sa.enrollment.manage'], ['POST', '/sa/players/{pid:\d+}/groups/{gid:\d+}/resume', 'SportsActivity\Controllers\PlayerLifecycleController@resume', ['auth', 'csrf'], 'sa.enrollment.manage'],
['POST', '/sa/players/{pid:\d+}/groups/{gid:\d+}/extend-grace', 'SportsActivity\Controllers\PlayerLifecycleController@extendGrace', ['auth', 'csrf'], 'sa.enrollment.manage'], ['POST', '/sa/players/{pid:\d+}/groups/{gid:\d+}/extend-grace', 'SportsActivity\Controllers\PlayerLifecycleController@extendGrace', ['auth', 'csrf'], 'sa.enrollment.manage'],
// ─── Reports ──────────────────────────────────────────────────────────────────
['GET', '/sa/reports/players', 'SportsActivity\Controllers\SaReportController@playerReport', ['auth'], 'sa.report.players'],
['GET', '/sa/reports/finance', 'SportsActivity\Controllers\SaReportController@financeReport', ['auth'], 'sa.report.finance'],
['GET', '/sa/reports/players/export-csv', 'SportsActivity\Controllers\SaReportController@exportPlayerCsv', ['auth'], 'sa.report.export'],
['GET', '/sa/reports/players/export-pdf', 'SportsActivity\Controllers\SaReportController@exportPlayerPdf', ['auth'], 'sa.report.export'],
['GET', '/sa/reports/finance/export-csv', 'SportsActivity\Controllers\SaReportController@exportFinanceCsv', ['auth'], 'sa.report.export'],
['GET', '/sa/reports/finance/export-pdf', 'SportsActivity\Controllers\SaReportController@exportFinancePdf', ['auth'], 'sa.report.export'],
]; ];
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
final class SaFinanceReportService
{
public static function generate(array $filters): array
{
$db = App::getInstance()->db();
[$dateFrom, $dateTo] = self::resolveDateRange($filters);
$disciplineId = !empty($filters['discipline_id']) ? (int) $filters['discipline_id'] : null;
$branchId = !empty($filters['branch_id']) ? (int) $filters['branch_id'] : null;
$subscriptionRevenue = self::getSubscriptionRevenue($db, $dateFrom, $dateTo, $disciplineId, $branchId);
$bookingRevenue = self::getBookingRevenue($db, $dateFrom, $dateTo, $disciplineId, $branchId);
$registrationRevenue = self::getRegistrationRevenue($db, $dateFrom, $dateTo, $branchId);
$coachCosts = self::getCoachCosts($db, $dateFrom, $dateTo, $disciplineId, $branchId);
$totalRevenue = bcadd(bcadd($subscriptionRevenue['total'], $bookingRevenue['total'], 2), $registrationRevenue['total'], 2);
$totalCosts = $coachCosts['total'];
$netProfit = bcsub($totalRevenue, $totalCosts, 2);
$monthlyTrend = self::getMonthlyTrend($db, $dateFrom, $dateTo, $disciplineId, $branchId);
$columns = [
'category' => 'البند',
'description' => 'التفاصيل',
'amount' => 'المبلغ',
];
$rows = [];
$rows[] = ['category' => 'إيرادات', 'description' => 'اشتراكات اللاعبين', 'amount' => $subscriptionRevenue['total']];
$rows[] = ['category' => 'إيرادات', 'description' => 'حجوزات المرافق', 'amount' => $bookingRevenue['total']];
$rows[] = ['category' => 'إيرادات', 'description' => 'رسوم تسجيل', 'amount' => $registrationRevenue['total']];
$rows[] = ['category' => 'تكاليف', 'description' => 'أجور المدربين', 'amount' => $coachCosts['total']];
$rows[] = ['category' => 'صافي', 'description' => 'صافي الربح', 'amount' => $netProfit];
return [
'summary' => [
'total_revenue' => $totalRevenue,
'total_costs' => $totalCosts,
'net_profit' => $netProfit,
],
'revenue_breakdown' => [
['label' => 'اشتراكات اللاعبين', 'amount' => $subscriptionRevenue['total'], 'count' => $subscriptionRevenue['count']],
['label' => 'حجوزات المرافق', 'amount' => $bookingRevenue['total'], 'count' => $bookingRevenue['count']],
['label' => 'رسوم تسجيل', 'amount' => $registrationRevenue['total'], 'count' => $registrationRevenue['count']],
],
'cost_breakdown' => [
['label' => 'أجور المدربين', 'amount' => $coachCosts['total'], 'detail' => $coachCosts['detail']],
],
'monthly_trend' => $monthlyTrend,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'columns' => $columns,
'rows' => $rows,
];
}
private static function resolveDateRange(array $filters): array
{
$preset = $filters['period_preset'] ?? 'monthly';
if ($preset === 'custom' && !empty($filters['date_from']) && !empty($filters['date_to'])) {
return [$filters['date_from'], $filters['date_to']];
}
$today = date('Y-m-d');
$year = (int) date('Y');
$month = (int) date('m');
return match ($preset) {
'daily' => [$today, $today],
'weekly' => [date('Y-m-d', strtotime('monday this week')), date('Y-m-d', strtotime('sunday this week'))],
'monthly' => [date('Y-m-01'), date('Y-m-t')],
'yearly' => $month >= 7
? [$year . '-07-01', ($year + 1) . '-06-30']
: [($year - 1) . '-07-01', $year . '-06-30'],
'3yr' => [date('Y-m-d', strtotime('-3 years')), $today],
'5yr' => [date('Y-m-d', strtotime('-5 years')), $today],
default => [date('Y-m-01'), date('Y-m-t')],
};
}
private static function getSubscriptionRevenue(\App\Core\Database $db, string $from, string $to, ?int $disciplineId, ?int $branchId): array
{
$where = "sub.payment_status = 'paid' AND sub.period_start <= ? AND sub.period_end >= ?";
$params = [$to, $from];
$joins = '';
if ($disciplineId !== null) {
$joins .= " INNER JOIN sa_groups g ON g.id = sub.group_id INNER JOIN sa_programs p ON p.id = g.program_id";
$where .= " AND p.discipline_id = ?";
$params[] = $disciplineId;
}
if ($branchId !== null) {
$joins .= " INNER JOIN sa_players sp ON sp.id = sub.player_id";
$where .= " AND sp.branch_id = ?";
$params[] = $branchId;
}
$row = $db->selectOne("SELECT COALESCE(SUM(sub.final_amount), 0) AS total, COUNT(*) AS cnt FROM sa_subscriptions sub {$joins} WHERE {$where}", $params);
return ['total' => $row['total'] ?? '0.00', 'count' => (int) ($row['cnt'] ?? 0)];
}
private static function getBookingRevenue(\App\Core\Database $db, string $from, string $to, ?int $disciplineId, ?int $branchId): array
{
$where = "b.payment_status = 'paid' AND b.booking_date BETWEEN ? AND ? AND b.status NOT IN ('cancelled')";
$params = [$from, $to];
$joins = '';
if ($disciplineId !== null) {
$joins .= " INNER JOIN sa_groups g ON g.id = b.group_id INNER JOIN sa_programs p ON p.id = g.program_id";
$where .= " AND p.discipline_id = ?";
$params[] = $disciplineId;
}
if ($branchId !== null) {
$where .= " AND b.branch_id = ?";
$params[] = $branchId;
}
$row = $db->selectOne("SELECT COALESCE(SUM(b.total_amount), 0) AS total, COUNT(*) AS cnt FROM sa_bookings b {$joins} WHERE {$where}", $params);
return ['total' => $row['total'] ?? '0.00', 'count' => (int) ($row['cnt'] ?? 0)];
}
private static function getRegistrationRevenue(\App\Core\Database $db, string $from, string $to, ?int $branchId): array
{
$where = "sp.registration_fee_paid = 1 AND sp.is_archived = 0 AND sp.created_at BETWEEN ? AND ?";
$params = [$from . ' 00:00:00', $to . ' 23:59:59'];
if ($branchId !== null) {
$where .= " AND sp.branch_id = ?";
$params[] = $branchId;
}
$row = $db->selectOne("SELECT COUNT(*) AS cnt FROM sa_players sp WHERE {$where}", $params);
$count = (int) ($row['cnt'] ?? 0);
$feeRow = $db->selectOne("SELECT base_price FROM sa_pricing_rules WHERE activity_type = 'registration' AND is_active = 1 ORDER BY id DESC LIMIT 1");
$feePerPlayer = $feeRow['base_price'] ?? '0.00';
$total = bcmul((string) $count, (string) $feePerPlayer, 2);
return ['total' => $total, 'count' => $count];
}
private static function getCoachCosts(\App\Core\Database $db, string $from, string $to, ?int $disciplineId, ?int $branchId): array
{
$where = "c.is_active = 1 AND c.is_archived = 0";
$params = [];
$joins = '';
if ($disciplineId !== null) {
$joins .= " INNER JOIN sa_coach_disciplines cd ON cd.coach_id = c.id";
$where .= " AND cd.discipline_id = ?";
$params[] = $disciplineId;
}
if ($branchId !== null) {
$where .= " AND c.branch_id = ?";
$params[] = $branchId;
}
$coaches = $db->select("SELECT c.id, c.full_name_ar, c.payment_model, c.monthly_rate, c.session_rate, c.hourly_rate FROM sa_coaches c {$joins} WHERE {$where}", $params);
$totalCost = '0.00';
$detail = [];
$monthsInRange = max(1, self::monthsBetween($from, $to));
foreach ($coaches as $coach) {
$coachCost = '0.00';
switch ($coach['payment_model']) {
case 'monthly':
$coachCost = bcmul((string) ($coach['monthly_rate'] ?? '0'), (string) $monthsInRange, 2);
break;
case 'per_session':
$sessionCount = $db->selectOne(
"SELECT COUNT(DISTINCT a.booking_id, a.attendance_date) AS cnt
FROM sa_attendance a
INNER JOIN sa_bookings bk ON bk.id = a.booking_id
WHERE bk.coach_id = ? AND a.attendance_date BETWEEN ? AND ?",
[(int) $coach['id'], $from, $to]
);
$cnt = (int) ($sessionCount['cnt'] ?? 0);
$coachCost = bcmul((string) ($coach['session_rate'] ?? '0'), (string) $cnt, 2);
break;
case 'hourly':
$sessionCount = $db->selectOne(
"SELECT COUNT(DISTINCT a.booking_id, a.attendance_date) AS cnt
FROM sa_attendance a
INNER JOIN sa_bookings bk ON bk.id = a.booking_id
WHERE bk.coach_id = ? AND a.attendance_date BETWEEN ? AND ?",
[(int) $coach['id'], $from, $to]
);
$cnt = (int) ($sessionCount['cnt'] ?? 0);
$coachCost = bcmul((string) ($coach['hourly_rate'] ?? '0'), (string) $cnt, 2);
break;
}
if (bccomp($coachCost, '0', 2) > 0) {
$detail[] = ['name' => $coach['full_name_ar'], 'model' => $coach['payment_model'], 'amount' => $coachCost];
}
$totalCost = bcadd($totalCost, $coachCost, 2);
}
return ['total' => $totalCost, 'detail' => $detail];
}
private static function getMonthlyTrend(\App\Core\Database $db, string $from, string $to, ?int $disciplineId, ?int $branchId): array
{
$subJoins = '';
$subWhere = "sub.payment_status = 'paid'";
$subParams = [$from, $to];
if ($disciplineId !== null) {
$subJoins .= " INNER JOIN sa_groups g ON g.id = sub.group_id INNER JOIN sa_programs p ON p.id = g.program_id";
$subWhere .= " AND p.discipline_id = ?";
$subParams[] = $disciplineId;
}
if ($branchId !== null) {
$subJoins .= " INNER JOIN sa_players sp ON sp.id = sub.player_id";
$subWhere .= " AND sp.branch_id = ?";
$subParams[] = $branchId;
}
$subRows = $db->select(
"SELECT DATE_FORMAT(sub.period_start, '%Y-%m') AS month_key, COALESCE(SUM(sub.final_amount), 0) AS revenue
FROM sa_subscriptions sub {$subJoins}
WHERE {$subWhere} AND sub.period_start BETWEEN ? AND ?
GROUP BY month_key ORDER BY month_key",
$subParams
);
$bkWhere = "b.payment_status = 'paid' AND b.status != 'cancelled'";
$bkParams = [$from, $to];
$bkJoins = '';
if ($disciplineId !== null) {
$bkJoins .= " INNER JOIN sa_groups g2 ON g2.id = b.group_id INNER JOIN sa_programs p2 ON p2.id = g2.program_id";
$bkWhere .= " AND p2.discipline_id = ?";
$bkParams[] = $disciplineId;
}
if ($branchId !== null) {
$bkWhere .= " AND b.branch_id = ?";
$bkParams[] = $branchId;
}
$bkRows = $db->select(
"SELECT DATE_FORMAT(b.booking_date, '%Y-%m') AS month_key, COALESCE(SUM(b.total_amount), 0) AS revenue
FROM sa_bookings b {$bkJoins}
WHERE {$bkWhere} AND b.booking_date BETWEEN ? AND ?
GROUP BY month_key ORDER BY month_key",
$bkParams
);
$monthMap = [];
foreach ($subRows as $r) {
$monthMap[$r['month_key']] = bcadd($monthMap[$r['month_key']] ?? '0.00', $r['revenue'], 2);
}
foreach ($bkRows as $r) {
$monthMap[$r['month_key']] = bcadd($monthMap[$r['month_key']] ?? '0.00', $r['revenue'], 2);
}
ksort($monthMap);
$trend = [];
foreach ($monthMap as $month => $revenue) {
$trend[] = ['month' => $month, 'revenue' => $revenue];
}
return $trend;
}
private static function monthsBetween(string $from, string $to): int
{
$d1 = new \DateTime($from);
$d2 = new \DateTime($to);
$diff = $d1->diff($d2);
return max(1, $diff->y * 12 + $diff->m + ($diff->d > 0 ? 1 : 0));
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
final class SaPlayerReportService
{
public static function generate(array $filters): array
{
$db = App::getInstance()->db();
$columns = [
'player_name' => 'اسم اللاعب',
'national_id' => 'الرقم القومي',
'phone' => 'الهاتف',
'player_type' => 'النوع',
'discipline_name' => 'اللعبة',
'program_name' => 'البرنامج',
'group_name' => 'المجموعة',
'coach_name' => 'المدرب',
'enrollment_status'=> 'حالة التسجيل',
'medical_status' => 'الحالة الطبية',
'medical_expiry' => 'انتهاء الشهادة الطبية',
'card_status' => 'حالة الكارت',
'reg_fee_paid' => 'رسوم التسجيل',
'sub_status' => 'حالة الاشتراك',
'branch_name' => 'الفرع',
];
$where = ['sp.is_archived = 0'];
$params = [];
if (!empty($filters['discipline_id'])) {
$where[] = 'd.id = ?';
$params[] = (int) $filters['discipline_id'];
}
if (!empty($filters['program_id'])) {
$where[] = 'p.id = ?';
$params[] = (int) $filters['program_id'];
}
if (!empty($filters['group_id'])) {
$where[] = 'g.id = ?';
$params[] = (int) $filters['group_id'];
}
if (!empty($filters['player_id'])) {
$where[] = 'sp.id = ?';
$params[] = (int) $filters['player_id'];
}
if (!empty($filters['player_type'])) {
$where[] = 'sp.player_type = ?';
$params[] = $filters['player_type'];
}
if (!empty($filters['medical_status'])) {
$where[] = 'sp.medical_status = ?';
$params[] = $filters['medical_status'];
}
if (!empty($filters['payment_status'])) {
$where[] = 'latest_sub.payment_status = ?';
$params[] = $filters['payment_status'];
}
if (!empty($filters['branch_id'])) {
$where[] = 'sp.branch_id = ?';
$params[] = (int) $filters['branch_id'];
}
$whereClause = implode(' AND ', $where);
$sql = "
SELECT
sp.id AS player_id,
sp.full_name_ar AS player_name,
sp.national_id,
COALESCE(sp.phone, sp.guardian_phone) AS phone,
sp.player_type,
COALESCE(d.name_ar, '—') AS discipline_name,
COALESCE(p.name_ar, '—') AS program_name,
COALESCE(g.name_ar, '—') AS group_name,
COALESCE(c.full_name_ar, '—') AS coach_name,
COALESCE(gp.status, '—') AS enrollment_status,
sp.medical_status,
sp.medical_expiry_date AS medical_expiry,
sp.card_status,
sp.registration_fee_paid AS reg_fee_paid,
COALESCE(latest_sub.payment_status, '—') AS sub_status,
COALESCE(br.name_ar, '—') AS branch_name
FROM sa_players sp
LEFT JOIN sa_group_players gp ON gp.player_id = sp.id
AND gp.status NOT IN ('removed','transferred')
LEFT JOIN sa_groups g ON g.id = gp.group_id AND g.is_archived = 0
LEFT JOIN sa_programs p ON p.id = g.program_id AND p.is_archived = 0
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id AND d.is_archived = 0
LEFT JOIN sa_coaches c ON c.id = g.coach_id
LEFT JOIN branches br ON br.id = sp.branch_id
LEFT JOIN (
SELECT s1.player_id, s1.group_id, s1.payment_status
FROM sa_subscriptions s1
INNER JOIN (
SELECT player_id, group_id, MAX(period_end) AS max_end
FROM sa_subscriptions
GROUP BY player_id, group_id
) s2 ON s1.player_id = s2.player_id
AND s1.group_id = s2.group_id
AND s1.period_end = s2.max_end
) latest_sub ON latest_sub.player_id = sp.id
AND latest_sub.group_id = COALESCE(g.id, 0)
WHERE {$whereClause}
ORDER BY sp.full_name_ar
";
$rows = $db->select($sql, $params);
$statusLabels = [
'pending_payment' => 'بانتظار الدفع',
'active' => 'نشط',
'paused' => 'متوقف',
'removed' => 'محذوف',
'transferred' => 'منقول',
'pending' => 'معلق',
'fit' => 'لائق',
'conditional' => 'مشروط',
'unfit' => 'غير لائق',
'expired' => 'منتهي',
'inactive' => 'غير نشط',
'active' => 'نشط',
'suspended' => 'موقوف',
'revoked' => 'ملغي',
'unpaid' => 'غير مدفوع',
'paid' => 'مدفوع',
'partial' => 'جزئي',
'exempt' => 'معفي',
'overdue' => 'متأخر',
'member' => 'عضو',
'non_member' => 'غير عضو',
];
$formatted = [];
foreach ($rows as $row) {
$formatted[] = [
'player_name' => $row['player_name'],
'national_id' => $row['national_id'] ?? '—',
'phone' => $row['phone'] ?? '—',
'player_type' => $statusLabels[$row['player_type']] ?? $row['player_type'],
'discipline_name' => $row['discipline_name'],
'program_name' => $row['program_name'],
'group_name' => $row['group_name'],
'coach_name' => $row['coach_name'],
'enrollment_status' => $statusLabels[$row['enrollment_status']] ?? $row['enrollment_status'],
'medical_status' => $statusLabels[$row['medical_status']] ?? $row['medical_status'],
'medical_expiry' => $row['medical_expiry'] ?? '—',
'card_status' => $statusLabels[$row['card_status']] ?? $row['card_status'],
'reg_fee_paid' => $row['reg_fee_paid'] ? 'نعم' : 'لا',
'sub_status' => $statusLabels[$row['sub_status']] ?? $row['sub_status'],
'branch_name' => $row['branch_name'],
];
}
return [
'columns' => $columns,
'rows' => $formatted,
'total' => count($formatted),
];
}
}
<?php
declare(strict_types=1);
$__template->layout('Layout.main');
$__template->section('title', 'التقارير المالية — الأنشطة الرياضية');
$__template->section('content');
$summary = $report['summary'] ?? [];
$revenueBreakdown = $report['revenue_breakdown'] ?? [];
$costBreakdown = $report['cost_breakdown'] ?? [];
$trend = $report['monthly_trend'] ?? [];
$dateFrom = $report['date_from'] ?? '';
$dateTo = $report['date_to'] ?? '';
$presets = [
'daily' => 'يومي',
'weekly' => 'أسبوعي',
'monthly' => 'شهري',
'yearly' => 'سنوي (سنة مالية)',
'3yr' => '3 سنوات',
'5yr' => '5 سنوات',
'custom' => 'فترة مخصصة',
];
$currentPreset = $filters['period_preset'] ?? 'monthly';
?>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<h1 style="font-size:22px;font-weight:700;color:#0D7377;">التقارير المالية — الأنشطة الرياضية</h1>
<a href="/sa" class="btn btn-outline btn-sm">العودة للوحة التحكم</a>
</div>
<!-- Filter Card -->
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #7C3AED;">
<form method="GET" action="/sa/reports/finance" id="financeFilterForm">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;margin-bottom:12px;">
<div class="form-group">
<label class="form-label">اللعبة</label>
<select name="discipline_id" class="form-select">
<option value="">كل الأنشطة</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">كل الفروع</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= ($filters['branch_id'] ?? '') == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" id="customDateFrom" style="display:<?= $currentPreset === 'custom' ? 'block' : 'none' ?>;">
<label class="form-label">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from'] ?? '') ?>">
</div>
<div class="form-group" id="customDateTo" style="display:<?= $currentPreset === 'custom' ? 'block' : 'none' ?>;">
<label class="form-label">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to'] ?? '') ?>">
</div>
</div>
<!-- Period Presets -->
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px;">
<span style="font-size:12px;color:#6B7280;font-weight:600;">الفترة:</span>
<?php foreach ($presets as $key => $label): ?>
<label style="display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;border:1px solid <?= $currentPreset === $key ? '#7C3AED' : '#E5E7EB' ?>;background:<?= $currentPreset === $key ? '#F5F3FF' : '#fff' ?>;color:<?= $currentPreset === $key ? '#7C3AED' : '#374151' ?>;">
<input type="radio" name="period_preset" value="<?= $key ?>" <?= $currentPreset === $key ? 'checked' : '' ?> onchange="toggleCustomDates(this.value);document.getElementById('financeFilterForm').submit();" style="display:none;">
<?= e($label) ?>
</label>
<?php endforeach; ?>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<button type="submit" class="btn btn-primary btn-sm">
<i data-lucide="bar-chart-3" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-left:4px;"></i> عرض التقرير
</button>
<span style="font-size:11px;color:#9CA3AF;">الفترة: <?= e($dateFrom) ?><?= e($dateTo) ?></span>
</div>
</form>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #059669;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">إجمالي الإيرادات</div>
<div style="font-size:28px;font-weight:700;color:#059669;direction:ltr;"><?= number_format((float) ($summary['total_revenue'] ?? 0), 2) ?></div>
<div style="font-size:10px;color:#9CA3AF;">ج.م</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:3px solid #DC2626;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">إجمالي التكاليف</div>
<div style="font-size:28px;font-weight:700;color:#DC2626;direction:ltr;"><?= number_format((float) ($summary['total_costs'] ?? 0), 2) ?></div>
<div style="font-size:10px;color:#9CA3AF;">ج.م</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:3px solid <?= bccomp($summary['net_profit'] ?? '0', '0', 2) >= 0 ? '#2563EB' : '#DC2626' ?>;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">صافي الربح</div>
<div style="font-size:28px;font-weight:700;color:<?= bccomp($summary['net_profit'] ?? '0', '0', 2) >= 0 ? '#2563EB' : '#DC2626' ?>;direction:ltr;"><?= number_format((float) ($summary['net_profit'] ?? 0), 2) ?></div>
<div style="font-size:10px;color:#9CA3AF;">ج.م</div>
</div>
</div>
<!-- Export Buttons -->
<div style="display:flex;gap:8px;margin-bottom:20px;">
<?php $exportParams = http_build_query(array_filter($filters, fn($v) => $v !== '' && $v !== null)); ?>
<a href="/sa/reports/finance/export-csv?<?= $exportParams ?>" class="btn btn-sm btn-outline" style="color:#059669;border-color:#059669;">
<i data-lucide="file-spreadsheet" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-left:4px;"></i> تصدير CSV
</a>
<a href="/sa/reports/finance/export-pdf?<?= $exportParams ?>" class="btn btn-sm btn-outline" style="color:#DC2626;border-color:#DC2626;">
<i data-lucide="file-text" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-left:4px;"></i> تصدير PDF
</a>
</div>
<!-- Revenue Breakdown -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<h2 style="font-size:15px;font-weight:600;color:#059669;margin:0 0 12px;">تفصيل الإيرادات</h2>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="border-bottom:2px solid #E5E7EB;">
<th style="padding:8px 10px;text-align:right;color:#6B7280;">البند</th>
<th style="padding:8px 10px;text-align:left;color:#6B7280;width:160px;">المبلغ (ج.م)</th>
<th style="padding:8px 10px;text-align:center;color:#6B7280;width:100px;">العدد</th>
</tr>
</thead>
<tbody>
<?php foreach ($revenueBreakdown as $item): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 10px;"><?= e($item['label']) ?></td>
<td style="padding:8px 10px;text-align:left;font-weight:600;color:#059669;direction:ltr;"><?= number_format((float) $item['amount'], 2) ?></td>
<td style="padding:8px 10px;text-align:center;color:#6B7280;"><?= (int) ($item['count'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Cost Breakdown -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<h2 style="font-size:15px;font-weight:600;color:#DC2626;margin:0 0 12px;">التكاليف</h2>
<?php if (empty($costBreakdown) || bccomp($summary['total_costs'] ?? '0', '0', 2) === 0): ?>
<p style="color:#9CA3AF;font-size:13px;">لا توجد تكاليف مسجلة في هذه الفترة.</p>
<?php else: ?>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="border-bottom:2px solid #E5E7EB;">
<th style="padding:8px 10px;text-align:right;color:#6B7280;">البند</th>
<th style="padding:8px 10px;text-align:left;color:#6B7280;width:160px;">المبلغ (ج.م)</th>
</tr>
</thead>
<tbody>
<?php foreach ($costBreakdown as $item): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 10px;"><?= e($item['label']) ?></td>
<td style="padding:8px 10px;text-align:left;font-weight:600;color:#DC2626;direction:ltr;"><?= number_format((float) $item['amount'], 2) ?></td>
</tr>
<?php if (!empty($item['detail'])): ?>
<tr>
<td colspan="2" style="padding:4px 10px 12px;">
<div style="font-size:11px;color:#6B7280;display:flex;flex-wrap:wrap;gap:6px;">
<?php foreach ($item['detail'] as $d): ?>
<span style="background:#FEF2F2;padding:2px 8px;border-radius:4px;">
<?= e($d['name']) ?> (<?= e($d['model']) ?>): <?= number_format((float) $d['amount'], 2) ?> ج.م
</span>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Monthly Trend -->
<?php if (!empty($trend)): ?>
<div class="card" style="padding:20px;">
<h2 style="font-size:15px;font-weight:600;color:#374151;margin:0 0 12px;">الاتجاه الشهري</h2>
<?php
$maxRevenue = max(array_map(fn($t) => (float) $t['revenue'], $trend));
$maxRevenue = $maxRevenue > 0 ? $maxRevenue : 1;
?>
<div style="display:flex;align-items:end;gap:8px;height:200px;padding:10px 0;">
<?php foreach ($trend as $t): ?>
<?php $pct = ((float) $t['revenue'] / $maxRevenue) * 100; ?>
<div style="flex:1;text-align:center;display:flex;flex-direction:column;justify-content:end;height:100%;">
<div style="font-size:10px;color:#374151;font-weight:600;margin-bottom:4px;direction:ltr;"><?= number_format((float) $t['revenue'], 0) ?></div>
<div style="background:linear-gradient(to top,#0D7377,#10B981);border-radius:4px 4px 0 0;min-height:4px;height:<?= round($pct) ?>%;transition:height 0.3s;"></div>
<div style="font-size:9px;color:#9CA3AF;margin-top:4px;direction:ltr;"><?= e($t['month']) ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<script>
function toggleCustomDates(preset) {
document.getElementById('customDateFrom').style.display = preset === 'custom' ? 'block' : 'none';
document.getElementById('customDateTo').style.display = preset === 'custom' ? 'block' : 'none';
if (preset !== 'custom') {
document.querySelector('input[name="date_from"]').value = '';
document.querySelector('input[name="date_to"]').value = '';
}
}
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
$__template->layout('Layout.main');
$__template->section('title', 'تقارير اللاعبين');
$__template->section('content');
?>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<h1 style="font-size:22px;font-weight:700;color:#0D7377;">تقارير اللاعبين</h1>
<a href="/sa" class="btn btn-outline btn-sm">العودة للوحة التحكم</a>
</div>
<!-- Filter Card -->
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #0D7377;">
<form method="GET" action="/sa/reports/players">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<div class="form-group">
<label class="form-label">اللعبة</label>
<select name="discipline_id" id="filter_discipline" class="form-select" onchange="filterPrograms()">
<option value="">الكل</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">البرنامج</label>
<select name="program_id" id="filter_program" class="form-select" onchange="filterGroups()">
<option value="">الكل</option>
<?php foreach ($programs as $p): ?>
<option value="<?= (int) $p['id'] ?>" data-discipline="<?= (int) $p['discipline_id'] ?>" <?= ($filters['program_id'] ?? '') == $p['id'] ? 'selected' : '' ?>><?= e($p['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المجموعة</label>
<select name="group_id" id="filter_group" class="form-select">
<option value="">الكل</option>
<?php foreach ($groups as $g): ?>
<option value="<?= (int) $g['id'] ?>" data-program="<?= (int) $g['program_id'] ?>" <?= ($filters['group_id'] ?? '') == $g['id'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع اللاعب</label>
<select name="player_type" class="form-select">
<option value="">الكل</option>
<option value="member" <?= ($filters['player_type'] ?? '') === 'member' ? 'selected' : '' ?>>عضو</option>
<option value="non_member" <?= ($filters['player_type'] ?? '') === 'non_member' ? 'selected' : '' ?>>غير عضو</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الحالة الطبية</label>
<select name="medical_status" class="form-select">
<option value="">الكل</option>
<option value="pending" <?= ($filters['medical_status'] ?? '') === 'pending' ? 'selected' : '' ?>>معلق</option>
<option value="fit" <?= ($filters['medical_status'] ?? '') === 'fit' ? 'selected' : '' ?>>لائق</option>
<option value="conditional" <?= ($filters['medical_status'] ?? '') === 'conditional' ? 'selected' : '' ?>>مشروط</option>
<option value="unfit" <?= ($filters['medical_status'] ?? '') === 'unfit' ? 'selected' : '' ?>>غير لائق</option>
<option value="expired" <?= ($filters['medical_status'] ?? '') === 'expired' ? 'selected' : '' ?>>منتهي</option>
</select>
</div>
<div class="form-group">
<label class="form-label">حالة الاشتراك</label>
<select name="payment_status" class="form-select">
<option value="">الكل</option>
<option value="paid" <?= ($filters['payment_status'] ?? '') === 'paid' ? 'selected' : '' ?>>مدفوع</option>
<option value="unpaid" <?= ($filters['payment_status'] ?? '') === 'unpaid' ? 'selected' : '' ?>>غير مدفوع</option>
<option value="overdue" <?= ($filters['payment_status'] ?? '') === 'overdue' ? 'selected' : '' ?>>متأخر</option>
<option value="exempt" <?= ($filters['payment_status'] ?? '') === 'exempt' ? 'selected' : '' ?>>معفي</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= ($filters['branch_id'] ?? '') == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="display:flex;align-items:end;">
<button type="submit" class="btn btn-primary" style="width:100%;">
<i data-lucide="search" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-left:4px;"></i> عرض التقرير
</button>
</div>
</div>
</form>
</div>
<?php if ($report !== null): ?>
<!-- Results -->
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<h2 style="font-size:16px;font-weight:600;color:#374151;margin:0;">
النتائج: <?= (int) $report['total'] ?> لاعب
</h2>
<div style="display:flex;gap:8px;">
<a href="/sa/reports/players/export-csv?<?= http_build_query(array_filter($filters, fn($v) => $v !== '' && $v !== null)) ?>" class="btn btn-sm btn-outline" style="color:#059669;border-color:#059669;">
<i data-lucide="file-spreadsheet" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-left:4px;"></i> تصدير CSV
</a>
<a href="/sa/reports/players/export-pdf?<?= http_build_query(array_filter($filters, fn($v) => $v !== '' && $v !== null)) ?>" class="btn btn-sm btn-outline" style="color:#DC2626;border-color:#DC2626;">
<i data-lucide="file-text" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-left:4px;"></i> تصدير PDF
</a>
</div>
</div>
<?php if (empty($report['rows'])): ?>
<div style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد نتائج تطابق الفلاتر المحددة.</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#F3F4F6;">
<th style="padding:8px 10px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">#</th>
<?php foreach ($report['columns'] as $label): ?>
<th style="padding:8px 10px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;"><?= e($label) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($report['rows'] as $idx => $row): ?>
<tr style="border-bottom:1px solid #F3F4F6;<?= $idx % 2 ? 'background:#FAFAFA;' : '' ?>">
<td style="padding:6px 10px;color:#9CA3AF;"><?= $idx + 1 ?></td>
<?php foreach (array_keys($report['columns']) as $key): ?>
<td style="padding:6px 10px;white-space:nowrap;"><?= e((string) ($row[$key] ?? '—')) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="card" style="padding:40px;text-align:center;color:#9CA3AF;">
<i data-lucide="filter" style="width:40px;height:40px;display:block;margin:0 auto 12px;opacity:0.5;"></i>
<p style="font-size:14px;">اختر الفلاتر ثم اضغط "عرض التقرير" لعرض النتائج.</p>
</div>
<?php endif; ?>
<script>
function filterPrograms() {
var discId = document.getElementById('filter_discipline').value;
var progSelect = document.getElementById('filter_program');
var opts = progSelect.querySelectorAll('option[data-discipline]');
opts.forEach(function(opt) {
opt.style.display = (!discId || opt.getAttribute('data-discipline') === discId) ? '' : 'none';
});
if (progSelect.selectedOptions[0] && progSelect.selectedOptions[0].style.display === 'none') {
progSelect.value = '';
}
filterGroups();
}
function filterGroups() {
var progId = document.getElementById('filter_program').value;
var grpSelect = document.getElementById('filter_group');
var opts = grpSelect.querySelectorAll('option[data-program]');
opts.forEach(function(opt) {
opt.style.display = (!progId || opt.getAttribute('data-program') === progId) ? '' : 'none';
});
if (grpSelect.selectedOptions[0] && grpSelect.selectedOptions[0].style.display === 'none') {
grpSelect.value = '';
}
}
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
filterPrograms();
});
</script>
<?php $__template->endSection(); ?>
...@@ -53,6 +53,9 @@ MenuRegistry::register('sports_activity', [ ...@@ -53,6 +53,9 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'تسجيل لاعب سباحة', 'label_en' => 'Register Swimmer','route' => '/sa/swimming/register','permission' => 'sa.swimming.register','order' => 32], ['label_ar' => 'تسجيل لاعب سباحة', 'label_en' => 'Register Swimmer','route' => '/sa/swimming/register','permission' => 'sa.swimming.register','order' => 32],
['label_ar' => 'تعيين في مجموعة', 'label_en' => 'Assign to Group', 'route' => '/sa/swimming/assign','permission' => 'sa.swimming.assign', 'order' => 33], ['label_ar' => 'تعيين في مجموعة', 'label_en' => 'Assign to Group', 'route' => '/sa/swimming/assign','permission' => 'sa.swimming.assign', 'order' => 33],
['label_ar' => 'حجوزات السباحة', 'label_en' => 'Pool Reservations','route' => '/sa/swimming/pool-reservations','permission' => 'sa.pool_reservation.view','order' => 34], ['label_ar' => 'حجوزات السباحة', 'label_en' => 'Pool Reservations','route' => '/sa/swimming/pool-reservations','permission' => 'sa.pool_reservation.view','order' => 34],
['label_ar' => '── التقارير ──','label_en' => '── Reports ──', 'route' => '#', 'permission' => 'sa.report.players', 'order' => 40],
['label_ar' => 'تقارير اللاعبين','label_en' => 'Player Reports', 'route' => '/sa/reports/players', 'permission' => 'sa.report.players', 'order' => 41],
['label_ar' => 'التقارير المالية','label_en' => 'Finance Reports', 'route' => '/sa/reports/finance', 'permission' => 'sa.report.finance', 'order' => 42],
], ],
]); ]);
...@@ -129,6 +132,9 @@ PermissionRegistry::register('sports_activity', [ ...@@ -129,6 +132,9 @@ PermissionRegistry::register('sports_activity', [
'sa.pool_reservation.view' => ['ar' => 'عرض حجوزات السباحة', 'en' => 'View Pool Reservations'], 'sa.pool_reservation.view' => ['ar' => 'عرض حجوزات السباحة', 'en' => 'View Pool Reservations'],
'sa.pool_reservation.create' => ['ar' => 'إنشاء حجز سباحة', 'en' => 'Create Pool Reservation'], 'sa.pool_reservation.create' => ['ar' => 'إنشاء حجز سباحة', 'en' => 'Create Pool Reservation'],
'sa.pool_reservation.manage' => ['ar' => 'إدارة حجوزات السباحة', 'en' => 'Manage Pool Reservations'], 'sa.pool_reservation.manage' => ['ar' => 'إدارة حجوزات السباحة', 'en' => 'Manage Pool Reservations'],
'sa.report.players' => ['ar' => 'تقارير اللاعبين', 'en' => 'Player Reports'],
'sa.report.finance' => ['ar' => 'التقارير المالية للأنشطة', 'en' => 'Sports Finance Reports'],
'sa.report.export' => ['ar' => 'تصدير تقارير الأنشطة', 'en' => 'Export Sports Reports'],
]); ]);
// ─── Event Listeners ──────────────────────────────────────────────────────── // ─── Event Listeners ────────────────────────────────────────────────────────
......
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