Commit d11abbce authored by Mahmoud Aglan's avatar Mahmoud Aglan

Phase 52: Medical approval workflow, mirror grid, facility dashboard, pricing integration

- Medical records: upload with certificate type (ممارس/أكاديمي/دولي), auto-validity
  calculation, Board of Trustees approval workflow (pending → approved/rejected)
- New /medical-approvals page for مجلس الأمناء with approve/reject actions
- Mirror (المراية): interactive grid system for facility scheduling - create grids,
  assign coaches/academies to boxes, manage trainees (max 5 per box)
- Facility dashboard: per-facility stats with day/week/month/custom date filtering
- FacilityPricingService: reads club pricing rules from system_config, integrates
  with reservation creation as rate fallback
- Pricing seed: official 2025 club ruleset for non-educational activities stored
  in system_config (member/non-member rates, entry tickets, general fees)
- Updated sidebar menu with medical approvals link
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d0ef9ed6
...@@ -27,6 +27,7 @@ MenuRegistry::register('sports_activities', [ ...@@ -27,6 +27,7 @@ MenuRegistry::register('sports_activities', [
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/coaches', 'permission' => 'coach.view', 'order' => 4], ['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/coaches', 'permission' => 'coach.view', 'order' => 4],
['label_ar' => 'المجموعات التدريبية','label_en' => 'Training Groups', 'route' => '/training-groups', 'permission' => 'training_group.view', 'order' => 4.2], ['label_ar' => 'المجموعات التدريبية','label_en' => 'Training Groups', 'route' => '/training-groups', 'permission' => 'training_group.view', 'order' => 4.2],
['label_ar' => 'شئون اللاعبين', 'label_en' => 'Players', 'route' => '/players', 'permission' => 'player.view', 'order' => 4.5], ['label_ar' => 'شئون اللاعبين', 'label_en' => 'Players', 'route' => '/players', 'permission' => 'player.view', 'order' => 4.5],
['label_ar' => 'اعتماد السجلات الطبية','label_en' => 'Medical Approvals', 'route' => '/medical-approvals', 'permission' => 'player.approve_medical', 'order' => 4.6],
['label_ar' => 'الحصص التدريبية', 'label_en' => 'Training Sessions', 'route' => '/sessions', 'permission' => 'session.view', 'order' => 4.7], ['label_ar' => 'الحصص التدريبية', 'label_en' => 'Training Sessions', 'route' => '/sessions', 'permission' => 'session.view', 'order' => 4.7],
['label_ar' => 'الحضور والغياب', 'label_en' => 'Attendance', 'route' => '/attendance', 'permission' => 'player.view', 'order' => 5], ['label_ar' => 'الحضور والغياب', 'label_en' => 'Attendance', 'route' => '/attendance', 'permission' => 'player.view', 'order' => 5],
['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 6], ['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 6],
......
...@@ -12,54 +12,94 @@ class FacilityDashboardController extends Controller ...@@ -12,54 +12,94 @@ class FacilityDashboardController extends Controller
{ {
public function show(Request $request, string $id): Response public function show(Request $request, string $id): Response
{ {
$this->authorize('facility.dashboard');
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$facility = $db->selectOne("SELECT * FROM facilities WHERE id = ? AND is_archived = 0", [(int) $id]); $facility = $db->selectOne("SELECT * FROM facilities WHERE id = ? AND is_archived = 0", [(int) $id]);
if (!$facility) { if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود'); return $this->redirect('/facilities')->withError('المرفق غير موجود');
} }
$today = date('Y-m-d'); $today = date('Y-m-d');
$monthStart = date('Y-m-01');
$weekStart = date('Y-m-d', strtotime('monday this week')); // Date filter: day, week, month, or custom range
$filterMode = trim((string) $request->get('filter', 'day'));
$dateFrom = trim((string) $request->get('date_from', ''));
$dateTo = trim((string) $request->get('date_to', ''));
switch ($filterMode) {
case 'week':
$periodStart = date('Y-m-d', strtotime('monday this week'));
$periodEnd = date('Y-m-d', strtotime('sunday this week'));
$periodLabel = 'هذا الأسبوع';
break;
case 'month':
$periodStart = date('Y-m-01');
$periodEnd = date('Y-m-t');
$periodLabel = 'هذا الشهر';
break;
case 'custom':
$periodStart = $dateFrom ?: $today;
$periodEnd = $dateTo ?: $today;
$periodLabel = $periodStart . ' — ' . $periodEnd;
break;
default: // day
$periodStart = $today;
$periodEnd = $today;
$periodLabel = 'اليوم';
break;
}
$todayBookings = $db->select( $todayBookings = $db->select(
"SELECT * FROM reservations WHERE facility_id = ? AND reservation_date = ? AND status NOT IN ('cancelled') ORDER BY start_time ASC", "SELECT * FROM reservations WHERE facility_id = ? AND reservation_date = ? AND status NOT IN ('cancelled') ORDER BY start_time ASC",
[(int) $id, $today] [(int) $id, $today]
); );
$periodBookings = $db->select(
"SELECT * FROM reservations WHERE facility_id = ? AND reservation_date BETWEEN ? AND ? AND status NOT IN ('cancelled') ORDER BY reservation_date ASC, start_time ASC",
[(int) $id, $periodStart, $periodEnd]
);
$periodRevenue = $db->selectOne(
"SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date BETWEEN ? AND ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')",
[(int) $id, $periodStart, $periodEnd]
);
$todayRevenue = $db->selectOne( $todayRevenue = $db->selectOne(
"SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date = ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')", "SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date = ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')",
[(int) $id, $today] [(int) $id, $today]
); );
$weekStart = date('Y-m-d', strtotime('monday this week'));
$weekRevenue = $db->selectOne( $weekRevenue = $db->selectOne(
"SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')", "SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND reservation_date <= ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')",
[(int) $id, $weekStart] [(int) $id, $weekStart, date('Y-m-d', strtotime('sunday this week'))]
); );
$monthStart = date('Y-m-01');
$monthRevenue = $db->selectOne( $monthRevenue = $db->selectOne(
"SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')", "SELECT COALESCE(SUM(total_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND reservation_date <= ? AND payment_id IS NOT NULL AND status NOT IN ('cancelled')",
[(int) $id, $monthStart] [(int) $id, $monthStart, date('Y-m-t')]
);
$monthBookingCount = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND status NOT IN ('cancelled')",
[(int) $id, $monthStart]
); );
$upcomingBookings = $db->select( $upcomingBookings = $db->select(
"SELECT * FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND status = 'confirmed' ORDER BY reservation_date ASC, start_time ASC LIMIT 5", "SELECT * FROM reservations WHERE facility_id = ? AND reservation_date > ? AND status = 'confirmed' ORDER BY reservation_date ASC, start_time ASC LIMIT 10",
[(int) $id, $today] [(int) $id, $today]
); );
return $this->view('FacilityDashboards.Views.dashboard', [ return $this->view('FacilityDashboards.Views.dashboard', [
'facility' => $facility, 'facility' => $facility,
'todayBookings' => $todayBookings, 'todayBookings' => $todayBookings,
'todayRevenue' => (float) ($todayRevenue['total'] ?? 0), 'periodBookings' => $periodBookings,
'weekRevenue' => (float) ($weekRevenue['total'] ?? 0), 'todayRevenue' => (float) ($todayRevenue['total'] ?? 0),
'monthRevenue' => (float) ($monthRevenue['total'] ?? 0), 'weekRevenue' => (float) ($weekRevenue['total'] ?? 0),
'monthBookingCount' => (int) ($monthBookingCount['cnt'] ?? 0), 'monthRevenue' => (float) ($monthRevenue['total'] ?? 0),
'upcomingBookings' => $upcomingBookings, 'periodRevenue' => (float) ($periodRevenue['total'] ?? 0),
'periodLabel' => $periodLabel,
'filterMode' => $filterMode,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'upcomingBookings' => $upcomingBookings,
]); ]);
} }
} }
...@@ -13,45 +13,166 @@ class MirrorDisplayController extends Controller ...@@ -13,45 +13,166 @@ class MirrorDisplayController extends Controller
{ {
public function index(Request $request): Response public function index(Request $request): Response
{ {
$facilityType = trim((string) $request->get('type', '')); $this->authorize('facility.mirror');
$disciplineId = $request->get('discipline_id', '') !== '' ? (int) $request->get('discipline_id') : null;
$states = MirrorDisplayService::getFacilityStates( $grids = MirrorDisplayService::getAllGrids();
$facilityType ?: null,
$disciplineId $db = App::getInstance()->db();
$facilities = $db->select(
"SELECT id, name_ar FROM facilities WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar"
); );
return $this->view('FacilityDashboards.Views.mirror_index', [
'grids' => $grids,
'facilities' => $facilities,
]);
}
public function createGrid(Request $request): Response
{
$this->authorize('facility.mirror');
$facilityId = (int) $request->post('facility_id', 0);
$nameAr = trim((string) $request->post('name_ar', ''));
$rows = max(1, min(10, (int) $request->post('rows_count', 4)));
$cols = max(1, min(12, (int) $request->post('cols_count', 6)));
if ($facilityId <= 0 || $nameAr === '') {
return $this->redirect('/mirror')->withError('يجب اختيار المرفق وإدخال الاسم');
}
$gridId = MirrorDisplayService::createGrid($facilityId, $nameAr, $rows, $cols);
return $this->redirect('/mirror/' . $gridId)->withSuccess('تم إنشاء المراية بنجاح');
}
public function show(Request $request, string $id): Response
{
$this->authorize('facility.mirror');
$grid = MirrorDisplayService::getGrid((int) $id);
if (!$grid) {
return $this->redirect('/mirror')->withError('المراية غير موجودة');
}
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$facilityTypes = $db->select( $coaches = $db->select(
"SELECT DISTINCT facility_type FROM facilities WHERE is_active = 1 AND is_archived = 0 ORDER BY facility_type" "SELECT id, full_name_ar FROM coaches WHERE is_active = 1 AND is_archived = 0 ORDER BY full_name_ar"
);
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 ORDER BY name_ar"
); );
$disciplines = $db->select( $players = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar" "SELECT id, full_name_ar FROM players WHERE is_archived = 0 AND card_status = 'active' ORDER BY full_name_ar"
); );
return $this->view('FacilityDashboards.Views.mirror', [ return $this->view('FacilityDashboards.Views.mirror', [
'states' => $states, 'grid' => $grid,
'facilityTypes' => $facilityTypes, 'coaches' => $coaches,
'disciplines' => $disciplines, 'academies' => $academies,
'currentType' => $facilityType, 'players' => $players,
'currentDisc' => $disciplineId,
]); ]);
} }
public function apiState(Request $request): Response public function updateBox(Request $request, string $gridId, string $boxId): Response
{ {
$facilityType = trim((string) $request->get('type', '')); $this->authorize('facility.mirror');
$disciplineId = $request->get('discipline_id', '') !== '' ? (int) $request->get('discipline_id') : null;
$states = MirrorDisplayService::getFacilityStates( $data = [];
$facilityType ?: null, if ($request->post('coach_id') !== null) {
$disciplineId $coachId = (int) $request->post('coach_id', 0);
$data['coach_id'] = $coachId > 0 ? $coachId : null;
}
if ($request->post('coach_name') !== null) {
$data['coach_name'] = trim((string) $request->post('coach_name', ''));
}
if ($request->post('academy_id') !== null) {
$academyId = (int) $request->post('academy_id', 0);
$data['academy_id'] = $academyId > 0 ? $academyId : null;
}
if ($request->post('age_group') !== null) {
$data['age_group'] = trim((string) $request->post('age_group', ''));
}
if ($request->post('gender') !== null) {
$data['gender'] = trim((string) $request->post('gender', ''));
}
if ($request->post('max_trainees') !== null) {
$data['max_trainees'] = max(1, min(10, (int) $request->post('max_trainees', 5)));
}
if ($request->post('label') !== null) {
$data['label'] = trim((string) $request->post('label', ''));
}
if (!empty($data)) {
MirrorDisplayService::updateBox((int) $boxId, $data);
}
return $this->redirect('/mirror/' . $gridId)->withSuccess('تم تحديث الخانة');
}
public function addTrainee(Request $request, string $gridId, string $boxId): Response
{
$this->authorize('facility.mirror');
$playerId = (int) $request->post('player_id', 0);
$traineeName = trim((string) $request->post('trainee_name', ''));
if ($playerId <= 0 && $traineeName === '') {
return $this->redirect('/mirror/' . $gridId)->withError('يجب اختيار لاعب أو كتابة اسم');
}
$success = MirrorDisplayService::addTrainee(
(int) $boxId,
$playerId > 0 ? $playerId : null,
$traineeName ?: null
); );
if (!$success) {
return $this->redirect('/mirror/' . $gridId)->withError('الخانة ممتلئة — الحد الأقصى 5');
}
return $this->redirect('/mirror/' . $gridId)->withSuccess('تم إضافة المتدرب');
}
public function removeTrainee(Request $request, string $gridId, string $traineeId): Response
{
$this->authorize('facility.mirror');
MirrorDisplayService::removeTrainee((int) $traineeId);
return $this->redirect('/mirror/' . $gridId)->withSuccess('تم إزالة المتدرب');
}
public function moveTrainee(Request $request, string $gridId, string $traineeId): Response
{
$this->authorize('facility.mirror');
$toBoxId = (int) $request->post('to_box_id', 0);
if ($toBoxId <= 0) {
return $this->redirect('/mirror/' . $gridId)->withError('يجب اختيار الخانة المستهدفة');
}
$success = MirrorDisplayService::moveTrainee((int) $traineeId, $toBoxId);
if (!$success) {
return $this->redirect('/mirror/' . $gridId)->withError('الخانة المستهدفة ممتلئة');
}
return $this->redirect('/mirror/' . $gridId)->withSuccess('تم نقل المتدرب');
}
public function apiState(Request $request, string $id): Response
{
$this->authorize('facility.mirror');
$state = MirrorDisplayService::getGridState((int) $id);
return $this->json([ return $this->json([
'states' => $states, 'boxes' => $state,
'timestamp' => date('Y-m-d H:i:s'), 'timestamp' => date('Y-m-d H:i:s'),
'time' => date('H:i'),
]); ]);
} }
public function deleteGrid(Request $request, string $id): Response
{
$this->authorize('facility.mirror');
$db = App::getInstance()->db();
$db->update('mirror_grids', ['is_active' => 0], 'id = ?', [(int) $id]);
return $this->redirect('/mirror')->withSuccess('تم حذف المراية');
}
} }
...@@ -2,7 +2,17 @@ ...@@ -2,7 +2,17 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
['GET', '/mirror', 'FacilityDashboards\Controllers\MirrorDisplayController@index', ['auth'], 'facility.mirror'], // Mirror Grid System
['GET', '/api/mirror/state', 'FacilityDashboards\Controllers\MirrorDisplayController@apiState', ['auth'], 'facility.mirror'], ['GET', '/mirror', 'FacilityDashboards\Controllers\MirrorDisplayController@index', ['auth'], 'facility.mirror'],
['GET', '/facilities/{id:\d+}/dashboard', 'FacilityDashboards\Controllers\FacilityDashboardController@show', ['auth'], 'facility.dashboard'], ['POST', '/mirror/create', 'FacilityDashboards\Controllers\MirrorDisplayController@createGrid', ['auth', 'csrf'], 'facility.mirror'],
['GET', '/mirror/{id:\d+}', 'FacilityDashboards\Controllers\MirrorDisplayController@show', ['auth'], 'facility.mirror'],
['POST', '/mirror/{gridId:\d+}/box/{boxId:\d+}', 'FacilityDashboards\Controllers\MirrorDisplayController@updateBox', ['auth', 'csrf'], 'facility.mirror'],
['POST', '/mirror/{gridId:\d+}/box/{boxId:\d+}/trainee', 'FacilityDashboards\Controllers\MirrorDisplayController@addTrainee', ['auth', 'csrf'], 'facility.mirror'],
['POST', '/mirror/{gridId:\d+}/trainee/{traineeId:\d+}/remove', 'FacilityDashboards\Controllers\MirrorDisplayController@removeTrainee', ['auth', 'csrf'], 'facility.mirror'],
['POST', '/mirror/{gridId:\d+}/trainee/{traineeId:\d+}/move', 'FacilityDashboards\Controllers\MirrorDisplayController@moveTrainee', ['auth', 'csrf'], 'facility.mirror'],
['POST', '/mirror/{id:\d+}/delete', 'FacilityDashboards\Controllers\MirrorDisplayController@deleteGrid', ['auth', 'csrf'], 'facility.mirror'],
['GET', '/api/mirror/{id:\d+}/state', 'FacilityDashboards\Controllers\MirrorDisplayController@apiState', ['auth'], 'facility.mirror'],
// Facility Dashboard
['GET', '/facilities/{id:\d+}/dashboard', 'FacilityDashboards\Controllers\FacilityDashboardController@show', ['auth'], 'facility.dashboard'],
]; ];
...@@ -8,6 +8,24 @@ ...@@ -8,6 +8,24 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<!-- Date Filter -->
<div class="card" style="margin-bottom:15px;padding:12px 20px;">
<form method="GET" action="/facilities/<?= (int) $facility['id'] ?>/dashboard" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<span style="font-size:13px;color:#6B7280;">فترة العرض:</span>
<a href="?filter=day" class="btn <?= $filterMode === 'day' ? 'btn-primary' : 'btn-outline' ?>" style="font-size:12px;padding:5px 12px;">اليوم</a>
<a href="?filter=week" class="btn <?= $filterMode === 'week' ? 'btn-primary' : 'btn-outline' ?>" style="font-size:12px;padding:5px 12px;">الأسبوع</a>
<a href="?filter=month" class="btn <?= $filterMode === 'month' ? 'btn-primary' : 'btn-outline' ?>" style="font-size:12px;padding:5px 12px;">الشهر</a>
<span style="color:#D1D5DB;">|</span>
<input type="hidden" name="filter" value="custom">
<input type="date" name="date_from" value="<?= e($dateFrom) ?>" class="form-input" style="width:140px;font-size:12px;padding:5px 8px;direction:ltr;">
<span style="font-size:12px;color:#9CA3AF;">إلى</span>
<input type="date" name="date_to" value="<?= e($dateTo) ?>" class="form-input" style="width:140px;font-size:12px;padding:5px 8px;direction:ltr;">
<button type="submit" class="btn btn-outline" style="font-size:12px;padding:5px 12px;">
<i data-lucide="filter" style="width:12px;height:12px;vertical-align:middle;margin-left:3px;"></i> تصفية
</button>
</form>
</div>
<!-- KPI Cards --> <!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;"> <div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #0D7377;"> <div class="card" style="padding:20px;text-align:center;border-top:4px solid #0D7377;">
...@@ -28,6 +46,17 @@ ...@@ -28,6 +46,17 @@
</div> </div>
</div> </div>
<?php if ($filterMode !== 'day'): ?>
<!-- Period Revenue -->
<div class="card" style="margin-bottom:20px;padding:15px 20px;display:flex;align-items:center;justify-content:space-between;background:#F0FDFA;border:1px solid #CCFBF1;">
<div>
<span style="font-size:13px;color:#6B7280;">إيرادات الفترة (<?= e($periodLabel) ?>):</span>
<span style="font-size:20px;font-weight:700;color:#059669;margin-right:8px;"><?= number_format($periodRevenue, 0) ?> ج.م</span>
</div>
<span style="font-size:13px;color:#6B7280;"><?= count($periodBookings) ?> حجز</span>
</div>
<?php endif; ?>
<!-- Today's Timeline --> <!-- Today's Timeline -->
<div class="card" style="margin-bottom:20px;"> <div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;"> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
...@@ -57,7 +86,7 @@ ...@@ -57,7 +86,7 @@
<?= ['confirmed'=>'مؤكد','checked_in'=>'جاري','completed'=>'مكتمل'][$b['status']] ?? $b['status'] ?> <?= ['confirmed'=>'مؤكد','checked_in'=>'جاري','completed'=>'مكتمل'][$b['status']] ?? $b['status'] ?>
</span> </span>
</div> </div>
<?php if ($b['total_amount'] > 0): ?> <?php if ((float)($b['total_amount'] ?? 0) > 0): ?>
<div style="font-size:12px;color:#059669;font-weight:600;"><?= number_format((float) $b['total_amount'], 0) ?> ج.م</div> <div style="font-size:12px;color:#059669;font-weight:600;"><?= number_format((float) $b['total_amount'], 0) ?> ج.م</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
...@@ -82,7 +111,7 @@ ...@@ -82,7 +111,7 @@
<tr style="color:#6B7280;font-size:12px;"> <tr style="color:#6B7280;font-size:12px;">
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">التاريخ</th> <th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">التاريخ</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الوقت</th> <th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الوقت</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الغرض</th> <th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">ملاحظات</th>
<th style="padding:8px;text-align:left;border-bottom:1px solid #E5E7EB;">المبلغ</th> <th style="padding:8px;text-align:left;border-bottom:1px solid #E5E7EB;">المبلغ</th>
</tr> </tr>
</thead> </thead>
...@@ -92,7 +121,7 @@ ...@@ -92,7 +121,7 @@
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($b['reservation_date']) ?></td> <td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($b['reservation_date']) ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;direction:ltr;text-align:right;"><?= e(substr($b['start_time'],0,5)) ?><?= e(substr($b['end_time'],0,5)) ?></td> <td style="padding:8px;border-bottom:1px solid #F3F4F6;direction:ltr;text-align:right;"><?= e(substr($b['start_time'],0,5)) ?><?= e(substr($b['end_time'],0,5)) ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($b['notes'] ?? '—') ?></td> <td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($b['notes'] ?? '—') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;text-align:left;"><?= $b['total_amount'] > 0 ? number_format((float)$b['total_amount'], 0) . ' ج.م' : '—' ?></td> <td style="padding:8px;border-bottom:1px solid #F3F4F6;text-align:left;"><?= (float)($b['total_amount'] ?? 0) > 0 ? number_format((float)$b['total_amount'], 0) . ' ج.م' : '—' ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>المراية — إدارة الشبكات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Create New Grid -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="plus-circle" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">إنشاء مراية جديدة</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/mirror/create" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div class="form-group" style="margin:0;flex:1;min-width:200px;">
<label class="form-label">المرفق</label>
<select name="facility_id" class="form-select" required>
<option value="">-- اختر المرفق --</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>"><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;flex:1;min-width:180px;">
<label class="form-label">اسم المراية</label>
<input type="text" name="name_ar" class="form-input" required placeholder="مثال: مراية حمام السباحة الأولمبي">
</div>
<div class="form-group" style="margin:0;width:100px;">
<label class="form-label">الصفوف</label>
<input type="number" name="rows_count" class="form-input" value="4" min="1" max="10" style="direction:ltr;text-align:center;">
</div>
<div class="form-group" style="margin:0;width:100px;">
<label class="form-label">الأعمدة</label>
<input type="number" name="cols_count" class="form-input" value="6" min="1" max="12" style="direction:ltr;text-align:center;">
</div>
<button type="submit" class="btn btn-primary" style="height:38px;">
<i data-lucide="grid-3x3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إنشاء
</button>
</form>
</div>
</div>
<!-- Existing Grids -->
<?php if (!empty($grids)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(300px, 1fr));gap:20px;">
<?php foreach ($grids as $g): ?>
<div class="card" style="padding:20px;border-top:4px solid #0D7377;">
<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px;">
<div>
<h3 style="margin:0 0 4px;font-size:16px;color:#1A1A2E;"><?= e($g['name_ar']) ?></h3>
<div style="font-size:12px;color:#6B7280;"><?= e($g['facility_name'] ?? '') ?></div>
</div>
<div style="width:40px;height:40px;border-radius:10px;background:#0D737715;display:flex;align-items:center;justify-content:center;">
<i data-lucide="grid-3x3" style="width:20px;height:20px;color:#0D7377;"></i>
</div>
</div>
<div style="font-size:13px;color:#6B7280;margin-bottom:15px;">
<?= (int) $g['rows_count'] ?> صفوف × <?= (int) $g['cols_count'] ?> أعمدة
= <?= (int) $g['rows_count'] * (int) $g['cols_count'] ?> خانة
</div>
<div style="display:flex;gap:8px;">
<a href="/mirror/<?= (int) $g['id'] ?>" class="btn btn-primary" style="font-size:13px;flex:1;text-align:center;">
<i data-lucide="eye" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> فتح المراية
</a>
<form method="POST" action="/mirror/<?= (int) $g['id'] ?>/delete" style="margin:0;" onsubmit="return confirm('هل أنت متأكد من حذف هذه المراية؟')">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="font-size:13px;color:#DC2626;border-color:#DC2626;">
<i data-lucide="trash-2" style="width:14px;height:14px;vertical-align:middle;"></i>
</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card" style="padding:40px;text-align:center;">
<i data-lucide="grid-3x3" style="width:48px;height:48px;color:#D1D5DB;margin-bottom:12px;"></i>
<p style="color:#6B7280;font-size:14px;">لا توجد مرايات بعد. أنشئ واحدة جديدة للبدء.</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PlayerAffairs\Services\MedicalRecordService;
class MedicalApprovalController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('player.approve_medical');
$db = App::getInstance()->db();
$filter = trim((string) $request->get('filter', 'pending'));
$whereClause = match ($filter) {
'approved' => "pmr.approval_status = 'approved'",
'rejected' => "pmr.approval_status = 'rejected'",
default => "pmr.approval_status = 'pending'",
};
$records = $db->select(
"SELECT pmr.*, p.full_name_ar AS player_name, p.registration_serial,
p.player_type, p.photo_path
FROM player_medical_records pmr
LEFT JOIN players p ON p.id = pmr.player_id
WHERE {$whereClause}
ORDER BY pmr.created_at DESC
LIMIT 100"
);
return $this->view('PlayerAffairs.Views.medical_approvals', [
'records' => $records,
'filter' => $filter,
]);
}
public function approve(Request $request, string $id): Response
{
$this->authorize('player.approve_medical');
$employee = App::getInstance()->currentEmployee();
$approvedBy = $employee ? (int) $employee->id : 0;
try {
MedicalRecordService::approveRecord((int) $id, $approvedBy);
} catch (\RuntimeException $e) {
return $this->redirect('/medical-approvals')->withError($e->getMessage());
}
return $this->redirect('/medical-approvals')->withSuccess('تم اعتماد السجل الطبي بنجاح');
}
public function reject(Request $request, string $id): Response
{
$this->authorize('player.approve_medical');
$reason = trim((string) $request->post('rejection_reason', ''));
if (empty($reason)) {
return $this->redirect('/medical-approvals')->withError('يجب إدخال سبب الرفض');
}
$employee = App::getInstance()->currentEmployee();
$rejectedBy = $employee ? (int) $employee->id : 0;
try {
MedicalRecordService::rejectRecord((int) $id, $rejectedBy, $reason);
} catch (\RuntimeException $e) {
return $this->redirect('/medical-approvals')->withError($e->getMessage());
}
return $this->redirect('/medical-approvals')->withSuccess('تم رفض السجل الطبي');
}
}
...@@ -320,28 +320,29 @@ class PlayerController extends Controller ...@@ -320,28 +320,29 @@ class PlayerController extends Controller
public function addMedical(Request $request, string $id): Response public function addMedical(Request $request, string $id): Response
{ {
$data = [ $data = [
'record_type' => trim((string) $request->post('record_type', '')), 'record_type' => trim((string) $request->post('record_type', '')),
'exam_date' => trim((string) $request->post('exam_date', '')), 'certificate_type' => trim((string) $request->post('certificate_type', 'recreational')),
'expiry_date' => trim((string) $request->post('expiry_date', '')) ?: null, 'exam_date' => trim((string) $request->post('exam_date', '')),
'doctor_name' => trim((string) $request->post('doctor_name', '')) ?: null, 'expiry_date' => trim((string) $request->post('expiry_date', '')) ?: null,
'clinic_name' => trim((string) $request->post('clinic_name', '')) ?: null, 'doctor_name' => trim((string) $request->post('doctor_name', '')) ?: null,
'result' => trim((string) $request->post('result', '')), 'clinic_name' => trim((string) $request->post('clinic_name', '')) ?: null,
'restrictions' => trim((string) $request->post('restrictions', '')) ?: null, 'issuing_authority' => trim((string) $request->post('issuing_authority', '')) ?: null,
'document_id' => $request->post('document_id') ? (int) $request->post('document_id') : null, 'cert_number' => trim((string) $request->post('cert_number', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null, 'restrictions' => trim((string) $request->post('restrictions', '')) ?: null,
'document_id' => $request->post('document_id') ? (int) $request->post('document_id') : null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
]; ];
// Validation
$errors = []; $errors = [];
if (!array_key_exists($data['record_type'], PlayerMedicalRecord::getRecordTypes())) { if (!array_key_exists($data['record_type'], PlayerMedicalRecord::getRecordTypes())) {
$errors[] = 'نوع السجل الطبي غير صالح'; $errors[] = 'نوع السجل الطبي غير صالح';
} }
if (!array_key_exists($data['certificate_type'], PlayerMedicalRecord::getCertificateTypes())) {
$errors[] = 'نوع الشهادة غير صالح';
}
if (empty($data['exam_date'])) { if (empty($data['exam_date'])) {
$errors[] = 'تاريخ الفحص مطلوب'; $errors[] = 'تاريخ الفحص مطلوب';
} }
if (!empty($data['result']) && !array_key_exists($data['result'], PlayerMedicalRecord::getResults())) {
$errors[] = 'نتيجة الفحص غير صالحة';
}
if (!empty($errors)) { if (!empty($errors)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -355,7 +356,7 @@ class PlayerController extends Controller ...@@ -355,7 +356,7 @@ class PlayerController extends Controller
return $this->redirect('/players/' . $id)->withError($e->getMessage()); return $this->redirect('/players/' . $id)->withError($e->getMessage());
} }
return $this->redirect('/players/' . $id)->withSuccess('تم إضافة السجل الطبي بنجاح'); return $this->redirect('/players/' . $id)->withSuccess('تم رفع السجل الطبي بنجاح — في انتظار اعتماد مجلس الأمناء');
} }
/** /**
......
...@@ -16,11 +16,19 @@ class PlayerMedicalRecord extends Model ...@@ -16,11 +16,19 @@ class PlayerMedicalRecord extends Model
protected static array $fillable = [ protected static array $fillable = [
'player_id', 'player_id',
'record_type', 'record_type',
'certificate_type',
'validity_months',
'exam_date', 'exam_date',
'expiry_date', 'expiry_date',
'doctor_name', 'doctor_name',
'clinic_name', 'clinic_name',
'issuing_authority',
'cert_number',
'result', 'result',
'approval_status',
'approved_by',
'approved_at',
'rejection_reason',
'restrictions', 'restrictions',
'document_id', 'document_id',
'notes', 'notes',
...@@ -41,12 +49,34 @@ class PlayerMedicalRecord extends Model ...@@ -41,12 +49,34 @@ class PlayerMedicalRecord extends Model
]; ];
} }
/**
* Get certificate types with Arabic labels.
*/
public static function getCertificateTypes(): array
{
return [
'recreational' => 'ممارس',
'academy' => 'أكاديمي',
'international' => 'دولي',
];
}
public static function getApprovalStatuses(): array
{
return [
'pending' => 'في الانتظار',
'approved' => 'معتمد',
'rejected' => 'مرفوض',
];
}
/** /**
* Get medical results with Arabic labels. * Get medical results with Arabic labels.
*/ */
public static function getResults(): array public static function getResults(): array
{ {
return [ return [
'pending' => 'في الانتظار',
'fit' => 'لائق', 'fit' => 'لائق',
'conditional' => 'لائق بشروط', 'conditional' => 'لائق بشروط',
'unfit' => 'غير لائق', 'unfit' => 'غير لائق',
......
...@@ -19,6 +19,11 @@ return [ ...@@ -19,6 +19,11 @@ return [
['POST', '/api/players/parse-nid', 'PlayerAffairs\Controllers\PlayerController@parseNid', ['auth'], 'player.register'], ['POST', '/api/players/parse-nid', 'PlayerAffairs\Controllers\PlayerController@parseNid', ['auth'], 'player.register'],
['GET', '/api/players/academies', 'PlayerAffairs\Controllers\PlayerController@apiAcademies', ['auth'], 'player.view'], ['GET', '/api/players/academies', 'PlayerAffairs\Controllers\PlayerController@apiAcademies', ['auth'], 'player.view'],
// Medical Approvals (مجلس الأمناء)
['GET', '/medical-approvals', 'PlayerAffairs\Controllers\MedicalApprovalController@index', ['auth'], 'player.approve_medical'],
['POST', '/medical-approvals/{id:\d+}/approve', 'PlayerAffairs\Controllers\MedicalApprovalController@approve', ['auth', 'csrf'], 'player.approve_medical'],
['POST', '/medical-approvals/{id:\d+}/reject', 'PlayerAffairs\Controllers\MedicalApprovalController@reject', ['auth', 'csrf'], 'player.approve_medical'],
// Attendance // Attendance
['GET', '/attendance', 'PlayerAffairs\Controllers\AttendanceController@index', ['auth'], 'player.view'], ['GET', '/attendance', 'PlayerAffairs\Controllers\AttendanceController@index', ['auth'], 'player.view'],
['POST', '/attendance/record', 'PlayerAffairs\Controllers\AttendanceController@record', ['auth', 'csrf'], 'player.edit'], ['POST', '/attendance/record', 'PlayerAffairs\Controllers\AttendanceController@record', ['auth', 'csrf'], 'player.edit'],
......
...@@ -10,9 +10,15 @@ use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord; ...@@ -10,9 +10,15 @@ use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord;
final class MedicalRecordService final class MedicalRecordService
{ {
/** public static function getCertificateTypes(): array
* Add a medical record for a player and update their medical status. {
*/ return [
'recreational' => ['label' => 'ممارس', 'validity_months' => 12],
'academy' => ['label' => 'أكاديمي', 'validity_months' => 6],
'international' => ['label' => 'دولي', 'validity_months' => 3],
];
}
public static function addRecord(int $playerId, array $data): PlayerMedicalRecord public static function addRecord(int $playerId, array $data): PlayerMedicalRecord
{ {
$player = Player::find($playerId); $player = Player::find($playerId);
...@@ -22,43 +28,107 @@ final class MedicalRecordService ...@@ -22,43 +28,107 @@ final class MedicalRecordService
$employee = App::getInstance()->currentEmployee(); $employee = App::getInstance()->currentEmployee();
$certType = $data['certificate_type'] ?? 'recreational';
$types = self::getCertificateTypes();
$validityMonths = $types[$certType]['validity_months'] ?? 12;
$examDate = $data['exam_date'] ?? date('Y-m-d');
$expiryDate = $data['expiry_date'] ?? date('Y-m-d', strtotime($examDate . ' +' . $validityMonths . ' months'));
$record = PlayerMedicalRecord::create([ $record = PlayerMedicalRecord::create([
'player_id' => $playerId, 'player_id' => $playerId,
'record_type' => $data['record_type'] ?? 'medical_exam', 'record_type' => $data['record_type'] ?? 'fitness_cert',
'exam_date' => $data['exam_date'] ?? date('Y-m-d'), 'certificate_type' => $certType,
'expiry_date' => $data['expiry_date'] ?? null, 'validity_months' => $validityMonths,
'doctor_name' => $data['doctor_name'] ?? null, 'exam_date' => $examDate,
'clinic_name' => $data['clinic_name'] ?? null, 'expiry_date' => $expiryDate,
'result' => $data['result'] ?? 'pending', 'doctor_name' => $data['doctor_name'] ?? null,
'restrictions' => $data['restrictions'] ?? null, 'clinic_name' => $data['clinic_name'] ?? null,
'document_id' => $data['document_id'] ?? null, 'issuing_authority' => $data['issuing_authority'] ?? null,
'notes' => $data['notes'] ?? null, 'cert_number' => $data['cert_number'] ?? null,
'created_by' => $employee ? (int) $employee->id : null, 'result' => 'pending',
'approval_status' => 'pending',
'restrictions' => $data['restrictions'] ?? null,
'document_id' => $data['document_id'] ?? null,
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
]);
EventBus::dispatch('player.medical_uploaded', [
'player_id' => $playerId,
'record_id' => $record->id,
'certificate_type' => $certType,
]); ]);
// Update the player's medical status and expiry based on the new record return $record;
$updateData = []; }
if (!empty($data['result'])) {
$updateData['medical_status'] = $data['result']; public static function approveRecord(int $recordId, int $approvedBy): void
{
$db = App::getInstance()->db();
$record = $db->selectOne("SELECT * FROM player_medical_records WHERE id = ?", [$recordId]);
if (!$record) {
throw new \RuntimeException('السجل غير موجود');
} }
if (!empty($data['expiry_date'])) {
$updateData['medical_expiry_date'] = $data['expiry_date']; $db->update('player_medical_records', [
'approval_status' => 'approved',
'approved_by' => $approvedBy,
'approved_at' => date('Y-m-d H:i:s'),
'result' => 'fit',
], 'id = ?', [$recordId]);
$player = Player::find((int) $record['player_id']);
if ($player) {
$player->update([
'medical_status' => 'fit',
'medical_expiry_date' => $record['expiry_date'],
]);
} }
if (!empty($updateData)) {
$player->update($updateData); EventBus::dispatch('player.medical_approved', [
'player_id' => (int) $record['player_id'],
'record_id' => $recordId,
]);
}
public static function rejectRecord(int $recordId, int $rejectedBy, string $reason): void
{
$db = App::getInstance()->db();
$record = $db->selectOne("SELECT * FROM player_medical_records WHERE id = ?", [$recordId]);
if (!$record) {
throw new \RuntimeException('السجل غير موجود');
} }
EventBus::dispatch('player.medical_updated', [ $db->update('player_medical_records', [
'player_id' => $playerId, 'approval_status' => 'rejected',
'record_id' => $record->id, 'approved_by' => $rejectedBy,
'approved_at' => date('Y-m-d H:i:s'),
'rejection_reason' => $reason,
'result' => 'unfit',
], 'id = ?', [$recordId]);
EventBus::dispatch('player.medical_rejected', [
'player_id' => (int) $record['player_id'],
'record_id' => $recordId,
'reason' => $reason,
]); ]);
}
return $record; public static function getPendingApprovals(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT pmr.*, p.full_name_ar AS player_name, p.id AS player_id
FROM player_medical_records pmr
LEFT JOIN players p ON p.id = pmr.player_id
WHERE pmr.approval_status = 'pending'
ORDER BY pmr.created_at DESC"
);
} }
/**
* Check if a player has valid medical clearance.
*/
public static function checkClearance(int $playerId): bool public static function checkClearance(int $playerId): bool
{ {
$player = Player::find($playerId); $player = Player::find($playerId);
...@@ -78,9 +148,6 @@ final class MedicalRecordService ...@@ -78,9 +148,6 @@ final class MedicalRecordService
return $player->medical_expiry_date > date('Y-m-d'); return $player->medical_expiry_date > date('Y-m-d');
} }
/**
* Get players whose medical records expire within the given number of days.
*/
public static function getExpiringRecords(int $daysAhead = 30): array public static function getExpiringRecords(int $daysAhead = 30): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
......
<?php
use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord;
$__template->layout('Layout.main');
$certTypes = PlayerMedicalRecord::getCertificateTypes();
$recordTypes = PlayerMedicalRecord::getRecordTypes();
?>
<?php $__template->section('title'); ?>اعتماد السجلات الطبية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filter Tabs -->
<div style="display:flex;gap:8px;margin-bottom:20px;">
<a href="?filter=pending" class="btn <?= $filter === 'pending' ? 'btn-primary' : 'btn-outline' ?>" style="font-size:13px;">
<i data-lucide="clock" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> في الانتظار
</a>
<a href="?filter=approved" class="btn <?= $filter === 'approved' ? 'btn-primary' : 'btn-outline' ?>" style="font-size:13px;">
<i data-lucide="check-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> معتمد
</a>
<a href="?filter=rejected" class="btn <?= $filter === 'rejected' ? 'btn-primary' : 'btn-outline' ?>" style="font-size:13px;">
<i data-lucide="x-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> مرفوض
</a>
</div>
<?php if (empty($records)): ?>
<div class="card" style="padding:40px;text-align:center;">
<i data-lucide="clipboard-check" style="width:48px;height:48px;color:#D1D5DB;margin-bottom:12px;"></i>
<p style="color:#6B7280;font-size:15px;">لا توجد سجلات طبية <?= $filter === 'pending' ? 'في الانتظار' : ($filter === 'approved' ? 'معتمدة' : 'مرفوضة') ?></p>
</div>
<?php else: ?>
<div style="display:grid;gap:15px;">
<?php foreach ($records as $rec):
$recCertType = $rec['certificate_type'] ?? 'recreational';
$certLabel = $certTypes[$recCertType] ?? $recCertType;
$certColorMap = ['recreational' => '#2563EB', 'academy' => '#7C3AED', 'international' => '#DC2626'];
$certColor = $certColorMap[$recCertType] ?? '#6B7280';
$recTypeLabel = $recordTypes[$rec['record_type'] ?? ''] ?? ($rec['record_type'] ?? '—');
?>
<div class="card" style="padding:20px;border-right:4px solid <?= $certColor ?>;">
<div style="display:flex;justify-content:space-between;align-items:start;gap:20px;">
<!-- Player Info -->
<div style="display:flex;gap:15px;align-items:center;flex:1;">
<div style="width:50px;height:50px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0;">
<?php if (!empty($rec['photo_path'])): ?>
<img src="/<?= e($rec['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i data-lucide="user" style="width:24px;height:24px;color:#9CA3AF;"></i>
<?php endif; ?>
</div>
<div>
<div style="font-size:15px;font-weight:700;color:#1A1A2E;margin-bottom:4px;">
<a href="/players/<?= (int) $rec['player_id'] ?>" style="color:inherit;text-decoration:none;"><?= e($rec['player_name'] ?? '—') ?></a>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:#6B7280;">
<?php if (!empty($rec['registration_serial'])): ?>
<span><i data-lucide="hash" style="width:11px;height:11px;vertical-align:middle;"></i> <?= e($rec['registration_serial']) ?></span>
<?php endif; ?>
<span><?= e($rec['player_type'] === 'member' ? 'عضو' : 'غير عضو') ?></span>
</div>
</div>
</div>
<!-- Certificate Details -->
<div style="flex:1;">
<div style="display:flex;gap:10px;align-items:center;margin-bottom:8px;">
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $certColor ?>15;color:<?= $certColor ?>;"><?= e($certLabel) ?></span>
<span style="font-size:13px;color:#374151;font-weight:600;"><?= e($recTypeLabel) ?></span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:12px;color:#6B7280;">
<span>تاريخ الفحص: <strong style="color:#374151;"><?= e($rec['exam_date'] ?? '—') ?></strong></span>
<span>الانتهاء: <strong style="color:#374151;"><?= e($rec['expiry_date'] ?? '—') ?></strong></span>
<?php if (!empty($rec['doctor_name'])): ?>
<span>الطبيب: <strong style="color:#374151;"><?= e($rec['doctor_name']) ?></strong></span>
<?php endif; ?>
<?php if (!empty($rec['issuing_authority'])): ?>
<span>جهة الإصدار: <strong style="color:#374151;"><?= e($rec['issuing_authority']) ?></strong></span>
<?php endif; ?>
<?php if (!empty($rec['cert_number'])): ?>
<span>رقم الشهادة: <strong style="color:#374151;"><?= e($rec['cert_number']) ?></strong></span>
<?php endif; ?>
<?php if (!empty($rec['clinic_name'])): ?>
<span>العيادة: <strong style="color:#374151;"><?= e($rec['clinic_name']) ?></strong></span>
<?php endif; ?>
</div>
<?php if (!empty($rec['notes'])): ?>
<div style="margin-top:6px;font-size:12px;color:#6B7280;font-style:italic;"><?= e($rec['notes']) ?></div>
<?php endif; ?>
<?php if ($filter === 'rejected' && !empty($rec['rejection_reason'])): ?>
<div style="margin-top:8px;padding:8px 12px;background:#FEF2F2;border:1px solid #FCA5A5;border-radius:6px;font-size:12px;color:#DC2626;">
<strong>سبب الرفض:</strong> <?= e($rec['rejection_reason']) ?>
</div>
<?php endif; ?>
</div>
<!-- Actions -->
<?php if ($filter === 'pending'): ?>
<div style="display:flex;flex-direction:column;gap:8px;flex-shrink:0;">
<form method="POST" action="/medical-approvals/<?= (int) $rec['id'] ?>/approve" style="margin:0;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" style="font-size:12px;padding:8px 16px;width:100%;" onclick="return confirm('تأكيد اعتماد هذا السجل الطبي؟')">
<i data-lucide="check" style="width:13px;height:13px;vertical-align:middle;margin-left:4px;"></i> اعتماد
</button>
</form>
<button type="button" class="btn" style="font-size:12px;padding:8px 16px;color:#DC2626;border:1px solid #FCA5A5;background:#FEF2F2;border-radius:6px;cursor:pointer;width:100%;" onclick="toggleRejectForm(<?= (int) $rec['id'] ?>)">
<i data-lucide="x" style="width:13px;height:13px;vertical-align:middle;margin-left:4px;"></i> رفض
</button>
</div>
<?php endif; ?>
</div>
<?php if ($filter === 'pending'): ?>
<div id="rejectForm-<?= (int) $rec['id'] ?>" style="display:none;margin-top:15px;padding:15px;background:#FEF2F2;border:1px solid #FCA5A5;border-radius:8px;">
<form method="POST" action="/medical-approvals/<?= (int) $rec['id'] ?>/reject">
<?= csrf_field() ?>
<div class="form-group" style="margin:0 0 10px 0;">
<label class="form-label" style="font-size:12px;color:#DC2626;">سبب الرفض <span style="color:#DC2626;">*</span></label>
<textarea name="rejection_reason" class="form-input" rows="2" placeholder="أدخل سبب رفض السجل الطبي..." required></textarea>
</div>
<div style="display:flex;gap:8px;">
<button type="submit" class="btn btn-danger" style="font-size:12px;padding:6px 14px;" onclick="return confirm('تأكيد رفض هذا السجل الطبي؟')">تأكيد الرفض</button>
<button type="button" class="btn btn-outline" style="font-size:12px;padding:6px 14px;" onclick="toggleRejectForm(<?= (int) $rec['id'] ?>)">إلغاء</button>
</div>
</form>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<script>
function toggleRejectForm(id) {
var el = document.getElementById('rejectForm-' + id);
if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
...@@ -25,6 +25,8 @@ $typeLabel = ($playerTypes[$playerType] ?? $playerType); ...@@ -25,6 +25,8 @@ $typeLabel = ($playerTypes[$playerType] ?? $playerType);
$typeColor = $playerType === 'member' ? '#0284C7' : '#7C3AED'; $typeColor = $playerType === 'member' ? '#0284C7' : '#7C3AED';
$age = $player->getAge(); $age = $player->getAge();
$recordTypes = PlayerMedicalRecord::getRecordTypes(); $recordTypes = PlayerMedicalRecord::getRecordTypes();
$certTypes = PlayerMedicalRecord::getCertificateTypes();
$approvalStatuses = PlayerMedicalRecord::getApprovalStatuses();
$resultTypes = PlayerMedicalRecord::getResults(); $resultTypes = PlayerMedicalRecord::getResults();
$enrollmentStatuses = AcademyEnrollment::getStatuses(); $enrollmentStatuses = AcademyEnrollment::getStatuses();
?> ?>
...@@ -394,9 +396,9 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses(); ...@@ -394,9 +396,9 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<!-- Add Medical Record Inline Form (hidden by default) --> <!-- Add Medical Record Inline Form (hidden by default) -->
<div id="addMedicalForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;"> <div id="addMedicalForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<form method="POST" action="/players/<?= (int) $player->id ?>/medical"> <form method="POST" action="/players/<?= (int) $player->id ?>/medical" enctype="multipart/form-data">
<?= csrf_field() ?> <?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div class="form-group" style="margin:0;"> <div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">نوع السجل <span style="color:#DC2626;">*</span></label> <label class="form-label" style="font-size:12px;">نوع السجل <span style="color:#DC2626;">*</span></label>
<select name="record_type" class="form-input" required> <select name="record_type" class="form-input" required>
...@@ -406,6 +408,14 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses(); ...@@ -406,6 +408,14 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">نوع الشهادة <span style="color:#DC2626;">*</span></label>
<select name="certificate_type" class="form-input" required>
<?php foreach ($certTypes as $ctKey => $ctLabel): ?>
<option value="<?= e($ctKey) ?>"><?= e($ctLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;"> <div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">تاريخ الفحص <span style="color:#DC2626;">*</span></label> <label class="form-label" style="font-size:12px;">تاريخ الفحص <span style="color:#DC2626;">*</span></label>
<input type="date" name="exam_date" class="form-input" required> <input type="date" name="exam_date" class="form-input" required>
...@@ -413,9 +423,10 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses(); ...@@ -413,9 +423,10 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<div class="form-group" style="margin:0;"> <div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">تاريخ الانتهاء</label> <label class="form-label" style="font-size:12px;">تاريخ الانتهاء</label>
<input type="date" name="expiry_date" class="form-input"> <input type="date" name="expiry_date" class="form-input">
<span style="font-size:10px;color:#9CA3AF;">يحسب تلقائياً إذا تُرك فارغاً</span>
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-top:15px;"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;margin-top:15px;">
<div class="form-group" style="margin:0;"> <div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اسم الطبيب</label> <label class="form-label" style="font-size:12px;">اسم الطبيب</label>
<input type="text" name="doctor_name" class="form-input" placeholder="اسم الطبيب"> <input type="text" name="doctor_name" class="form-input" placeholder="اسم الطبيب">
...@@ -425,13 +436,12 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses(); ...@@ -425,13 +436,12 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<input type="text" name="clinic_name" class="form-input" placeholder="اسم المكان"> <input type="text" name="clinic_name" class="form-input" placeholder="اسم المكان">
</div> </div>
<div class="form-group" style="margin:0;"> <div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">النتيجة <span style="color:#DC2626;">*</span></label> <label class="form-label" style="font-size:12px;">جهة الإصدار</label>
<select name="result" class="form-input" required> <input type="text" name="issuing_authority" class="form-input" placeholder="جهة إصدار الشهادة">
<option value="">-- اختر --</option> </div>
<?php foreach ($resultTypes as $resKey => $resLabel): ?> <div class="form-group" style="margin:0;">
<option value="<?= e($resKey) ?>"><?= e($resLabel) ?></option> <label class="form-label" style="font-size:12px;">رقم الشهادة</label>
<?php endforeach; ?> <input type="text" name="cert_number" class="form-input" placeholder="رقم الشهادة" style="direction:ltr;text-align:left;">
</select>
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:15px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:15px;">
...@@ -444,9 +454,12 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses(); ...@@ -444,9 +454,12 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea> <textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea>
</div> </div>
</div> </div>
<div style="margin-top:15px;padding:12px;background:#FEF3C7;border:1px solid #FDE68A;border-radius:8px;">
<span style="font-size:12px;color:#92400E;"><i data-lucide="info" style="width:13px;height:13px;vertical-align:middle;margin-left:4px;"></i> بعد الرفع سيتم إرسال السجل لمجلس الأمناء للاعتماد</span>
</div>
<div style="margin-top:15px;display:flex;gap:8px;"> <div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;"> <button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ السجل <i data-lucide="upload" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> رفع السجل الطبي
</button> </button>
<button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleMedicalForm()">إلغاء</button> <button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleMedicalForm()">إلغاء</button>
</div> </div>
...@@ -459,26 +472,31 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses(); ...@@ -459,26 +472,31 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<thead> <thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;"> <tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">نوع السجل</th> <th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">نوع السجل</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">نوع الشهادة</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">تاريخ الفحص</th> <th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">تاريخ الفحص</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">تاريخ الانتهاء</th> <th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">تاريخ الانتهاء</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">النتيجة</th> <th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">حالة الاعتماد</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">الطبيب</th> <th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">الطبيب</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($medicalRecords as $rec): <?php foreach ($medicalRecords as $rec):
$recType = $rec['record_type'] ?? ''; $recType = $rec['record_type'] ?? '';
$recResult = $rec['result'] ?? ''; $recCertType = $rec['certificate_type'] ?? 'recreational';
$resultLabel = $resultTypes[$recResult] ?? $recResult; $recApproval = $rec['approval_status'] ?? 'pending';
$resultColorMap = ['fit' => '#059669', 'conditional' => '#D97706', 'unfit' => '#DC2626']; $approvalColorMap = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' => '#DC2626'];
$resultColor = $resultColorMap[$recResult] ?? '#6B7280'; $approvalColor = $approvalColorMap[$recApproval] ?? '#6B7280';
?> ?>
<tr style="border-bottom:1px solid #F3F4F6;"> <tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 15px;font-weight:600;"><?= e($recordTypes[$recType] ?? $recType) ?></td> <td style="padding:12px 15px;font-weight:600;"><?= e($recordTypes[$recType] ?? $recType) ?></td>
<td style="padding:12px 15px;"><?= e($certTypes[$recCertType] ?? $recCertType) ?></td>
<td style="padding:12px 15px;"><?= e($rec['exam_date'] ?? '—') ?></td> <td style="padding:12px 15px;"><?= e($rec['exam_date'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($rec['expiry_date'] ?? '—') ?></td> <td style="padding:12px 15px;"><?= e($rec['expiry_date'] ?? '—') ?></td>
<td style="padding:12px 15px;"> <td style="padding:12px 15px;">
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $resultColor ?>15;color:<?= $resultColor ?>;"><?= e($resultLabel) ?></span> <span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $approvalColor ?>15;color:<?= $approvalColor ?>;"><?= e($approvalStatuses[$recApproval] ?? $recApproval) ?></span>
<?php if ($recApproval === 'rejected' && !empty($rec['rejection_reason'])): ?>
<div style="font-size:11px;color:#DC2626;margin-top:4px;"><?= e($rec['rejection_reason']) ?></div>
<?php endif; ?>
</td> </td>
<td style="padding:12px 15px;"><?= e($rec['doctor_name'] ?? '—') ?></td> <td style="padding:12px 15px;"><?= e($rec['doctor_name'] ?? '—') ?></td>
</tr> </tr>
......
...@@ -11,6 +11,7 @@ PermissionRegistry::register('player_affairs', [ ...@@ -11,6 +11,7 @@ PermissionRegistry::register('player_affairs', [
'player.manage_card' => ['ar' => 'إدارة كارنيه اللاعب', 'en' => 'Manage Player Card'], 'player.manage_card' => ['ar' => 'إدارة كارنيه اللاعب', 'en' => 'Manage Player Card'],
'player.view_medical' => ['ar' => 'عرض السجل الطبي', 'en' => 'View Medical Records'], 'player.view_medical' => ['ar' => 'عرض السجل الطبي', 'en' => 'View Medical Records'],
'player.manage_medical' => ['ar' => 'إدارة السجل الطبي', 'en' => 'Manage Medical Records'], 'player.manage_medical' => ['ar' => 'إدارة السجل الطبي', 'en' => 'Manage Medical Records'],
'player.approve_medical' => ['ar' => 'اعتماد السجل الطبي', 'en' => 'Approve Medical Records'],
'player.record_attendance' => ['ar' => 'تسجيل الحضور', 'en' => 'Record Attendance'], 'player.record_attendance' => ['ar' => 'تسجيل الحضور', 'en' => 'Record Attendance'],
]); ]);
......
<?php
declare(strict_types=1);
namespace App\Modules\Reservations\Services;
use App\Core\App;
final class FacilityPricingService
{
private static ?array $memberPricing = null;
private static ?array $nonMemberPricing = null;
private static ?array $generalFees = null;
public static function calculateRate(int $facilityId, bool $isMember, string $timeTier = 'AM'): ?float
{
$db = App::getInstance()->db();
$facility = $db->selectOne("SELECT * FROM facilities WHERE id = ?", [$facilityId]);
if (!$facility) {
return null;
}
$rateCol = $timeTier === 'PM'
? ($isMember ? 'hourly_rate_member_pm' : 'hourly_rate_nonmember_pm')
: ($isMember ? 'hourly_rate_member' : 'hourly_rate_nonmember');
$directRate = (float) ($facility[$rateCol] ?? 0);
if ($directRate > 0) {
return $directRate;
}
$facilityType = $facility['facility_type'] ?? '';
if (empty($facilityType)) {
return null;
}
$pricing = $isMember ? self::loadMemberPricing() : self::loadNonMemberPricing();
foreach ($pricing as $rule) {
if (($rule['facility_type'] ?? '') === $facilityType) {
return (float) ($timeTier === 'PM'
? ($rule['price_evening'] ?? 0)
: ($rule['price_morning'] ?? 0));
}
}
return null;
}
public static function getEntryTicketPrice(string $dayType, bool $isAbove12): ?float
{
$db = App::getInstance()->db();
$json = $db->selectOne(
"SELECT config_value FROM system_config WHERE config_key = 'entry_tickets_non_member'"
);
if (!$json) {
return null;
}
$tickets = json_decode($json['config_value'], true) ?: [];
foreach ($tickets as $ticket) {
if (($ticket['day_type'] ?? '') === $dayType) {
return (float) ($isAbove12 ? $ticket['age_above_12'] : $ticket['age_6_to_12']);
}
}
return null;
}
public static function getGeneralFee(string $feeCode): ?float
{
$fees = self::loadGeneralFees();
foreach ($fees as $fee) {
if (($fee['code'] ?? '') === $feeCode) {
return (float) ($fee['amount'] ?? 0);
}
}
return null;
}
public static function getAllPricingForFacilityType(string $facilityType): array
{
$memberPricing = self::loadMemberPricing();
$nonMemberPricing = self::loadNonMemberPricing();
$result = ['member' => null, 'non_member' => null];
foreach ($memberPricing as $rule) {
if (($rule['facility_type'] ?? '') === $facilityType) {
$result['member'] = $rule;
break;
}
}
foreach ($nonMemberPricing as $rule) {
if (($rule['facility_type'] ?? '') === $facilityType) {
$result['non_member'] = $rule;
break;
}
}
return $result;
}
private static function loadMemberPricing(): array
{
if (self::$memberPricing !== null) {
return self::$memberPricing;
}
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = 'sports_pricing_member'");
self::$memberPricing = $row ? (json_decode($row['config_value'], true) ?: []) : [];
return self::$memberPricing;
}
private static function loadNonMemberPricing(): array
{
if (self::$nonMemberPricing !== null) {
return self::$nonMemberPricing;
}
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = 'sports_pricing_non_member'");
self::$nonMemberPricing = $row ? (json_decode($row['config_value'], true) ?: []) : [];
return self::$nonMemberPricing;
}
private static function loadGeneralFees(): array
{
if (self::$generalFees !== null) {
return self::$generalFees;
}
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = 'general_fees'");
self::$generalFees = $row ? (json_decode($row['config_value'], true) ?: []) : [];
return self::$generalFees;
}
}
...@@ -6,7 +6,6 @@ namespace App\Modules\Reservations\Services; ...@@ -6,7 +6,6 @@ namespace App\Modules\Reservations\Services;
use App\Core\App; use App\Core\App;
use App\Core\EventBus; use App\Core\EventBus;
use App\Modules\Reservations\Models\Reservation; use App\Modules\Reservations\Models\Reservation;
use App\Modules\Facilities\Models\Facility;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
class ReservationService class ReservationService
...@@ -49,10 +48,14 @@ class ReservationService ...@@ -49,10 +48,14 @@ class ReservationService
// Calculate unit rate and total amount if facility is set // Calculate unit rate and total amount if facility is set
if (!empty($data['facility_id']) && empty($data['unit_rate'])) { if (!empty($data['facility_id']) && empty($data['unit_rate'])) {
$facility = Facility::find((int) $data['facility_id']); $isMember = in_array($data['booker_type'] ?? '', ['member', 'player']);
if ($facility) { $rate = FacilityPricingService::calculateRate(
$isMember = in_array($data['booker_type'] ?? '', ['member', 'player']); (int) $data['facility_id'],
$data['unit_rate'] = $facility->getRate($isMember, $data['time_tier']); $isMember,
$data['time_tier']
);
if ($rate !== null) {
$data['unit_rate'] = $rate;
} }
} }
......
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `mirror_grids` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`facility_id` BIGINT UNSIGNED NOT NULL,
`name_ar` VARCHAR(300) NOT NULL,
`rows_count` INT UNSIGNED NOT NULL DEFAULT 4,
`cols_count` INT UNSIGNED NOT NULL DEFAULT 6,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_mg_facility` (`facility_id`),
CONSTRAINT `fk_mg_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `mirror_grids`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `mirror_boxes` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`grid_id` BIGINT UNSIGNED NOT NULL,
`row_index` INT UNSIGNED NOT NULL,
`col_index` INT UNSIGNED NOT NULL,
`label` VARCHAR(100) NULL,
`coach_id` BIGINT UNSIGNED NULL,
`coach_name` VARCHAR(300) NULL COMMENT 'Manual name if no coach entity',
`academy_id` BIGINT UNSIGNED NULL,
`max_trainees` INT UNSIGNED NOT NULL DEFAULT 5,
`age_group` VARCHAR(50) NULL COMMENT 'e.g. 4-6, 7-9, 10-12',
`gender` VARCHAR(10) NULL COMMENT 'male, female, mixed',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_mb_grid` (`grid_id`),
UNIQUE KEY `uq_mb_position` (`grid_id`, `row_index`, `col_index`),
CONSTRAINT `fk_mb_grid` FOREIGN KEY (`grid_id`) REFERENCES `mirror_grids`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `mirror_boxes`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `mirror_box_trainees` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`box_id` BIGINT UNSIGNED NOT NULL,
`player_id` BIGINT UNSIGNED NULL,
`trainee_name` VARCHAR(300) NULL COMMENT 'Manual name if no player entity',
`assigned_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`assigned_by` BIGINT UNSIGNED NULL,
INDEX `idx_mbt_box` (`box_id`),
INDEX `idx_mbt_player` (`player_id`),
CONSTRAINT `fk_mbt_box` FOREIGN KEY (`box_id`) REFERENCES `mirror_boxes`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_mbt_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `mirror_box_trainees`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `player_medical_records`
ADD COLUMN `approval_status` VARCHAR(30) NOT NULL DEFAULT 'pending' COMMENT 'pending, approved, rejected' AFTER `result`,
ADD COLUMN `approved_by` BIGINT UNSIGNED NULL AFTER `approval_status`,
ADD COLUMN `approved_at` TIMESTAMP NULL AFTER `approved_by`,
ADD COLUMN `rejection_reason` VARCHAR(500) NULL AFTER `approved_at`,
ADD INDEX `idx_pmr_approval` (`approval_status`)",
'down' => "ALTER TABLE `player_medical_records`
DROP INDEX `idx_pmr_approval`,
DROP COLUMN `rejection_reason`,
DROP COLUMN `approved_at`,
DROP COLUMN `approved_by`,
DROP COLUMN `approval_status`",
];
This diff is collapsed.
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