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', [
['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' => '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' => 'Attendance', 'route' => '/attendance', 'permission' => 'player.view', 'order' => 5],
['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 6],
......
......@@ -12,54 +12,94 @@ class FacilityDashboardController extends Controller
{
public function show(Request $request, string $id): Response
{
$this->authorize('facility.dashboard');
$db = App::getInstance()->db();
$facility = $db->selectOne("SELECT * FROM facilities WHERE id = ? AND is_archived = 0", [(int) $id]);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$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(
"SELECT * FROM reservations WHERE facility_id = ? AND reservation_date = ? AND status NOT IN ('cancelled') ORDER BY start_time ASC",
[(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(
"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]
);
$weekStart = date('Y-m-d', strtotime('monday this week'));
$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')",
[(int) $id, $weekStart]
"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, date('Y-m-d', strtotime('sunday this week'))]
);
$monthStart = date('Y-m-01');
$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')",
[(int) $id, $monthStart]
);
$monthBookingCount = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND status NOT IN ('cancelled')",
[(int) $id, $monthStart]
"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, date('Y-m-t')]
);
$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]
);
return $this->view('FacilityDashboards.Views.dashboard', [
'facility' => $facility,
'todayBookings' => $todayBookings,
'todayRevenue' => (float) ($todayRevenue['total'] ?? 0),
'weekRevenue' => (float) ($weekRevenue['total'] ?? 0),
'monthRevenue' => (float) ($monthRevenue['total'] ?? 0),
'monthBookingCount' => (int) ($monthBookingCount['cnt'] ?? 0),
'upcomingBookings' => $upcomingBookings,
'facility' => $facility,
'todayBookings' => $todayBookings,
'periodBookings' => $periodBookings,
'todayRevenue' => (float) ($todayRevenue['total'] ?? 0),
'weekRevenue' => (float) ($weekRevenue['total'] ?? 0),
'monthRevenue' => (float) ($monthRevenue['total'] ?? 0),
'periodRevenue' => (float) ($periodRevenue['total'] ?? 0),
'periodLabel' => $periodLabel,
'filterMode' => $filterMode,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'upcomingBookings' => $upcomingBookings,
]);
}
}
......@@ -13,45 +13,166 @@ class MirrorDisplayController extends Controller
{
public function index(Request $request): Response
{
$facilityType = trim((string) $request->get('type', ''));
$disciplineId = $request->get('discipline_id', '') !== '' ? (int) $request->get('discipline_id') : null;
$this->authorize('facility.mirror');
$states = MirrorDisplayService::getFacilityStates(
$facilityType ?: null,
$disciplineId
$grids = MirrorDisplayService::getAllGrids();
$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();
$facilityTypes = $db->select(
"SELECT DISTINCT facility_type FROM facilities WHERE is_active = 1 AND is_archived = 0 ORDER BY facility_type"
$coaches = $db->select(
"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(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar"
$players = $db->select(
"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', [
'states' => $states,
'facilityTypes' => $facilityTypes,
'disciplines' => $disciplines,
'currentType' => $facilityType,
'currentDisc' => $disciplineId,
'grid' => $grid,
'coaches' => $coaches,
'academies' => $academies,
'players' => $players,
]);
}
public function apiState(Request $request): Response
public function updateBox(Request $request, string $gridId, string $boxId): Response
{
$facilityType = trim((string) $request->get('type', ''));
$disciplineId = $request->get('discipline_id', '') !== '' ? (int) $request->get('discipline_id') : null;
$this->authorize('facility.mirror');
$states = MirrorDisplayService::getFacilityStates(
$facilityType ?: null,
$disciplineId
$data = [];
if ($request->post('coach_id') !== null) {
$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([
'states' => $states,
'boxes' => $state,
'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 @@
declare(strict_types=1);
return [
['GET', '/mirror', 'FacilityDashboards\Controllers\MirrorDisplayController@index', ['auth'], 'facility.mirror'],
['GET', '/api/mirror/state', 'FacilityDashboards\Controllers\MirrorDisplayController@apiState', ['auth'], 'facility.mirror'],
['GET', '/facilities/{id:\d+}/dashboard', 'FacilityDashboards\Controllers\FacilityDashboardController@show', ['auth'], 'facility.dashboard'],
// Mirror Grid System
['GET', '/mirror', 'FacilityDashboards\Controllers\MirrorDisplayController@index', ['auth'], 'facility.mirror'],
['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'],
];
......@@ -7,102 +7,198 @@ use App\Core\App;
final class MirrorDisplayService
{
public static function getFacilityStates(?string $facilityType = null, ?int $disciplineId = null): array
public static function getGrid(int $gridId): ?array
{
$db = App::getInstance()->db();
$now = date('H:i:s');
$today = date('Y-m-d');
$sql = "SELECT f.id, f.name_ar, f.name_en, f.facility_type, f.capacity, f.location,
f.linked_discipline_id, f.is_active
FROM facilities f
WHERE f.is_active = 1 AND f.is_archived = 0";
$params = [];
if ($facilityType) {
$sql .= " AND f.facility_type = ?";
$params[] = $facilityType;
}
if ($disciplineId) {
$sql .= " AND f.linked_discipline_id = ?";
$params[] = $disciplineId;
}
$sql .= " ORDER BY f.facility_type ASC, f.name_ar ASC";
$facilities = $db->select($sql, $params);
$grid = $db->selectOne(
"SELECT mg.*, f.name_ar AS facility_name
FROM mirror_grids mg
LEFT JOIN facilities f ON f.id = mg.facility_id
WHERE mg.id = ? AND mg.is_active = 1",
[$gridId]
);
$states = [];
foreach ($facilities as $f) {
$facilityId = (int) $f['id'];
if (!$grid) {
return null;
}
$currentBooking = $db->selectOne(
"SELECT id, member_id, notes, start_time, end_time, status
FROM reservations
WHERE facility_id = ? AND reservation_date = ? AND status IN ('confirmed', 'checked_in')
AND start_time <= ? AND end_time > ?
ORDER BY start_time ASC LIMIT 1",
[$facilityId, $today, $now, $now]
);
$boxes = $db->select(
"SELECT mb.*, c.full_name_ar AS coach_full_name, a.name_ar AS academy_name
FROM mirror_boxes mb
LEFT JOIN coaches c ON c.id = mb.coach_id
LEFT JOIN academies a ON a.id = mb.academy_id
WHERE mb.grid_id = ? AND mb.is_active = 1
ORDER BY mb.row_index ASC, mb.col_index ASC",
[$gridId]
);
$nextBooking = $db->selectOne(
"SELECT id, notes, start_time, end_time
FROM reservations
WHERE facility_id = ? AND reservation_date = ? AND status = 'confirmed'
AND start_time > ?
ORDER BY start_time ASC LIMIT 1",
[$facilityId, $today, $now]
foreach ($boxes as &$box) {
$box['trainees'] = $db->select(
"SELECT mbt.*, p.full_name_ar AS player_full_name
FROM mirror_box_trainees mbt
LEFT JOIN players p ON p.id = mbt.player_id
WHERE mbt.box_id = ?
ORDER BY mbt.assigned_at ASC",
[(int) $box['id']]
);
$box['trainee_count'] = count($box['trainees']);
}
unset($box);
$todayBookings = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM reservations
WHERE facility_id = ? AND reservation_date = ? AND status NOT IN ('cancelled')
",
[$facilityId, $today]
);
$grid['boxes'] = $boxes;
return $grid;
}
if ($currentBooking) {
$status = $currentBooking['status'] === 'checked_in' ? 'in_progress' : 'booked';
} else {
$status = 'available';
}
public static function getAllGrids(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT mg.*, f.name_ar AS facility_name
FROM mirror_grids mg
LEFT JOIN facilities f ON f.id = mg.facility_id
WHERE mg.is_active = 1
ORDER BY mg.name_ar ASC"
);
}
public static function createGrid(int $facilityId, string $nameAr, int $rows, int $cols): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$timeUntilNext = null;
if ($nextBooking) {
$diff = strtotime($nextBooking['start_time']) - strtotime($now);
if ($diff > 0) {
$mins = (int) ($diff / 60);
if ($mins < 60) {
$timeUntilNext = $mins . ' دقيقة';
} else {
$hours = (int) ($mins / 60);
$remaining = $mins % 60;
$timeUntilNext = $hours . ' ساعة' . ($remaining > 0 ? ' و ' . $remaining . ' دقيقة' : '');
}
}
$gridId = $db->insert('mirror_grids', [
'facility_id' => $facilityId,
'name_ar' => $nameAr,
'rows_count' => $rows,
'cols_count' => $cols,
'is_active' => 1,
'created_by' => $employee ? ($employee->id ?? $employee['id'] ?? null) : null,
]);
for ($r = 0; $r < $rows; $r++) {
for ($c = 0; $c < $cols; $c++) {
$db->insert('mirror_boxes', [
'grid_id' => $gridId,
'row_index' => $r,
'col_index' => $c,
'max_trainees' => 5,
'is_active' => 1,
]);
}
}
return $gridId;
}
public static function updateBox(int $boxId, array $data): void
{
$db = App::getInstance()->db();
$db->update('mirror_boxes', $data, 'id = ?', [$boxId]);
}
public static function addTrainee(int $boxId, ?int $playerId, ?string $traineeName): bool
{
$db = App::getInstance()->db();
$box = $db->selectOne("SELECT id, max_trainees FROM mirror_boxes WHERE id = ?", [$boxId]);
if (!$box) {
return false;
}
$currentCount = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM mirror_box_trainees WHERE box_id = ?",
[$boxId]
)['cnt'] ?? 0;
if ((int) $currentCount >= (int) $box['max_trainees']) {
return false;
}
$employee = App::getInstance()->currentEmployee();
$db->insert('mirror_box_trainees', [
'box_id' => $boxId,
'player_id' => $playerId,
'trainee_name' => $traineeName,
'assigned_by' => $employee ? ($employee->id ?? $employee['id'] ?? null) : null,
]);
return true;
}
public static function removeTrainee(int $traineeId): void
{
$db = App::getInstance()->db();
$db->delete('mirror_box_trainees', 'id = ?', [$traineeId]);
}
public static function moveTrainee(int $traineeId, int $toBoxId): bool
{
$db = App::getInstance()->db();
$box = $db->selectOne("SELECT id, max_trainees FROM mirror_boxes WHERE id = ?", [$toBoxId]);
if (!$box) {
return false;
}
$currentCount = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM mirror_box_trainees WHERE box_id = ?",
[$toBoxId]
)['cnt'] ?? 0;
if ((int) $currentCount >= (int) $box['max_trainees']) {
return false;
}
$db->update('mirror_box_trainees', ['box_id' => $toBoxId], 'id = ?', [$traineeId]);
return true;
}
public static function getGridState(int $gridId): array
{
$db = App::getInstance()->db();
$boxes = $db->select(
"SELECT mb.id, mb.row_index, mb.col_index, mb.label, mb.coach_name,
mb.max_trainees, mb.age_group, mb.gender,
c.full_name_ar AS coach_full_name,
a.name_ar AS academy_name
FROM mirror_boxes mb
LEFT JOIN coaches c ON c.id = mb.coach_id
LEFT JOIN academies a ON a.id = mb.academy_id
WHERE mb.grid_id = ? AND mb.is_active = 1
ORDER BY mb.row_index ASC, mb.col_index ASC",
[$gridId]
);
$result = [];
foreach ($boxes as $box) {
$trainees = $db->select(
"SELECT mbt.id, mbt.trainee_name, p.full_name_ar AS player_name
FROM mirror_box_trainees mbt
LEFT JOIN players p ON p.id = mbt.player_id
WHERE mbt.box_id = ?",
[(int) $box['id']]
);
$states[] = [
'facility_id' => $facilityId,
'name_ar' => $f['name_ar'],
'type' => $f['facility_type'],
'location' => $f['location'] ?? '',
'capacity' => (int) ($f['capacity'] ?? 0),
'current_status' => $status,
'current_booking' => $currentBooking ? [
'purpose' => $currentBooking['notes'] ?? '',
'start_time' => $currentBooking['start_time'],
'end_time' => $currentBooking['end_time'],
] : null,
'next_booking' => $nextBooking ? [
'purpose' => $nextBooking['notes'] ?? '',
'start_time' => $nextBooking['start_time'],
'end_time' => $nextBooking['end_time'],
] : null,
'time_until_next' => $timeUntilNext,
'today_count' => (int) ($todayBookings['cnt'] ?? 0),
$result[] = [
'id' => (int) $box['id'],
'row' => (int) $box['row_index'],
'col' => (int) $box['col_index'],
'label' => $box['label'],
'coach' => $box['coach_full_name'] ?? $box['coach_name'] ?? '',
'academy' => $box['academy_name'] ?? '',
'age_group' => $box['age_group'] ?? '',
'gender' => $box['gender'] ?? 'mixed',
'max_trainees' => (int) $box['max_trainees'],
'trainees' => array_map(fn($t) => [
'id' => (int) $t['id'],
'name' => $t['player_name'] ?? $t['trainee_name'] ?? '',
], $trainees),
'trainee_count' => count($trainees),
];
}
return $states;
return $result;
}
}
......@@ -8,6 +8,24 @@
<?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 -->
<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;">
......@@ -28,6 +46,17 @@
</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 -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
......@@ -57,7 +86,7 @@
<?= ['confirmed'=>'مؤكد','checked_in'=>'جاري','completed'=>'مكتمل'][$b['status']] ?? $b['status'] ?>
</span>
</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>
<?php endif; ?>
</div>
......@@ -82,7 +111,7 @@
<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:left;border-bottom:1px solid #E5E7EB;">المبلغ</th>
</tr>
</thead>
......@@ -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;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;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>
<?php endforeach; ?>
</tbody>
......
<?php $__template->layout('Layout.mirror'); ?>
<?php $__template->section('title'); ?>المراية — عرض مباشر<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>المراية: <?= e($grid['name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/mirror" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> كل المرايات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusConfig = [
'available' => ['label' => 'متاح', 'color' => '#10B981', 'bg' => '#ECFDF5', 'icon' => 'check-circle'],
'booked' => ['label' => 'محجوز', 'color' => '#3B82F6', 'bg' => '#EFF6FF', 'icon' => 'calendar'],
'in_progress' => ['label' => 'جاري', 'color' => '#F59E0B', 'bg' => '#FFFBEB', 'icon' => 'play-circle'],
'maintenance' => ['label' => 'صيانة', 'color' => '#EF4444', 'bg' => '#FEF2F2', 'icon' => 'wrench'],
'closed' => ['label' => 'مغلق', 'color' => '#6B7280', 'bg' => '#F9FAFB', 'icon' => 'x-circle'],
];
$typeLabels = [
'court' => 'ملعب',
'pool' => 'حمام سباحة',
'gym' => 'صالة رياضية',
'track' => 'مضمار',
'hall' => 'قاعة',
'field' => 'ملعب كبير',
'other' => 'أخرى',
];
$rows = (int) $grid['rows_count'];
$cols = (int) $grid['cols_count'];
$gridId = (int) $grid['id'];
$boxMap = [];
foreach ($grid['boxes'] as $box) {
$key = $box['row_index'] . '_' . $box['col_index'];
$boxMap[$key] = $box;
}
$genderLabels = ['male' => 'أولاد', 'female' => 'بنات', 'mixed' => 'مختلط'];
$genderColors = ['male' => '#2563EB', 'female' => '#EC4899', 'mixed' => '#6B7280'];
?>
<!-- Header Bar -->
<div style="position:fixed;top:0;right:0;left:0;z-index:100;background:linear-gradient(135deg, #0D7377, #14919B);padding:15px 30px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 4px 20px rgba(0,0,0,0.15);">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="monitor" style="width:28px;height:28px;color:white;"></i>
<h1 style="margin:0;font-size:22px;color:white;font-weight:700;">المراية — عرض مباشر</h1>
</div>
<div style="display:flex;align-items:center;gap:20px;">
<!-- Filters -->
<select id="filterType" style="padding:6px 12px;border-radius:6px;border:none;font-size:13px;background:rgba(255,255,255,0.9);" onchange="applyFilters()">
<option value="">كل الأنواع</option>
<?php foreach ($facilityTypes as $ft): ?>
<option value="<?= e($ft['facility_type']) ?>" <?= $currentType === $ft['facility_type'] ? 'selected' : '' ?>><?= e($typeLabels[$ft['facility_type']] ?? $ft['facility_type']) ?></option>
<?php endforeach; ?>
</select>
<!-- Clock -->
<div id="liveClock" style="font-size:28px;font-weight:700;color:white;font-family:monospace;direction:ltr;"></div>
<!-- Status Indicator -->
<div style="display:flex;align-items:center;gap:6px;">
<span id="syncDot" style="width:10px;height:10px;border-radius:50%;background:#10B981;animation:pulse 2s infinite;"></span>
<span style="font-size:11px;color:rgba(255,255,255,0.8);">مباشر</span>
<!-- Grid Header -->
<div class="card" style="margin-bottom:15px;padding:15px 20px;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:10px;">
<i data-lucide="grid-3x3" style="width:20px;height:20px;color:#0D7377;"></i>
<div>
<span style="font-size:15px;font-weight:600;color:#1A1A2E;"><?= e($grid['name_ar']) ?></span>
<span style="font-size:12px;color:#6B7280;margin-right:8px;"><?= e($grid['facility_name'] ?? '') ?></span>
</div>
</div>
<div style="font-size:12px;color:#6B7280;">
<?= $rows ?> × <?= $cols ?> = <?= $rows * $cols ?> خانة
</div>
</div>
<!-- Legend -->
<div style="position:fixed;top:65px;right:0;left:0;z-index:99;background:white;padding:10px 30px;border-bottom:1px solid #E5E7EB;display:flex;gap:20px;align-items:center;">
<?php foreach ($statusConfig as $st => $cfg): ?>
<span style="display:flex;align-items:center;gap:6px;font-size:13px;">
<span style="width:16px;height:16px;border-radius:4px;background:<?= $cfg['color'] ?>;display:inline-block;"></span>
<?= $cfg['label'] ?>
</span>
<?php endforeach; ?>
<span style="margin-right:auto;font-size:12px;color:#9CA3AF;" id="lastUpdate">آخر تحديث: <?= date('H:i:s') ?></span>
</div>
<!-- Facility Grid -->
<div id="facilityGrid" style="padding:120px 30px 30px;display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:20px;min-height:100vh;">
<?php foreach ($states as $s):
$cfg = $statusConfig[$s['current_status']] ?? $statusConfig['closed'];
?>
<div class="mirror-card" data-facility-id="<?= $s['facility_id'] ?>" style="
border-radius:16px;
border:3px solid <?= $cfg['color'] ?>40;
background:<?= $cfg['bg'] ?>;
padding:20px;
transition:all 0.3s;
position:relative;
overflow:hidden;
">
<!-- Status indicator stripe -->
<div style="position:absolute;top:0;right:0;left:0;height:5px;background:<?= $cfg['color'] ?>;"></div>
<!-- Header -->
<div style="display:flex;align-items:start;justify-content:space-between;margin-bottom:12px;">
<div>
<h3 style="margin:0 0 4px;font-size:18px;font-weight:700;color:#1A1A2E;"><?= e($s['name_ar']) ?></h3>
<span style="font-size:12px;color:#6B7280;"><?= e($typeLabels[$s['type']] ?? $s['type']) ?></span>
<!-- Interactive Grid -->
<div style="display:grid;grid-template-columns:repeat(<?= $cols ?>, 1fr);gap:10px;margin-bottom:20px;">
<?php for ($r = 0; $r < $rows; $r++): ?>
<?php for ($c = 0; $c < $cols; $c++):
$key = $r . '_' . $c;
$box = $boxMap[$key] ?? null;
$boxId = $box ? (int) $box['id'] : 0;
$traineeCount = $box ? (int) ($box['trainee_count'] ?? 0) : 0;
$maxTrainees = $box ? (int) ($box['max_trainees'] ?? 5) : 5;
$isFull = $traineeCount >= $maxTrainees;
$gender = $box['gender'] ?? 'mixed';
$gc = $genderColors[$gender] ?? '#6B7280';
$coachDisplay = '';
if (!empty($box['coach_full_name'])) $coachDisplay = $box['coach_full_name'];
elseif (!empty($box['coach_name'])) $coachDisplay = $box['coach_name'];
$academyDisplay = $box['academy_name'] ?? '';
?>
<div class="mirror-box" data-box-id="<?= $boxId ?>" onclick="openBoxModal(<?= $boxId ?>)"
style="border:2px solid <?= $isFull ? '#EF4444' : ($traineeCount > 0 ? '#0D7377' : '#E5E7EB') ?>;
border-radius:12px;padding:12px;min-height:140px;cursor:pointer;
background:<?= $isFull ? '#FEF2F2' : ($traineeCount > 0 ? '#F0FDFA' : 'white') ?>;
transition:all 0.2s;position:relative;">
<!-- Box label/position -->
<div style="position:absolute;top:4px;left:4px;font-size:10px;color:#9CA3AF;direction:ltr;">
<?= $r + 1 ?>,<?= $c + 1 ?>
</div>
<div style="width:44px;height:44px;border-radius:12px;background:<?= $cfg['color'] ?>20;display:flex;align-items:center;justify-content:center;">
<i data-lucide="<?= $cfg['icon'] ?>" style="width:22px;height:22px;color:<?= $cfg['color'] ?>;"></i>
</div>
</div>
<!-- Status Badge -->
<div style="margin-bottom:12px;">
<span style="padding:5px 14px;border-radius:20px;font-size:14px;font-weight:700;background:<?= $cfg['color'] ?>20;color:<?= $cfg['color'] ?>;">
<?= $cfg['label'] ?>
</span>
</div>
<?php if ($box && ($coachDisplay || $academyDisplay || $traineeCount > 0)): ?>
<!-- Coach/Academy -->
<?php if ($coachDisplay): ?>
<div style="font-size:11px;font-weight:600;color:#0D7377;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="<?= e($coachDisplay) ?>">
<i data-lucide="user" style="width:11px;height:11px;vertical-align:middle;margin-left:2px;"></i>
<?= e($coachDisplay) ?>
</div>
<?php endif; ?>
<?php if ($academyDisplay): ?>
<div style="font-size:10px;color:#6B7280;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
<?= e($academyDisplay) ?>
</div>
<?php endif; ?>
<!-- Current/Next Info -->
<?php if ($s['current_booking']): ?>
<div style="padding:10px;background:white;border-radius:8px;margin-bottom:8px;font-size:13px;">
<div style="color:#6B7280;font-size:11px;margin-bottom:2px;">الآن:</div>
<div style="color:#1A1A2E;font-weight:600;"><?= e($s['current_booking']['purpose'] ?: 'حجز') ?></div>
<div style="color:#6B7280;direction:ltr;text-align:right;font-size:12px;"><?= e($s['current_booking']['start_time']) ?><?= e($s['current_booking']['end_time']) ?></div>
</div>
<?php endif; ?>
<!-- Age + Gender -->
<?php if (!empty($box['age_group']) || $gender !== 'mixed'): ?>
<div style="display:flex;gap:4px;margin-bottom:6px;flex-wrap:wrap;">
<?php if (!empty($box['age_group'])): ?>
<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:#F3F4F6;color:#374151;"><?= e($box['age_group']) ?></span>
<?php endif; ?>
<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:<?= $gc ?>15;color:<?= $gc ?>;"><?= $genderLabels[$gender] ?? '' ?></span>
</div>
<?php endif; ?>
<?php if ($s['next_booking']): ?>
<div style="padding:8px 10px;background:white;border-radius:8px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="color:#6B7280;">التالي: <?= e($s['next_booking']['purpose'] ?: 'حجز') ?></span>
<?php if ($s['time_until_next']): ?>
<span style="font-weight:600;color:#D97706;">بعد <?= e($s['time_until_next']) ?></span>
<!-- Trainees -->
<div style="display:flex;flex-direction:column;gap:2px;">
<?php if (!empty($box['trainees'])):
foreach ($box['trainees'] as $t):
$tName = $t['player_full_name'] ?? $t['trainee_name'] ?? '';
?>
<div style="font-size:10px;color:#374151;background:white;border:1px solid #E5E7EB;border-radius:4px;padding:2px 5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="<?= e($tName) ?>">
<?= e($tName) ?>
</div>
<?php endforeach; endif; ?>
</div>
<!-- Count -->
<div style="position:absolute;bottom:4px;left:6px;font-size:10px;font-weight:700;color:<?= $isFull ? '#DC2626' : '#059669' ?>;direction:ltr;">
<?= $traineeCount ?>/<?= $maxTrainees ?>
</div>
<?php else: ?>
<div style="display:flex;align-items:center;justify-content:center;height:100%;opacity:0.4;">
<i data-lucide="plus" style="width:24px;height:24px;color:#9CA3AF;"></i>
</div>
<?php endif; ?>
</div>
<?php elseif ($s['current_status'] === 'available'): ?>
<div style="padding:8px 10px;background:white;border-radius:8px;font-size:13px;text-align:center;color:#059669;font-weight:600;">
لا يوجد حجوزات قادمة
<?php endfor; ?>
<?php endfor; ?>
</div>
<!-- Box Edit Modal -->
<div id="boxModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;align-items:center;justify-content:center;">
<div style="background:white;border-radius:16px;width:500px;max-width:95%;max-height:90vh;overflow-y:auto;box-shadow:0 25px 50px rgba(0,0,0,0.25);">
<div style="padding:20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;font-size:16px;color:#0D7377;">تعديل الخانة</h3>
<button onclick="closeBoxModal()" style="background:none;border:none;cursor:pointer;padding:5px;">
<i data-lucide="x" style="width:20px;height:20px;color:#6B7280;"></i>
</button>
</div>
<?php endif; ?>
<!-- Footer -->
<div style="margin-top:12px;display:flex;justify-content:space-between;font-size:11px;color:#9CA3AF;">
<span>حجوزات اليوم: <?= $s['today_count'] ?></span>
<?php if ($s['capacity']): ?>
<span>سعة: <?= $s['capacity'] ?></span>
<?php endif; ?>
<!-- Box Settings Form -->
<form id="boxForm" method="POST" action="">
<?= csrf_field() ?>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">المدرب</label>
<select name="coach_id" class="form-select" id="modalCoachId">
<option value="0">— بدون —</option>
<?php foreach ($coaches as $coach): ?>
<option value="<?= (int) $coach['id'] ?>"><?= e($coach['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">أو اسم يدوي</label>
<input type="text" name="coach_name" id="modalCoachName" class="form-input" placeholder="اسم المدرب">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الأكاديمية</label>
<select name="academy_id" class="form-select" id="modalAcademyId">
<option value="0">— بدون —</option>
<?php foreach ($academies as $ac): ?>
<option value="<?= (int) $ac['id'] ?>"><?= e($ac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الفئة العمرية</label>
<input type="text" name="age_group" id="modalAgeGroup" class="form-input" placeholder="مثال: 4-6" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الجنس</label>
<select name="gender" class="form-select" id="modalGender">
<option value="mixed">مختلط</option>
<option value="male">أولاد</option>
<option value="female">بنات</option>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">أقصى عدد متدربين</label>
<input type="number" name="max_trainees" id="modalMaxTrainees" class="form-input" value="5" min="1" max="10" style="direction:ltr;text-align:center;">
</div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;">
<i data-lucide="save" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ إعدادات الخانة
</button>
</div>
</form>
<div style="border-top:1px solid #E5E7EB;padding:20px;">
<h4 style="margin:0 0 10px;font-size:14px;color:#374151;">إضافة متدرب</h4>
<form id="traineeForm" method="POST" action="" style="display:flex;gap:8px;flex-wrap:wrap;">
<?= csrf_field() ?>
<select name="player_id" class="form-select" style="flex:1;min-width:150px;">
<option value="0">— اختر لاعب —</option>
<?php foreach ($players as $p): ?>
<option value="<?= (int) $p['id'] ?>"><?= e($p['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
<input type="text" name="trainee_name" class="form-input" placeholder="أو اكتب اسم يدوي" style="flex:1;min-width:150px;">
<button type="submit" class="btn btn-primary" style="white-space:nowrap;">
<i data-lucide="user-plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إضافة
</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($states)): ?>
<div style="padding:120px 30px;text-align:center;">
<i data-lucide="monitor-off" style="width:64px;height:64px;color:#D1D5DB;margin-bottom:20px;"></i>
<h2 style="color:#6B7280;">لا توجد مرافق نشطة</h2>
<!-- Current Trainees in Box -->
<div id="modalTrainees" style="border-top:1px solid #E5E7EB;padding:15px 20px;display:none;">
<h4 style="margin:0 0 10px;font-size:14px;color:#374151;">المتدربون الحاليون</h4>
<div id="traineeList"></div>
</div>
</div>
</div>
<?php endif; ?>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
body { overflow-x: hidden; }
.mirror-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.1); }
.mirror-box:hover { transform:scale(1.02); box-shadow:0 4px 12px rgba(0,0,0,0.08); }
</style>
<script>
var gridId = <?= $gridId ?>;
var boxesData = <?= json_encode($grid['boxes'], JSON_UNESCAPED_UNICODE) ?>;
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
updateClock();
setInterval(updateClock, 1000);
setInterval(refreshMirror, 30000);
});
function updateClock() {
var now = new Date();
var h = String(now.getHours()).padStart(2, '0');
var m = String(now.getMinutes()).padStart(2, '0');
var s = String(now.getSeconds()).padStart(2, '0');
document.getElementById('liveClock').textContent = h + ':' + m + ':' + s;
}
function openBoxModal(boxId) {
if (!boxId) return;
var box = boxesData.find(b => parseInt(b.id) === boxId);
if (!box) return;
function refreshMirror() {
var type = document.getElementById('filterType').value;
var url = '/api/mirror/state?type=' + encodeURIComponent(type);
fetch(url)
.then(r => r.json())
.then(data => {
document.getElementById('lastUpdate').textContent = 'آخر تحديث: ' + data.time;
updateCards(data.states);
})
.catch(() => {
document.getElementById('syncDot').style.background = '#EF4444';
document.getElementById('boxForm').action = '/mirror/' + gridId + '/box/' + boxId;
document.getElementById('traineeForm').action = '/mirror/' + gridId + '/box/' + boxId + '/trainee';
document.getElementById('modalCoachId').value = box.coach_id || '0';
document.getElementById('modalCoachName').value = box.coach_name || '';
document.getElementById('modalAcademyId').value = box.academy_id || '0';
document.getElementById('modalAgeGroup').value = box.age_group || '';
document.getElementById('modalGender').value = box.gender || 'mixed';
document.getElementById('modalMaxTrainees').value = box.max_trainees || 5;
// Render trainees
var traineeDiv = document.getElementById('modalTrainees');
var traineeList = document.getElementById('traineeList');
traineeList.innerHTML = '';
if (box.trainees && box.trainees.length > 0) {
traineeDiv.style.display = 'block';
box.trainees.forEach(function(t) {
var name = t.player_full_name || t.trainee_name || '';
var row = document.createElement('div');
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:6px 8px;background:#F9FAFB;border-radius:6px;margin-bottom:4px;font-size:13px;';
row.innerHTML = '<span>' + escapeHtml(name) + '</span>' +
'<div style="display:flex;gap:4px;">' +
'<form method="POST" action="/mirror/' + gridId + '/trainee/' + t.id + '/remove" style="margin:0;">' +
'<input type="hidden" name="_token" value="' + getCsrf() + '">' +
'<button type="submit" style="background:none;border:none;cursor:pointer;color:#DC2626;font-size:11px;padding:2px 6px;" title="إزالة">✕</button>' +
'</form>' +
'</div>';
traineeList.appendChild(row);
});
} else {
traineeDiv.style.display = 'none';
}
document.getElementById('boxModal').style.display = 'flex';
}
function updateCards(states) {
var statusConfig = {
available: {label:'متاح', color:'#10B981', bg:'#ECFDF5'},
booked: {label:'محجوز', color:'#3B82F6', bg:'#EFF6FF'},
in_progress: {label:'جاري', color:'#F59E0B', bg:'#FFFBEB'},
maintenance: {label:'صيانة', color:'#EF4444', bg:'#FEF2F2'},
closed: {label:'مغلق', color:'#6B7280', bg:'#F9FAFB'},
};
states.forEach(function(s) {
var card = document.querySelector('[data-facility-id="' + s.facility_id + '"]');
if (!card) return;
var cfg = statusConfig[s.current_status] || statusConfig.closed;
card.style.borderColor = cfg.color + '40';
card.style.background = cfg.bg;
var stripe = card.querySelector('div');
if (stripe) stripe.style.background = cfg.color;
});
document.getElementById('syncDot').style.background = '#10B981';
function closeBoxModal() {
document.getElementById('boxModal').style.display = 'none';
}
function applyFilters() {
var type = document.getElementById('filterType').value;
window.location.href = '/mirror' + (type ? '?type=' + encodeURIComponent(type) : '');
function getCsrf() {
var el = document.querySelector('input[name="_token"]');
return el ? el.value : '';
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
document.getElementById('boxModal').addEventListener('click', function(e) {
if (e.target === this) closeBoxModal();
});
</script>
<?php $__template->endSection(); ?>
<?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
public function addMedical(Request $request, string $id): Response
{
$data = [
'record_type' => trim((string) $request->post('record_type', '')),
'exam_date' => trim((string) $request->post('exam_date', '')),
'expiry_date' => trim((string) $request->post('expiry_date', '')) ?: null,
'doctor_name' => trim((string) $request->post('doctor_name', '')) ?: null,
'clinic_name' => trim((string) $request->post('clinic_name', '')) ?: null,
'result' => trim((string) $request->post('result', '')),
'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,
'record_type' => trim((string) $request->post('record_type', '')),
'certificate_type' => trim((string) $request->post('certificate_type', 'recreational')),
'exam_date' => trim((string) $request->post('exam_date', '')),
'expiry_date' => trim((string) $request->post('expiry_date', '')) ?: null,
'doctor_name' => trim((string) $request->post('doctor_name', '')) ?: null,
'clinic_name' => trim((string) $request->post('clinic_name', '')) ?: null,
'issuing_authority' => trim((string) $request->post('issuing_authority', '')) ?: null,
'cert_number' => trim((string) $request->post('cert_number', '')) ?: 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 = [];
if (!array_key_exists($data['record_type'], PlayerMedicalRecord::getRecordTypes())) {
$errors[] = 'نوع السجل الطبي غير صالح';
}
if (!array_key_exists($data['certificate_type'], PlayerMedicalRecord::getCertificateTypes())) {
$errors[] = 'نوع الشهادة غير صالح';
}
if (empty($data['exam_date'])) {
$errors[] = 'تاريخ الفحص مطلوب';
}
if (!empty($data['result']) && !array_key_exists($data['result'], PlayerMedicalRecord::getResults())) {
$errors[] = 'نتيجة الفحص غير صالحة';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
......@@ -355,7 +356,7 @@ class PlayerController extends Controller
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
protected static array $fillable = [
'player_id',
'record_type',
'certificate_type',
'validity_months',
'exam_date',
'expiry_date',
'doctor_name',
'clinic_name',
'issuing_authority',
'cert_number',
'result',
'approval_status',
'approved_by',
'approved_at',
'rejection_reason',
'restrictions',
'document_id',
'notes',
......@@ -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.
*/
public static function getResults(): array
{
return [
'pending' => 'في الانتظار',
'fit' => 'لائق',
'conditional' => 'لائق بشروط',
'unfit' => 'غير لائق',
......
......@@ -19,6 +19,11 @@ return [
['POST', '/api/players/parse-nid', 'PlayerAffairs\Controllers\PlayerController@parseNid', ['auth'], 'player.register'],
['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
['GET', '/attendance', 'PlayerAffairs\Controllers\AttendanceController@index', ['auth'], 'player.view'],
['POST', '/attendance/record', 'PlayerAffairs\Controllers\AttendanceController@record', ['auth', 'csrf'], 'player.edit'],
......
......@@ -10,9 +10,15 @@ use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord;
final class MedicalRecordService
{
/**
* Add a medical record for a player and update their medical status.
*/
public static function getCertificateTypes(): array
{
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
{
$player = Player::find($playerId);
......@@ -22,43 +28,107 @@ final class MedicalRecordService
$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([
'player_id' => $playerId,
'record_type' => $data['record_type'] ?? 'medical_exam',
'exam_date' => $data['exam_date'] ?? date('Y-m-d'),
'expiry_date' => $data['expiry_date'] ?? null,
'doctor_name' => $data['doctor_name'] ?? null,
'clinic_name' => $data['clinic_name'] ?? null,
'result' => $data['result'] ?? 'pending',
'restrictions' => $data['restrictions'] ?? null,
'document_id' => $data['document_id'] ?? null,
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'player_id' => $playerId,
'record_type' => $data['record_type'] ?? 'fitness_cert',
'certificate_type' => $certType,
'validity_months' => $validityMonths,
'exam_date' => $examDate,
'expiry_date' => $expiryDate,
'doctor_name' => $data['doctor_name'] ?? null,
'clinic_name' => $data['clinic_name'] ?? null,
'issuing_authority' => $data['issuing_authority'] ?? null,
'cert_number' => $data['cert_number'] ?? 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
$updateData = [];
if (!empty($data['result'])) {
$updateData['medical_status'] = $data['result'];
return $record;
}
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', [
'player_id' => $playerId,
'record_id' => $record->id,
$db->update('player_medical_records', [
'approval_status' => 'rejected',
'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
{
$player = Player::find($playerId);
......@@ -78,9 +148,6 @@ final class MedicalRecordService
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
{
$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);
$typeColor = $playerType === 'member' ? '#0284C7' : '#7C3AED';
$age = $player->getAge();
$recordTypes = PlayerMedicalRecord::getRecordTypes();
$certTypes = PlayerMedicalRecord::getCertificateTypes();
$approvalStatuses = PlayerMedicalRecord::getApprovalStatuses();
$resultTypes = PlayerMedicalRecord::getResults();
$enrollmentStatuses = AcademyEnrollment::getStatuses();
?>
......@@ -394,9 +396,9 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<!-- Add Medical Record Inline Form (hidden by default) -->
<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() ?>
<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;">
<label class="form-label" style="font-size:12px;">نوع السجل <span style="color:#DC2626;">*</span></label>
<select name="record_type" class="form-input" required>
......@@ -406,6 +408,14 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<?php endforeach; ?>
</select>
</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;">
<label class="form-label" style="font-size:12px;">تاريخ الفحص <span style="color:#DC2626;">*</span></label>
<input type="date" name="exam_date" class="form-input" required>
......@@ -413,9 +423,10 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">تاريخ الانتهاء</label>
<input type="date" name="expiry_date" class="form-input">
<span style="font-size:10px;color:#9CA3AF;">يحسب تلقائياً إذا تُرك فارغاً</span>
</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;">
<label class="form-label" style="font-size:12px;">اسم الطبيب</label>
<input type="text" name="doctor_name" class="form-input" placeholder="اسم الطبيب">
......@@ -425,13 +436,12 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<input type="text" name="clinic_name" class="form-input" placeholder="اسم المكان">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">النتيجة <span style="color:#DC2626;">*</span></label>
<select name="result" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($resultTypes as $resKey => $resLabel): ?>
<option value="<?= e($resKey) ?>"><?= e($resLabel) ?></option>
<?php endforeach; ?>
</select>
<label class="form-label" style="font-size:12px;">جهة الإصدار</label>
<input type="text" name="issuing_authority" class="form-input" placeholder="جهة إصدار الشهادة">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">رقم الشهادة</label>
<input type="text" name="cert_number" class="form-input" placeholder="رقم الشهادة" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:15px;">
......@@ -444,9 +454,12 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea>
</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;">
<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 type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleMedicalForm()">إلغاء</button>
</div>
......@@ -459,26 +472,31 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<thead>
<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>
</tr>
</thead>
<tbody>
<?php foreach ($medicalRecords as $rec):
$recType = $rec['record_type'] ?? '';
$recResult = $rec['result'] ?? '';
$resultLabel = $resultTypes[$recResult] ?? $recResult;
$resultColorMap = ['fit' => '#059669', 'conditional' => '#D97706', 'unfit' => '#DC2626'];
$resultColor = $resultColorMap[$recResult] ?? '#6B7280';
$recCertType = $rec['certificate_type'] ?? 'recreational';
$recApproval = $rec['approval_status'] ?? 'pending';
$approvalColorMap = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' => '#DC2626'];
$approvalColor = $approvalColorMap[$recApproval] ?? '#6B7280';
?>
<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;"><?= e($certTypes[$recCertType] ?? $recCertType) ?></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;">
<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 style="padding:12px 15px;"><?= e($rec['doctor_name'] ?? '—') ?></td>
</tr>
......
......@@ -11,6 +11,7 @@ PermissionRegistry::register('player_affairs', [
'player.manage_card' => ['ar' => 'إدارة كارنيه اللاعب', 'en' => 'Manage Player Card'],
'player.view_medical' => ['ar' => 'عرض السجل الطبي', 'en' => 'View 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'],
]);
......
<?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;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Reservations\Models\Reservation;
use App\Modules\Facilities\Models\Facility;
use App\Modules\Rules\Services\RuleEngine;
class ReservationService
......@@ -49,10 +48,14 @@ class ReservationService
// Calculate unit rate and total amount if facility is set
if (!empty($data['facility_id']) && empty($data['unit_rate'])) {
$facility = Facility::find((int) $data['facility_id']);
if ($facility) {
$isMember = in_array($data['booker_type'] ?? '', ['member', 'player']);
$data['unit_rate'] = $facility->getRate($isMember, $data['time_tier']);
$isMember = in_array($data['booker_type'] ?? '', ['member', 'player']);
$rate = FacilityPricingService::calculateRate(
(int) $data['facility_id'],
$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`",
];
<?php
declare(strict_types=1);
use App\Core\Database;
/**
* تسعير إيجارات الملاعب والصالات الرياضية بنادي شيراتون
* طبقاً للائحة المعتمدة من مجلس الأمناء - أبريل 2025
* يطبق فقط على: الحجز الحر + إدارة النادي للنشاط (غير الأكاديمي)
*/
return function (Database $db): void {
// تأكد من وجود جدول service_catalog
$tableExists = $db->selectOne(
"SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'service_catalog'"
);
if (!$tableExists) {
return;
}
// تسعير إيجار الملاعب للأعضاء (ثانياً من اللائحة)
$memberPricing = [
['facility_type' => 'football_full', 'name_ar' => 'كرة قدم قانوني', 'duration_minutes' => 60, 'price_morning' => 300, 'price_evening' => 500, 'max_players' => 22],
['facility_type' => 'football_five', 'name_ar' => 'كرة قدم خماسي', 'duration_minutes' => 60, 'price_morning' => 150, 'price_evening' => 200, 'max_players' => 12],
['facility_type' => 'tennis_clay', 'name_ar' => 'تنس أرضي', 'duration_minutes' => 60, 'price_morning' => 75, 'price_evening' => 100, 'max_players' => 4],
['facility_type' => 'padel', 'name_ar' => 'بادي تنس', 'duration_minutes' => 60, 'price_morning' => 100, 'price_evening' => 150, 'max_players' => 4],
['facility_type' => 'squash', 'name_ar' => 'الاسكواش', 'duration_minutes' => 60, 'price_morning' => 75, 'price_evening' => 75, 'max_players' => 2],
['facility_type' => 'bowling', 'name_ar' => 'البولينج', 'duration_minutes' => 0, 'price_morning' => 30, 'price_evening' => 30, 'max_players' => 5],
['facility_type' => 'multi_court', 'name_ar' => 'ملعب متعدد', 'duration_minutes' => 60, 'price_morning' => 50, 'price_evening' => 100, 'max_players' => 15],
['facility_type' => 'table_tennis', 'name_ar' => 'تنس طاولة', 'duration_minutes' => 60, 'price_morning' => 50, 'price_evening' => 50, 'max_players' => 4],
['facility_type' => 'playstation', 'name_ar' => 'بلاي ستيشن', 'duration_minutes' => 60, 'price_morning' => 60, 'price_evening' => 60, 'max_players' => 4],
['facility_type' => 'billiards', 'name_ar' => 'البلياردو', 'duration_minutes' => 60, 'price_morning' => 60, 'price_evening' => 60, 'max_players' => 4],
['facility_type' => 'gym', 'name_ar' => 'صالة اللياقات', 'duration_minutes' => 60, 'price_morning' => 300, 'price_evening' => 300, 'max_players' => 10],
];
// تسعير إيجار الملاعب لغير الأعضاء (رابعاً من اللائحة - ممارسة)
$nonMemberPricing = [
['facility_type' => 'football_full', 'name_ar' => 'كرة قدم قانوني', 'duration_minutes' => 60, 'price_morning' => 200, 'price_evening' => 300, 'max_players' => 22],
['facility_type' => 'football_five', 'name_ar' => 'كرة قدم خماسي', 'duration_minutes' => 60, 'price_morning' => 150, 'price_evening' => 150, 'max_players' => 12],
['facility_type' => 'tennis_clay', 'name_ar' => 'تنس أرضي', 'duration_minutes' => 60, 'price_morning' => 100, 'price_evening' => 150, 'max_players' => 4],
['facility_type' => 'padel', 'name_ar' => 'بادي تنس', 'duration_minutes' => 60, 'price_morning' => 200, 'price_evening' => 300, 'max_players' => 4],
['facility_type' => 'squash', 'name_ar' => 'الاسكواش', 'duration_minutes' => 60, 'price_morning' => 150, 'price_evening' => 150, 'max_players' => 2],
['facility_type' => 'tennis_table', 'name_ar' => 'تنس طاولة', 'duration_minutes' => 60, 'price_morning' => 60, 'price_evening' => 60, 'max_players' => 4],
['facility_type' => 'bowling', 'name_ar' => 'البولينج', 'duration_minutes' => 0, 'price_morning' => 60, 'price_evening' => 60, 'max_players' => 5],
['facility_type' => 'billiards', 'name_ar' => 'البلياردو', 'duration_minutes' => 60, 'price_morning' => 100, 'price_evening' => 100, 'max_players' => 4],
['facility_type' => 'playstation', 'name_ar' => 'بلاي ستيشن', 'duration_minutes' => 30, 'price_morning' => 60, 'price_evening' => 60, 'max_players' => 4],
];
// تذاكر الدخول لغير الأعضاء (أولاً من اللائحة)
$entryTickets = [
['day_type' => 'weekend', 'age_above_12' => 70, 'age_6_to_12' => 40, 'notes' => 'أيام الجمعة والسبت والعطلات والأعياد الرسمية'],
['day_type' => 'weekday', 'age_above_12' => 50, 'age_6_to_12' => 30, 'notes' => 'من يوم الأحد إلى الخميس ماعدا العطلات والأعياد الرسمية'],
];
// رسوم عامة (من ضوابط مالية)
$generalFees = [
['code' => 'HALL_RENT_SMALL_MEMBER', 'name_ar' => 'إيجار القاعة الصغيرة للعضو', 'amount' => 300, 'unit' => 'per_hour'],
['code' => 'HALL_RENT_SMALL_NON_MEMBER', 'name_ar' => 'إيجار القاعة الصغيرة لغير العضو', 'amount' => 600, 'unit' => 'per_hour'],
['code' => 'HALL_RENT_LARGE_MEMBER', 'name_ar' => 'إيجار القاعة الكبيرة للعضو', 'amount' => 500, 'unit' => 'per_hour'],
['code' => 'HALL_RENT_LARGE_NON_MEMBER', 'name_ar' => 'إيجار القاعة الكبيرة لغير العضو', 'amount' => 1000, 'unit' => 'per_hour'],
['code' => 'CAMERA_ENTRY_MEMBER', 'name_ar' => 'رسم دخول كاميرا فيديو للعضو', 'amount' => 100, 'unit' => 'per_hour'],
['code' => 'CAMERA_ENTRY_NON_MEMBER', 'name_ar' => 'رسم دخول كاميرا فيديو لغير العضو', 'amount' => 200, 'unit' => 'per_hour'],
['code' => 'PHOTO_CAMERA_MEMBER', 'name_ar' => 'رسم دخول كاميرا تصوير للعضو', 'amount' => 50, 'unit' => 'per_hour'],
['code' => 'PHOTO_CAMERA_NON_MEMBER', 'name_ar' => 'رسم دخول كاميرا تصوير لغير العضو', 'amount' => 100, 'unit' => 'per_hour'],
['code' => 'DJ_ENTRY_MEMBER', 'name_ar' => 'رسم دخول دي جي للعضو', 'amount' => 100, 'unit' => 'per_hour'],
['code' => 'DJ_ENTRY_NON_MEMBER', 'name_ar' => 'رسم دخول دي جي لغير العضو', 'amount' => 200, 'unit' => 'per_hour'],
['code' => 'PUPPET_SHOW_MEMBER', 'name_ar' => 'شو عرائس وألعاب أطفال للعضو', 'amount' => 75, 'unit' => 'per_hour'],
['code' => 'PUPPET_SHOW_NON_MEMBER', 'name_ar' => 'شو عرائس وألعاب أطفال لغير العضو', 'amount' => 150, 'unit' => 'per_hour'],
['code' => 'FOOD_CART', 'name_ar' => 'رسم دخول عربة (غزل بنات/مشار/آيس كريم)', 'amount' => 200, 'unit' => 'per_day'],
['code' => 'THEATER_RENT_MEMBER', 'name_ar' => 'إيجار المسرح الرومانى للعضو', 'amount' => 500, 'unit' => 'per_hour'],
['code' => 'THEATER_RENT_NON_MEMBER', 'name_ar' => 'إيجار المسرح الرومانى لغير العضو', 'amount' => 1000, 'unit' => 'per_hour'],
['code' => 'SPECIAL_EVENT', 'name_ar' => 'أحداث ذات طابع خاص (تصوير/برامج تلفزيونية)', 'amount' => 7000, 'unit' => 'per_hour'],
['code' => 'SPECIAL_EVENT_12HR', 'name_ar' => 'أحداث ذات طابع خاص (12 ساعة)', 'amount' => 70000, 'unit' => 'per_12_hours'],
['code' => 'WAITING_AREA_MEMBER', 'name_ar' => 'رسم ساحة الانتظار للعضو (3 ساعات)', 'amount' => 10, 'unit' => 'per_day'],
['code' => 'WAITING_AREA_NON_MEMBER', 'name_ar' => 'رسم ساحة الانتظار لغير العضو (3 ساعات)', 'amount' => 20, 'unit' => 'per_day'],
['code' => 'ACADEMY_CARD_FEE', 'name_ar' => 'بدل فاقد كارنيه الأكاديمية', 'amount' => 50, 'unit' => 'one_time'],
['code' => 'MEMBERSHIP_CARD_FEE', 'name_ar' => 'بدل فاقد كارنيه العضوية', 'amount' => 100, 'unit' => 'one_time'],
['code' => 'PARKING_FEE', 'name_ar' => 'غسيل السيارة', 'amount' => 50, 'unit' => 'per_wash'],
];
// إيجار طاولات بالنادي
$tableRentals = [
['location' => 'inside_main', 'name_ar' => 'داخل المبنى الرئيسي', 'price_day' => 300, 'price_week' => 1500, 'price_month' => 5400],
['location' => 'open_court', 'name_ar' => 'الملاعب المفتوحة', 'price_day' => 300, 'price_week' => 1500, 'price_month' => 5400],
['location' => 'pool_area', 'name_ar' => 'حمام السباحة', 'price_day' => 400, 'price_week' => 2000, 'price_month' => 7200],
];
// حجز اللوكر
$lockerPricing = [
['period' => 'monthly', 'amount' => 150],
['period' => 'quarterly', 'amount' => 450],
['period' => 'yearly', 'amount' => 1000],
];
// تراك الدرجات
$trackPricing = [
['period' => 'monthly', 'amount' => 100],
['period' => 'semi_annual', 'amount' => 300],
['period' => 'yearly', 'amount' => 600],
];
// رسوم حمام السباحة (للمؤسسات)
$poolInstitutional = [
['name_ar' => 'حمام سباحة 25 متر', 'price_morning' => 500, 'price_evening' => 800, 'max_players' => 8],
['name_ar' => 'حمام سباحة 50 متر', 'price_morning' => 800, 'price_evening' => 1000, 'max_players' => 12],
];
// حفظ في system_config كـ JSON
$configs = [
['config_key' => 'sports_pricing_member', 'config_value' => json_encode($memberPricing, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'تسعير الملاعب للأعضاء - لائحة 2025'],
['config_key' => 'sports_pricing_non_member', 'config_value' => json_encode($nonMemberPricing, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'تسعير الملاعب لغير الأعضاء - لائحة 2025'],
['config_key' => 'entry_tickets_non_member', 'config_value' => json_encode($entryTickets, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'تذاكر الدخول لغير الأعضاء'],
['config_key' => 'general_fees', 'config_value' => json_encode($generalFees, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'الرسوم العامة - لائحة 2025'],
['config_key' => 'table_rentals', 'config_value' => json_encode($tableRentals, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'إيجار الطاولات بالنادي'],
['config_key' => 'locker_pricing', 'config_value' => json_encode($lockerPricing, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'حجز اللوكر'],
['config_key' => 'track_pricing', 'config_value' => json_encode($trackPricing, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'تراك الدرجات'],
['config_key' => 'pool_institutional', 'config_value' => json_encode($poolInstitutional, JSON_UNESCAPED_UNICODE), 'category' => 'sports_pricing', 'description' => 'رسوم حمام السباحة للمؤسسات'],
];
foreach ($configs as $config) {
$exists = $db->selectOne(
"SELECT id FROM system_config WHERE config_key = ?",
[$config['config_key']]
);
if ($exists) {
$db->update('system_config', [
'config_value' => $config['config_value'],
'description' => $config['description'],
], 'config_key = ?', [$config['config_key']]);
} else {
$db->insert('system_config', $config);
}
}
};
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