Commit 526babd9 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fsgdrndfh

parent af806c60
...@@ -13,29 +13,21 @@ MenuRegistry::register('sports_activities', [ ...@@ -13,29 +13,21 @@ MenuRegistry::register('sports_activities', [
'label_ar' => 'الأنشطة الرياضية', 'label_ar' => 'الأنشطة الرياضية',
'label_en' => 'Sports Activities', 'label_en' => 'Sports Activities',
'icon' => 'activity', 'icon' => 'activity',
'route' => '/disciplines', 'route' => '/sports-dashboard',
'permission' => 'discipline.view', 'permission' => 'discipline.view',
'parent' => null, 'parent' => null,
'order' => 395, 'order' => 395,
'children' => [ 'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Sports Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 0], ['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 1],
['label_ar' => 'الأنشطة الرياضية', 'label_en' => 'Disciplines', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 1], ['label_ar' => 'الأنشطة والأكاديميات', 'label_en' => 'Disciplines & Academies', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 2],
['label_ar' => 'الملاعب والمرافق', 'label_en' => 'Facilities', 'route' => '/facilities', 'permission' => 'facility.view', 'order' => 2], ['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/coaches', 'permission' => 'coach.view', 'order' => 3],
['label_ar' => 'المراية', 'label_en' => 'Mirror Display', 'route' => '/mirror', 'permission' => 'facility.mirror', 'order' => 2.5], ['label_ar' => 'المجموعات واللاعبين', 'label_en' => 'Groups & Players', 'route' => '/training-groups', 'permission' => 'training_group.view', 'order' => 4],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/academies', 'permission' => 'academy.view', 'order' => 3], ['label_ar' => 'الحصص والحضور', 'label_en' => 'Sessions & Attendance', 'route' => '/sessions', 'permission' => 'session.view', 'order' => 5],
['label_ar' => 'عقود الأكاديميات', 'label_en' => 'Academy Contracts', 'route' => '/academy-contracts', 'permission' => 'academy_contract.view', 'order' => 3.5], ['label_ar' => 'المرافق والشبكات', 'label_en' => 'Facilities & Grids', 'route' => '/facility-grids', 'permission' => 'facility_grid.view', 'order' => 6],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/coaches', 'permission' => 'coach.view', 'order' => 4], ['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 7],
['label_ar' => 'المجموعات التدريبية','label_en' => 'Training Groups', 'route' => '/training-groups', 'permission' => 'training_group.view', 'order' => 4.2], ['label_ar' => 'التأجير المؤسسي', 'label_en' => 'Corporate Rentals', 'route' => '/rentals', 'permission' => 'rental.view', 'order' => 8],
['label_ar' => 'شئون اللاعبين', 'label_en' => 'Players', 'route' => '/players', 'permission' => 'player.view', 'order' => 4.5], ['label_ar' => 'الاشتراكات والتسعير', 'label_en' => 'Subscriptions & Pricing', 'route' => '/activity-subscriptions', 'permission' => 'activity_sub.view', 'order' => 9],
['label_ar' => 'اعتماد السجلات الطبية','label_en' => 'Medical Approvals', 'route' => '/medical-approvals', 'permission' => 'player.approve_medical', 'order' => 4.6], ['label_ar' => 'العقود والتسويات', 'label_en' => 'Contracts & Settlements', 'route' => '/academy-contracts', 'permission' => 'academy_contract.view', 'order' => 10],
['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],
['label_ar' => 'حمام السباحة', 'label_en' => 'Pool Management', 'route' => '/pool', 'permission' => 'pool.view', 'order' => 6.5],
['label_ar' => 'التأجير المؤسسي', 'label_en' => 'Corporate Rentals', 'route' => '/rentals', 'permission' => 'rental.view', 'order' => 7],
['label_ar' => 'اشتراكات الأنشطة', 'label_en' => 'Activity Subscriptions','route' => '/activity-subscriptions', 'permission' => 'activity_sub.view', 'order' => 8],
['label_ar' => 'تسعير الأنشطة', 'label_en' => 'Activity Pricing', 'route' => '/activity-subscriptions/pricing', 'permission' => 'activity_sub.manage_pricing', 'order' => 9],
['label_ar' => 'التسويات المالية', 'label_en' => 'Settlements', 'route' => '/academy-contracts/settlements', 'permission' => 'academy_contract.view', 'order' => 10],
], ],
]); ]);
......
<?php <?php
declare(strict_types=1); declare(strict_types=1);
return [ // Mirror routes have been migrated to the unified FacilityGrids module.
// Mirror Grid System // Old /mirror/* paths now redirect to /facility-grids/* via FacilityGrids\Controllers\RedirectController.
['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 return [
// Facility Dashboard (per-facility stats page — still relevant)
['GET', '/facilities/{id:\d+}/dashboard', 'FacilityDashboards\Controllers\FacilityDashboardController@show', ['auth'], 'facility.dashboard'], ['GET', '/facilities/{id:\d+}/dashboard', 'FacilityDashboards\Controllers\FacilityDashboardController@show', ['auth'], 'facility.dashboard'],
]; ];
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\FacilityGrids\Models\FacilityGrid;
use App\Modules\FacilityGrids\Services\GridStateService;
class FacilityGridController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('facility_grid.view');
$grids = FacilityGrid::allActive();
return $this->view('FacilityGrids.Views.index', ['grids' => $grids]);
}
public function create(Request $request): Response
{
$this->authorize('facility_grid.manage');
$gridTypes = FacilityGrid::getGridTypes();
$facilities = $this->getFacilities();
return $this->view('FacilityGrids.Views.create', [
'gridTypes' => $gridTypes,
'facilities' => $facilities,
]);
}
public function store(Request $request): Response
{
$this->authorize('facility_grid.manage');
$rules = [
'facility_id' => 'required|numeric',
'name_ar' => 'required|string|max:200',
'grid_type' => 'required|in:pool,court,generic',
'rows_count' => 'required|numeric|min:1|max:50',
'cols_count' => 'required|numeric|min:1|max:50',
];
if (!$this->validate($_POST, $rules)) {
return $this->redirect('/facility-grids/create');
}
$grid = FacilityGrid::create([
'facility_id' => (int) $_POST['facility_id'],
'name_ar' => $_POST['name_ar'],
'name_en' => $_POST['name_en'] ?? null,
'grid_type' => $_POST['grid_type'],
'rows_count' => (int) $_POST['rows_count'],
'cols_count' => (int) $_POST['cols_count'],
'row_label_ar' => $_POST['row_label_ar'] ?? 'حارة',
'col_label_ar' => $_POST['col_label_ar'] ?? 'قسم',
'physical_length'=> !empty($_POST['physical_length']) ? (float) $_POST['physical_length'] : null,
'physical_width' => !empty($_POST['physical_width']) ? (float) $_POST['physical_width'] : null,
'is_active' => 1,
]);
$this->generateZones((int) $grid->id, (int) $_POST['rows_count'], (int) $_POST['cols_count']);
return $this->redirect('/facility-grids/' . $grid->id)->withSuccess('تم إنشاء الشبكة بنجاح');
}
public function show(Request $request, int $id): Response
{
$this->authorize('facility_grid.view');
$grid = FacilityGrid::find($id);
if (!$grid) return $this->redirect('/facility-grids')->withError('الشبكة غير موجودة');
$date = $_GET['date'] ?? date('Y-m-d');
$time = $_GET['time'] ?? null;
$planMonth = $_GET['month'] ?? substr($date, 0, 7);
$state = GridStateService::getState($id, $date, $time);
return $this->view('FacilityGrids.Views.show', [
'grid' => $grid,
'state' => $state,
'date' => $date,
'planMonth' => $planMonth,
]);
}
public function edit(Request $request, int $id): Response
{
$this->authorize('facility_grid.manage');
$grid = FacilityGrid::find($id);
if (!$grid) return $this->redirect('/facility-grids')->withError('الشبكة غير موجودة');
return $this->view('FacilityGrids.Views.edit', [
'grid' => $grid,
'gridTypes' => FacilityGrid::getGridTypes(),
'facilities' => $this->getFacilities(),
]);
}
public function update(Request $request, int $id): Response
{
$this->authorize('facility_grid.manage');
$grid = FacilityGrid::find($id);
if (!$grid) return $this->redirect('/facility-grids')->withError('الشبكة غير موجودة');
$rules = [
'name_ar' => 'required|string|max:200',
'grid_type' => 'required|in:pool,court,generic',
'rows_count' => 'required|numeric|min:1|max:50',
'cols_count' => 'required|numeric|min:1|max:50',
];
if (!$this->validate($_POST, $rules)) {
return $this->redirect('/facility-grids/' . $id . '/edit');
}
$grid->update([
'name_ar' => $_POST['name_ar'],
'name_en' => $_POST['name_en'] ?? null,
'grid_type' => $_POST['grid_type'],
'rows_count' => (int) $_POST['rows_count'],
'cols_count' => (int) $_POST['cols_count'],
'row_label_ar' => $_POST['row_label_ar'] ?? 'حارة',
'col_label_ar' => $_POST['col_label_ar'] ?? 'قسم',
'physical_length'=> !empty($_POST['physical_length']) ? (float) $_POST['physical_length'] : null,
'physical_width' => !empty($_POST['physical_width']) ? (float) $_POST['physical_width'] : null,
]);
$this->regenerateZones($id, (int) $_POST['rows_count'], (int) $_POST['cols_count']);
return $this->redirect('/facility-grids/' . $id)->withSuccess('تم تحديث الشبكة');
}
public function apiState(Request $request, int $id): Response
{
$date = $_GET['date'] ?? date('Y-m-d');
$time = $_GET['time'] ?? null;
$state = GridStateService::getState($id, $date, $time);
return $this->json(['success' => true, 'data' => $state]);
}
public function toggle(Request $request, int $id): Response
{
$this->authorize('facility_grid.manage');
$grid = FacilityGrid::find($id);
if (!$grid) return $this->json(['success' => false, 'message' => 'غير موجود']);
$grid->update(['is_active' => $grid->is_active ? 0 : 1]);
return $this->redirect('/facility-grids')->withSuccess('تم تحديث الحالة');
}
private function generateZones(int $gridId, int $rows, int $cols): void
{
$db = \App\Core\App::getInstance()->db();
for ($r = 0; $r < $rows; $r++) {
for ($c = 0; $c < $cols; $c++) {
$db->insert('facility_grid_zones', [
'grid_id' => $gridId,
'row_index' => $r,
'col_index' => $c,
'max_occupants' => 8,
'is_active' => 1,
]);
}
}
}
private function regenerateZones(int $gridId, int $rows, int $cols): void
{
$db = \App\Core\App::getInstance()->db();
$existing = $db->select(
"SELECT row_index, col_index FROM facility_grid_zones WHERE grid_id = ?",
[$gridId]
);
$existingMap = [];
foreach ($existing as $z) {
$existingMap[$z['row_index'] . ':' . $z['col_index']] = true;
}
for ($r = 0; $r < $rows; $r++) {
for ($c = 0; $c < $cols; $c++) {
$key = $r . ':' . $c;
if (!isset($existingMap[$key])) {
$db->insert('facility_grid_zones', [
'grid_id' => $gridId,
'row_index' => $r,
'col_index' => $c,
'max_occupants' => 8,
'is_active' => 1,
]);
}
}
}
$db->query(
"UPDATE facility_grid_zones SET is_active = 0 WHERE grid_id = ? AND (row_index >= ? OR col_index >= ?)",
[$gridId, $rows, $cols]
);
}
private function getFacilities(): array
{
$db = \App\Core\App::getInstance()->db();
return $db->select("SELECT id, name_ar FROM facilities WHERE is_active = 1 ORDER BY name_ar");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\FacilityGrids\Models\FacilityGrid;
class GridDashboardController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('facility_grid.view');
$db = App::getInstance()->db();
$gridId = (int) ($_GET['grid_id'] ?? 0);
$filter = $_GET['filter'] ?? 'day';
$from = $_GET['from'] ?? date('Y-m-d');
$to = $_GET['to'] ?? date('Y-m-d');
switch ($filter) {
case 'week':
$from = date('Y-m-d', strtotime('monday this week'));
$to = date('Y-m-d', strtotime('sunday this week'));
break;
case 'month':
$from = date('Y-m-01');
$to = date('Y-m-t');
break;
case 'year':
$from = date('Y-01-01');
$to = date('Y-12-31');
break;
case 'custom':
$from = $_GET['from'] ?? date('Y-m-d');
$to = $_GET['to'] ?? date('Y-m-d');
break;
}
$grids = FacilityGrid::allActive();
$stats = $this->getStats($db, $gridId ?: null, $from, $to);
$schedulesByDay = $this->getSchedulesByDay($db, $gridId ?: null, $from, $to);
$occupancyByHour = $this->getOccupancyByHour($db, $gridId ?: null, $from, $to);
$topCoaches = $this->getTopCoaches($db, $gridId ?: null, $from, $to);
return $this->view('FacilityGrids.Views.dashboard', [
'grids' => $grids,
'selectedGrid' => $gridId,
'filter' => $filter,
'from' => $from,
'to' => $to,
'stats' => $stats,
'schedulesByDay' => $schedulesByDay,
'occupancyByHour' => $occupancyByHour,
'topCoaches' => $topCoaches,
]);
}
private function getStats($db, ?int $gridId, string $from, string $to): array
{
$where = "fzs.is_active = 1 AND fzs.effective_from <= ? AND (fzs.effective_to IS NULL OR fzs.effective_to >= ?)";
$params = [$to, $from];
if ($gridId) {
$where .= " AND fzs.grid_id = ?";
$params[] = $gridId;
}
$totalSchedules = $db->selectOne(
"SELECT COUNT(*) as cnt FROM facility_zone_schedules fzs WHERE $where",
$params
);
$totalTrainees = $db->selectOne(
"SELECT COUNT(*) as cnt FROM facility_zone_trainees fzt
JOIN facility_grid_zones fgz ON fgz.id = fzt.zone_id" .
($gridId ? " WHERE fgz.grid_id = ?" : ""),
$gridId ? [$gridId] : []
);
$totalZones = $db->selectOne(
"SELECT COUNT(*) as cnt FROM facility_grid_zones WHERE is_active = 1" .
($gridId ? " AND grid_id = ?" : ""),
$gridId ? [$gridId] : []
);
$occupiedZones = $db->selectOne(
"SELECT COUNT(DISTINCT CONCAT(fzs.grid_id, ':', fzs.zone_selection_json)) as cnt
FROM facility_zone_schedules fzs WHERE $where",
$params
);
return [
'total_schedules' => (int) $totalSchedules['cnt'],
'total_trainees' => (int) $totalTrainees['cnt'],
'total_zones' => (int) $totalZones['cnt'],
'occupied_zones' => (int) $occupiedZones['cnt'],
];
}
private function getSchedulesByDay($db, ?int $gridId, string $from, string $to): array
{
$where = "is_active = 1 AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)";
$params = [$to, $from];
if ($gridId) {
$where .= " AND grid_id = ?";
$params[] = $gridId;
}
return $db->select(
"SELECT day_of_week, COUNT(*) as cnt FROM facility_zone_schedules WHERE $where GROUP BY day_of_week ORDER BY day_of_week",
$params
);
}
private function getOccupancyByHour($db, ?int $gridId, string $from, string $to): array
{
$where = "is_active = 1 AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)";
$params = [$to, $from];
if ($gridId) {
$where .= " AND grid_id = ?";
$params[] = $gridId;
}
return $db->select(
"SELECT HOUR(start_time) as hour, COUNT(*) as cnt FROM facility_zone_schedules WHERE $where GROUP BY HOUR(start_time) ORDER BY hour",
$params
);
}
private function getTopCoaches($db, ?int $gridId, string $from, string $to): array
{
$where = "is_active = 1 AND coach_name IS NOT NULL AND coach_name != '' AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)";
$params = [$to, $from];
if ($gridId) {
$where .= " AND grid_id = ?";
$params[] = $gridId;
}
return $db->select(
"SELECT coach_name, COUNT(*) as sessions FROM facility_zone_schedules WHERE $where GROUP BY coach_name ORDER BY sessions DESC LIMIT 10",
$params
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
class RedirectController extends Controller
{
public function mirrorIndex(Request $request): Response
{
return $this->redirect('/facility-grids');
}
public function mirrorShow(Request $request, int $id): Response
{
return $this->redirect('/facility-grids/' . $id);
}
public function poolIndex(Request $request): Response
{
return $this->redirect('/facility-grids');
}
public function poolGrid(Request $request, int $id): Response
{
return $this->redirect('/facility-grids/' . $id);
}
public function poolSchedules(Request $request, int $id): Response
{
return $this->redirect('/facility-grids/' . $id . '/schedules');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\FacilityGrids\Models\FacilityGrid;
use App\Modules\FacilityGrids\Models\FacilityZoneSchedule;
use App\Modules\FacilityGrids\Services\GridStateService;
class ZoneScheduleController extends Controller
{
public function index(Request $request, int $gridId): Response
{
$this->authorize('facility_grid.manage');
$grid = FacilityGrid::find($gridId);
if (!$grid) return $this->redirect('/facility-grids')->withError('الشبكة غير موجودة');
$schedules = FacilityZoneSchedule::getAllForGrid($gridId);
$dayNames = FacilityZoneSchedule::getDayNames();
return $this->view('FacilityGrids.Views.schedules', [
'grid' => $grid,
'schedules' => $schedules,
'dayNames' => $dayNames,
]);
}
public function store(Request $request, int $gridId): Response
{
$this->authorize('facility_grid.manage');
$grid = FacilityGrid::find($gridId);
if (!$grid) return $this->json(['success' => false, 'message' => 'شبكة غير موجودة']);
$rules = [
'schedule_name' => 'required|string|max:255',
'zone_selection_type' => 'required|in:cells,row,column,all',
'day_of_week' => 'required|numeric|min:0|max:6',
'start_time' => 'required',
'end_time' => 'required',
'effective_from' => 'required|date',
];
if (!$this->validate($_POST, $rules)) {
return $this->json(['success' => false, 'message' => 'بيانات غير صالحة']);
}
$selectionType = $_POST['zone_selection_type'];
$selectionJson = $_POST['zone_selection_json'] ?? '[]';
$positions = GridStateService::resolvePositions([
'zone_selection_type' => $selectionType,
'zone_selection_json' => $selectionJson,
], (int) $grid->rows_count, (int) $grid->cols_count);
$conflicts = GridStateService::checkConflicts(
$gridId,
$positions,
(int) $_POST['day_of_week'],
$_POST['start_time'],
$_POST['end_time'],
$_POST['effective_from'],
$_POST['effective_to'] ?? null
);
if (!empty($conflicts)) {
return $this->json([
'success' => false,
'message' => 'يوجد تعارض مع جداول أخرى',
'conflicts' => $conflicts,
]);
}
$maxOccupants = FacilityZoneSchedule::getMaxOccupants($selectionType);
$planMonth = $_POST['plan_month'] ?? date('Y-m');
$schedule = FacilityZoneSchedule::create([
'grid_id' => $gridId,
'plan_month' => $planMonth,
'schedule_name' => $_POST['schedule_name'],
'zone_selection_type' => $selectionType,
'zone_selection_json' => $selectionJson,
'day_of_week' => (int) $_POST['day_of_week'],
'start_time' => $_POST['start_time'],
'end_time' => $_POST['end_time'],
'activity_type' => $_POST['activity_type'] ?? null,
'coach_id' => !empty($_POST['coach_id']) ? (int) $_POST['coach_id'] : null,
'coach_name' => $_POST['coach_name'] ?? null,
'academy_id' => !empty($_POST['academy_id']) ? (int) $_POST['academy_id'] : null,
'academy_name' => $_POST['academy_name'] ?? null,
'age_group' => $_POST['age_group'] ?? null,
'gender' => $_POST['gender'] ?? 'male',
'max_occupants' => $maxOccupants,
'color' => $_POST['color'] ?? '#3B82F6',
'effective_from' => $_POST['effective_from'],
'effective_to' => $_POST['effective_to'] ?? null,
'is_active' => 1,
'notes' => $_POST['notes'] ?? null,
]);
return $this->json([
'success' => true,
'message' => 'تم إنشاء الجدول بنجاح',
'schedule' => ['id' => (int) $schedule->id],
]);
}
public function update(Request $request, int $gridId, int $scheduleId): Response
{
$this->authorize('facility_grid.manage');
$schedule = FacilityZoneSchedule::find($scheduleId);
if (!$schedule || (int) $schedule->grid_id !== $gridId) {
return $this->json(['success' => false, 'message' => 'جدول غير موجود']);
}
$grid = FacilityGrid::find($gridId);
$selectionType = $_POST['zone_selection_type'] ?? $schedule->zone_selection_type;
$selectionJson = $_POST['zone_selection_json'] ?? $schedule->zone_selection_json;
$positions = GridStateService::resolvePositions([
'zone_selection_type' => $selectionType,
'zone_selection_json' => $selectionJson,
], (int) $grid->rows_count, (int) $grid->cols_count);
$conflicts = GridStateService::checkConflicts(
$gridId,
$positions,
(int) ($_POST['day_of_week'] ?? $schedule->day_of_week),
$_POST['start_time'] ?? $schedule->start_time,
$_POST['end_time'] ?? $schedule->end_time,
$_POST['effective_from'] ?? $schedule->effective_from,
$_POST['effective_to'] ?? $schedule->effective_to,
$scheduleId
);
if (!empty($conflicts)) {
return $this->json([
'success' => false,
'message' => 'يوجد تعارض مع جداول أخرى',
'conflicts' => $conflicts,
]);
}
$schedule->update([
'schedule_name' => $_POST['schedule_name'] ?? $schedule->schedule_name,
'zone_selection_type' => $selectionType,
'zone_selection_json' => $selectionJson,
'day_of_week' => (int) ($_POST['day_of_week'] ?? $schedule->day_of_week),
'start_time' => $_POST['start_time'] ?? $schedule->start_time,
'end_time' => $_POST['end_time'] ?? $schedule->end_time,
'activity_type' => $_POST['activity_type'] ?? $schedule->activity_type,
'coach_id' => !empty($_POST['coach_id']) ? (int) $_POST['coach_id'] : $schedule->coach_id,
'coach_name' => $_POST['coach_name'] ?? $schedule->coach_name,
'academy_id' => !empty($_POST['academy_id']) ? (int) $_POST['academy_id'] : $schedule->academy_id,
'academy_name' => $_POST['academy_name'] ?? $schedule->academy_name,
'age_group' => $_POST['age_group'] ?? $schedule->age_group,
'gender' => $_POST['gender'] ?? $schedule->gender,
'max_occupants' => !empty($_POST['max_occupants']) ? (int) $_POST['max_occupants'] : (int) $schedule->max_occupants,
'color' => $_POST['color'] ?? $schedule->color,
'effective_from' => $_POST['effective_from'] ?? $schedule->effective_from,
'effective_to' => $_POST['effective_to'] ?? $schedule->effective_to,
'notes' => $_POST['notes'] ?? $schedule->notes,
]);
return $this->json(['success' => true, 'message' => 'تم تحديث الجدول']);
}
public function destroy(Request $request, int $gridId, int $scheduleId): Response
{
$this->authorize('facility_grid.manage');
$schedule = FacilityZoneSchedule::find($scheduleId);
if (!$schedule || (int) $schedule->grid_id !== $gridId) {
return $this->json(['success' => false, 'message' => 'جدول غير موجود']);
}
$schedule->update(['is_active' => 0]);
return $this->json(['success' => true, 'message' => 'تم حذف الجدول']);
}
public function checkConflicts(Request $request, int $gridId): Response
{
$grid = FacilityGrid::find($gridId);
if (!$grid) return $this->json(['success' => false]);
$positions = GridStateService::resolvePositions([
'zone_selection_type' => $_GET['type'] ?? 'cells',
'zone_selection_json' => $_GET['json'] ?? '[]',
], (int) $grid->rows_count, (int) $grid->cols_count);
$conflicts = GridStateService::checkConflicts(
$gridId,
$positions,
(int) ($_GET['day'] ?? 0),
$_GET['start'] ?? '00:00',
$_GET['end'] ?? '23:59',
$_GET['from'] ?? date('Y-m-d'),
$_GET['to'] ?? null,
!empty($_GET['exclude']) ? (int) $_GET['exclude'] : null
);
return $this->json(['success' => true, 'conflicts' => $conflicts]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class ZoneTraineeController extends Controller
{
public function assign(Request $request, int $gridId): Response
{
$this->authorize('facility_grid.manage');
$db = App::getInstance()->db();
$zoneId = (int) ($_POST['zone_id'] ?? 0);
$playerId = !empty($_POST['player_id']) ? (int) $_POST['player_id'] : null;
$traineeName = $_POST['trainee_name'] ?? null;
$scheduleId = !empty($_POST['schedule_id']) ? (int) $_POST['schedule_id'] : null;
if (!$zoneId || (!$playerId && !$traineeName)) {
return $this->json(['success' => false, 'message' => 'بيانات غير كاملة']);
}
$zone = $db->selectOne("SELECT * FROM facility_grid_zones WHERE id = ? AND grid_id = ?", [$zoneId, $gridId]);
if (!$zone) {
return $this->json(['success' => false, 'message' => 'المنطقة غير موجودة']);
}
$currentCount = $db->selectOne(
"SELECT COUNT(*) as cnt FROM facility_zone_trainees WHERE zone_id = ?",
[$zoneId]
);
if ((int) $currentCount['cnt'] >= (int) $zone['max_occupants']) {
return $this->json(['success' => false, 'message' => 'المنطقة ممتلئة']);
}
$session = App::getInstance()->session();
$db->insert('facility_zone_trainees', [
'zone_id' => $zoneId,
'schedule_id' => $scheduleId,
'player_id' => $playerId,
'trainee_name' => $traineeName ?? $this->getPlayerName($playerId),
'assigned_by' => $session->get('employee_id'),
]);
return $this->json(['success' => true, 'message' => 'تم تسجيل المتدرب']);
}
public function remove(Request $request, int $gridId, int $traineeId): Response
{
$this->authorize('facility_grid.manage');
$db = App::getInstance()->db();
$db->delete('facility_zone_trainees', 'id = ?', [$traineeId]);
return $this->json(['success' => true, 'message' => 'تم إزالة المتدرب']);
}
public function move(Request $request, int $gridId, int $traineeId): Response
{
$this->authorize('facility_grid.manage');
$db = App::getInstance()->db();
$targetZoneId = (int) ($_POST['target_zone_id'] ?? 0);
if (!$targetZoneId) {
return $this->json(['success' => false, 'message' => 'لم يتم تحديد المنطقة']);
}
$targetZone = $db->selectOne(
"SELECT * FROM facility_grid_zones WHERE id = ? AND grid_id = ?",
[$targetZoneId, $gridId]
);
if (!$targetZone) {
return $this->json(['success' => false, 'message' => 'المنطقة غير موجودة']);
}
$currentCount = $db->selectOne(
"SELECT COUNT(*) as cnt FROM facility_zone_trainees WHERE zone_id = ?",
[$targetZoneId]
);
if ((int) $currentCount['cnt'] >= (int) $targetZone['max_occupants']) {
return $this->json(['success' => false, 'message' => 'المنطقة المستهدفة ممتلئة']);
}
$db->update('facility_zone_trainees', ['zone_id' => $targetZoneId], 'id = ?', [$traineeId]);
return $this->json(['success' => true, 'message' => 'تم نقل المتدرب']);
}
public function clearZone(Request $request, int $gridId, int $zoneId): Response
{
$this->authorize('facility_grid.manage');
$db = App::getInstance()->db();
$db->delete('facility_zone_trainees', 'zone_id = ?', [$zoneId]);
return $this->json(['success' => true, 'message' => 'تم إفراغ المنطقة']);
}
private function getPlayerName(?int $playerId): ?string
{
if (!$playerId) return null;
$db = App::getInstance()->db();
$player = $db->selectOne("SELECT full_name_ar FROM players WHERE id = ?", [$playerId]);
return $player['full_name_ar'] ?? null;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Models;
use App\Core\Model;
use App\Core\App;
class FacilityGrid extends Model
{
protected static string $table = 'facility_grids';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'facility_id', 'name_ar', 'name_en', 'grid_type',
'rows_count', 'cols_count', 'row_label_ar', 'col_label_ar',
'physical_length', 'physical_width', 'config_json', 'is_active',
];
public function getConfig(): array
{
$raw = $this->config_json;
if (empty($raw)) return [];
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
public function getZones(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM facility_grid_zones WHERE grid_id = ? AND is_active = 1 ORDER BY row_index ASC, col_index ASC",
[(int) $this->id]
);
}
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT fg.*, f.name_ar AS facility_name
FROM facility_grids fg
LEFT JOIN facilities f ON f.id = fg.facility_id
WHERE fg.is_active = 1
ORDER BY fg.name_ar ASC"
);
}
public static function getGridTypes(): array
{
return [
'pool' => 'حمام سباحة',
'court' => 'ملعب',
'generic' => 'عام',
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Models;
use App\Core\Model;
use App\Core\App;
class FacilityZoneSchedule extends Model
{
protected static string $table = 'facility_zone_schedules';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'grid_id', 'plan_month', 'schedule_name', 'zone_selection_type', 'zone_selection_json',
'day_of_week', 'start_time', 'end_time', 'activity_type',
'coach_id', 'coach_name', 'academy_id', 'academy_name',
'age_group', 'gender', 'max_occupants', 'color',
'effective_from', 'effective_to', 'is_active', 'notes',
];
public function getZonePositions(): array
{
$decoded = json_decode($this->zone_selection_json ?? '[]', true);
return is_array($decoded) ? $decoded : [];
}
public static function getActiveForGrid(int $gridId, string $date): array
{
$db = App::getInstance()->db();
$dayOfWeek = (int) date('w', strtotime($date));
return $db->select(
"SELECT * FROM facility_zone_schedules
WHERE grid_id = ? AND day_of_week = ? AND is_active = 1
AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)
ORDER BY start_time ASC",
[$gridId, $dayOfWeek, $date, $date]
);
}
public static function getForGridByMonth(int $gridId, string $month): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT fzs.*, c.full_name_ar AS coach_full_name, a.name_ar AS acad_name
FROM facility_zone_schedules fzs
LEFT JOIN coaches c ON c.id = fzs.coach_id
LEFT JOIN academies a ON a.id = fzs.academy_id
WHERE fzs.grid_id = ? AND fzs.plan_month = ? AND fzs.is_active = 1
ORDER BY fzs.day_of_week ASC, fzs.start_time ASC",
[$gridId, $month]
);
}
public static function getForGridDateAndHour(int $gridId, string $date, string $startTime, string $endTime): array
{
$db = App::getInstance()->db();
$dayOfWeek = (int) date('w', strtotime($date));
return $db->select(
"SELECT * FROM facility_zone_schedules
WHERE grid_id = ? AND day_of_week = ? AND is_active = 1
AND start_time = ? AND end_time = ?
AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)
ORDER BY zone_selection_type ASC",
[$gridId, $dayOfWeek, $startTime, $endTime, $date, $date]
);
}
public static function getMaxOccupants(string $selectionType): int
{
return match ($selectionType) {
'row' => 8,
'column' => 6,
default => 5,
};
}
public static function getAllForGrid(int $gridId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT fzs.*, c.full_name_ar AS coach_full_name, a.name_ar AS acad_name
FROM facility_zone_schedules fzs
LEFT JOIN coaches c ON c.id = fzs.coach_id
LEFT JOIN academies a ON a.id = fzs.academy_id
WHERE fzs.grid_id = ? AND fzs.is_active = 1
ORDER BY fzs.day_of_week ASC, fzs.start_time ASC",
[$gridId]
);
}
public static function getDayNames(): array
{
return [0 => 'الأحد', 1 => 'الإثنين', 2 => 'الثلاثاء', 3 => 'الأربعاء', 4 => 'الخميس', 5 => 'الجمعة', 6 => 'السبت'];
}
public static function getActivityTypes(): array
{
return [
'swimming_lessons' => 'دروس سباحة',
'academy_session' => 'حصة أكاديمية',
'free_swim' => 'سباحة حرة',
'lap_swimming' => 'سباحة (لفات)',
'competition' => 'مسابقة / تدريب',
'private_lesson' => 'درس خصوصي',
'training' => 'تدريب',
'maintenance' => 'صيانة',
];
}
}
<?php
declare(strict_types=1);
return [
// Dashboard
['GET', '/facility-grids/dashboard', 'FacilityGrids\Controllers\GridDashboardController@index', ['auth'], 'facility_grid.view'],
// Grid CRUD
['GET', '/facility-grids', 'FacilityGrids\Controllers\FacilityGridController@index', ['auth'], 'facility_grid.view'],
['GET', '/facility-grids/create', 'FacilityGrids\Controllers\FacilityGridController@create', ['auth'], 'facility_grid.manage'],
['POST', '/facility-grids', 'FacilityGrids\Controllers\FacilityGridController@store', ['auth', 'csrf'], 'facility_grid.manage'],
['GET', '/facility-grids/{id:\d+}', 'FacilityGrids\Controllers\FacilityGridController@show', ['auth'], 'facility_grid.view'],
['GET', '/facility-grids/{id:\d+}/edit', 'FacilityGrids\Controllers\FacilityGridController@edit', ['auth'], 'facility_grid.manage'],
['POST', '/facility-grids/{id:\d+}/update','FacilityGrids\Controllers\FacilityGridController@update', ['auth', 'csrf'], 'facility_grid.manage'],
['POST', '/facility-grids/{id:\d+}/toggle','FacilityGrids\Controllers\FacilityGridController@toggle', ['auth', 'csrf'], 'facility_grid.manage'],
// API — grid state (for AJAX polling)
['GET', '/api/facility-grids/{id:\d+}/state', 'FacilityGrids\Controllers\FacilityGridController@apiState', ['auth'], 'facility_grid.view'],
// Zone Schedules
['GET', '/facility-grids/{gridId:\d+}/schedules', 'FacilityGrids\Controllers\ZoneScheduleController@index', ['auth'], 'facility_grid.manage'],
['POST', '/facility-grids/{gridId:\d+}/schedules', 'FacilityGrids\Controllers\ZoneScheduleController@store', ['auth', 'csrf'], 'facility_grid.manage'],
['POST', '/facility-grids/{gridId:\d+}/schedules/{scheduleId:\d+}', 'FacilityGrids\Controllers\ZoneScheduleController@update', ['auth', 'csrf'], 'facility_grid.manage'],
['POST', '/facility-grids/{gridId:\d+}/schedules/{scheduleId:\d+}/delete','FacilityGrids\Controllers\ZoneScheduleController@destroy', ['auth', 'csrf'], 'facility_grid.manage'],
['GET', '/facility-grids/{gridId:\d+}/schedules/conflicts', 'FacilityGrids\Controllers\ZoneScheduleController@checkConflicts', ['auth'], 'facility_grid.view'],
// Zone Trainees
['POST', '/facility-grids/{gridId:\d+}/trainees', 'FacilityGrids\Controllers\ZoneTraineeController@assign', ['auth', 'csrf'], 'facility_grid.manage'],
['POST', '/facility-grids/{gridId:\d+}/trainees/{traineeId:\d+}/remove', 'FacilityGrids\Controllers\ZoneTraineeController@remove', ['auth', 'csrf'], 'facility_grid.manage'],
['POST', '/facility-grids/{gridId:\d+}/trainees/{traineeId:\d+}/move', 'FacilityGrids\Controllers\ZoneTraineeController@move', ['auth', 'csrf'], 'facility_grid.manage'],
['POST', '/facility-grids/{gridId:\d+}/zones/{zoneId:\d+}/clear', 'FacilityGrids\Controllers\ZoneTraineeController@clearZone', ['auth', 'csrf'], 'facility_grid.manage'],
// Legacy redirects (mirror + pool → unified facility-grids)
['GET', '/mirror', 'FacilityGrids\Controllers\RedirectController@mirrorIndex', ['auth'], 'facility_grid.view'],
['GET', '/mirror/{id:\d+}', 'FacilityGrids\Controllers\RedirectController@mirrorShow', ['auth'], 'facility_grid.view'],
['GET', '/pool/{id:\d+}/grid', 'FacilityGrids\Controllers\RedirectController@poolGrid', ['auth'], 'facility_grid.view'],
['GET', '/pool/{id:\d+}/schedules', 'FacilityGrids\Controllers\RedirectController@poolSchedules', ['auth'], 'facility_grid.view'],
];
<?php
declare(strict_types=1);
namespace App\Modules\FacilityGrids\Services;
use App\Core\App;
use App\Modules\FacilityGrids\Models\FacilityGrid;
use App\Modules\FacilityGrids\Models\FacilityZoneSchedule;
final class GridStateService
{
public static function getState(int $gridId, string $date, ?string $time = null): array
{
$grid = FacilityGrid::find($gridId);
if (!$grid) return [];
$time = $time ?: date('H:i');
$zones = $grid->getZones();
$schedules = FacilityZoneSchedule::getActiveForGrid($gridId, $date);
$db = App::getInstance()->db();
$trainees = $db->select(
"SELECT fzt.*, fgz.row_index, fgz.col_index, p.full_name_ar AS player_name
FROM facility_zone_trainees fzt
JOIN facility_grid_zones fgz ON fgz.id = fzt.zone_id
LEFT JOIN players p ON p.id = fzt.player_id
WHERE fgz.grid_id = ?",
[$gridId]
);
// Build trainee map: "row:col" => [trainees]
$traineeMap = [];
foreach ($trainees as $t) {
$key = $t['row_index'] . ':' . $t['col_index'];
$traineeMap[$key][] = [
'id' => (int) $t['id'],
'name' => $t['player_name'] ?? $t['trainee_name'] ?? '',
];
}
// Resolve which zones have active schedules right now
$zoneStates = [];
foreach ($zones as $zone) {
$r = (int) $zone['row_index'];
$c = (int) $zone['col_index'];
$key = $r . ':' . $c;
$activeSchedule = null;
foreach ($schedules as $sch) {
if ($sch['start_time'] > $time || $sch['end_time'] <= $time) continue;
$positions = self::resolvePositions($sch, (int) $grid->rows_count, (int) $grid->cols_count);
foreach ($positions as $pos) {
if ((int) $pos['row'] === $r && (int) $pos['col'] === $c) {
$activeSchedule = $sch;
break 2;
}
}
}
$zoneStates[$key] = [
'zone_id' => (int) $zone['id'],
'row' => $r,
'col' => $c,
'label' => $zone['label'] ?? null,
'max_occupants'=> (int) $zone['max_occupants'],
'status' => $activeSchedule ? 'occupied' : 'available',
'schedule' => $activeSchedule ? [
'id' => (int) $activeSchedule['id'],
'name' => $activeSchedule['schedule_name'],
'activity_type' => $activeSchedule['activity_type'],
'coach' => $activeSchedule['coach_name'] ?? '',
'academy' => $activeSchedule['academy_name'] ?? '',
'age_group' => $activeSchedule['age_group'] ?? '',
'gender' => $activeSchedule['gender'] ?? 'mixed',
'color' => $activeSchedule['color'] ?? '#3B82F6',
'time' => substr($activeSchedule['start_time'], 0, 5) . '–' . substr($activeSchedule['end_time'], 0, 5),
] : null,
'trainees' => $traineeMap[$key] ?? [],
'trainee_count'=> count($traineeMap[$key] ?? []),
];
}
// All schedules for today (for timeline display)
$todaySchedules = [];
foreach ($schedules as $sch) {
$todaySchedules[] = [
'id' => (int) $sch['id'],
'name' => $sch['schedule_name'],
'start' => substr($sch['start_time'], 0, 5),
'end' => substr($sch['end_time'], 0, 5),
'color' => $sch['color'] ?? '#3B82F6',
'coach' => $sch['coach_name'] ?? '',
'positions' => self::resolvePositions($sch, (int) $grid->rows_count, (int) $grid->cols_count),
];
}
return [
'grid_id' => $gridId,
'date' => $date,
'time' => $time,
'rows' => (int) $grid->rows_count,
'cols' => (int) $grid->cols_count,
'zones' => $zoneStates,
'schedules' => $todaySchedules,
];
}
public static function resolvePositions(array $schedule, int $totalRows, int $totalCols): array
{
$type = $schedule['zone_selection_type'] ?? 'cells';
$json = json_decode($schedule['zone_selection_json'] ?? '[]', true);
if (!is_array($json)) return [];
$positions = [];
switch ($type) {
case 'row':
$row = (int) ($json['row'] ?? 0);
for ($c = 0; $c < $totalCols; $c++) {
$positions[] = ['row' => $row, 'col' => $c];
}
break;
case 'column':
$col = (int) ($json['col'] ?? 0);
for ($r = 0; $r < $totalRows; $r++) {
$positions[] = ['row' => $r, 'col' => $col];
}
break;
case 'all':
for ($r = 0; $r < $totalRows; $r++) {
for ($c = 0; $c < $totalCols; $c++) {
$positions[] = ['row' => $r, 'col' => $c];
}
}
break;
default: // cells
foreach ($json as $cell) {
if (isset($cell['row'], $cell['col'])) {
$positions[] = ['row' => (int) $cell['row'], 'col' => (int) $cell['col']];
}
}
break;
}
return $positions;
}
public static function checkConflicts(int $gridId, array $positions, int $dayOfWeek, string $startTime, string $endTime, string $effectiveFrom, ?string $effectiveTo = null, ?int $excludeScheduleId = null): array
{
$db = App::getInstance()->db();
$grid = FacilityGrid::find($gridId);
if (!$grid) return [];
$query = "SELECT * FROM facility_zone_schedules
WHERE grid_id = ? AND day_of_week = ? AND is_active = 1
AND start_time < ? AND end_time > ?
AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)";
$params = [$gridId, $dayOfWeek, $endTime, $startTime, $effectiveTo ?: '9999-12-31', $effectiveFrom];
if ($excludeScheduleId) {
$query .= " AND id != ?";
$params[] = $excludeScheduleId;
}
$existing = $db->select($query, $params);
$conflicts = [];
foreach ($existing as $sch) {
$schPositions = self::resolvePositions($sch, (int) $grid->rows_count, (int) $grid->cols_count);
$overlap = [];
foreach ($positions as $pos) {
foreach ($schPositions as $sp) {
if ((int) $pos['row'] === (int) $sp['row'] && (int) $pos['col'] === (int) $sp['col']) {
$overlap[] = $pos;
}
}
}
if (!empty($overlap)) {
$conflicts[] = [
'schedule_id' => (int) $sch['id'],
'schedule_name' => $sch['schedule_name'],
'time' => substr($sch['start_time'], 0, 5) . '–' . substr($sch['end_time'], 0, 5),
'overlap_zones' => $overlap,
];
}
}
return $conflicts;
}
}
<?php
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>إنشاء شبكة مرفق<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:700px;margin:0 auto;">
<div style="margin-bottom:20px;">
<a href="/facility-grids" style="color:#6B7280;text-decoration:none;font-size:13px;"><i data-lucide="arrow-right" style="width:14px;height:14px;vertical-align:middle;"></i> شبكات المرافق</a>
</div>
<div class="card" style="padding:24px;">
<h2 style="margin:0 0 24px;font-size:18px;font-weight:800;"><i data-lucide="grid-3x3" style="width:20px;height:20px;vertical-align:middle;margin-left:8px;color:#0D7377;"></i> إنشاء شبكة جديدة</h2>
<form action="/facility-grids" method="POST">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">المرفق *</label>
<select name="facility_id" required style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<option value="">— اختر المرفق —</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>"><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">نوع الشبكة *</label>
<select name="grid_type" required style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<?php foreach ($gridTypes as $k => $v): ?>
<option value="<?= e($k) ?>"><?= e($v) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الاسم بالعربية *</label>
<input type="text" name="name_ar" required placeholder="مثال: حمام سباحة الأوليمبي" value="<?= old('name_ar') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الاسم بالإنجليزية</label>
<input type="text" name="name_en" placeholder="Olympic Pool" value="<?= old('name_en') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">عدد الصفوف (طولي) *</label>
<input type="number" name="rows_count" required min="1" max="50" value="<?= old('rows_count', '6') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<small style="font-size:11px;color:#9CA3AF;">مثال: عدد الحارات</small>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">عدد الأعمدة (عرضي) *</label>
<input type="number" name="cols_count" required min="1" max="50" value="<?= old('cols_count', '4') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<small style="font-size:11px;color:#9CA3AF;">مثال: تقسيمات عرضية</small>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">مسمى الصف</label>
<input type="text" name="row_label_ar" value="حارة" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">مسمى العمود</label>
<input type="text" name="col_label_ar" value="قسم" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الطول الفعلي (متر)</label>
<input type="number" name="physical_length" step="0.1" min="0" placeholder="50" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">العرض الفعلي (متر)</label>
<input type="number" name="physical_width" step="0.1" min="0" placeholder="25" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<!-- Preview -->
<div style="margin-bottom:20px;padding:16px;background:#F9FAFB;border-radius:8px;border:1px dashed #D1D5DB;">
<p style="font-size:12px;font-weight:600;color:#6B7280;margin:0 0 8px;">معاينة الشبكة:</p>
<div id="gridPreview" style="display:grid;gap:3px;max-width:300px;"></div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:12px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i> إنشاء الشبكة
</button>
</form>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
function updatePreview() {
const rows = parseInt(document.querySelector('[name="rows_count"]').value) || 3;
const cols = parseInt(document.querySelector('[name="cols_count"]').value) || 3;
const preview = document.getElementById('gridPreview');
preview.style.gridTemplateColumns = `repeat(${Math.min(cols, 10)}, 1fr)`;
let html = '';
const r = Math.min(rows, 8);
const c = Math.min(cols, 10);
for (let i = 0; i < r * c; i++) {
html += '<div style="aspect-ratio:1;background:#D1FAE5;border:1px solid #86EFAC;border-radius:4px;"></div>';
}
preview.innerHTML = html;
}
document.querySelector('[name="rows_count"]').addEventListener('input', updatePreview);
document.querySelector('[name="cols_count"]').addEventListener('input', updatePreview);
updatePreview();
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\FacilityGrids\Models\FacilityZoneSchedule;
$__template->layout('Layout.main');
$dayNames = FacilityZoneSchedule::getDayNames();
?>
<?php $__template->section('title'); ?>لوحة تحكم المرافق<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="padding:16px;margin-bottom:20px;">
<form method="GET" action="/facility-grids/dashboard" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<div>
<select name="grid_id" style="padding:8px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<option value="">كل المرافق</option>
<?php foreach ($grids as $g): ?>
<option value="<?= (int) $g['id'] ?>" <?= $selectedGrid === (int) $g['id'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="display:flex;gap:0;border:1px solid #D1D5DB;border-radius:8px;overflow:hidden;">
<?php foreach (['day' => 'اليوم', 'week' => 'الأسبوع', 'month' => 'الشهر', 'year' => 'السنة', 'custom' => 'مخصص'] as $fKey => $fLabel): ?>
<button type="submit" name="filter" value="<?= $fKey ?>"
style="padding:8px 14px;font-size:12px;font-weight:600;border:none;cursor:pointer;background:<?= $filter === $fKey ? '#0D7377' : 'white' ?>;color:<?= $filter === $fKey ? 'white' : '#6B7280' ?>;">
<?= $fLabel ?>
</button>
<?php endforeach; ?>
</div>
<?php if ($filter === 'custom'): ?>
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" name="from" value="<?= e($from) ?>" style="padding:8px 10px;border:1px solid #D1D5DB;border-radius:8px;font-size:12px;">
<span style="color:#9CA3AF;">إلى</span>
<input type="date" name="to" value="<?= e($to) ?>" style="padding:8px 10px;border:1px solid #D1D5DB;border-radius:8px;font-size:12px;">
<button type="submit" class="btn btn-primary" style="padding:8px 14px;font-size:12px;">تطبيق</button>
</div>
<?php endif; ?>
<span style="font-size:12px;color:#9CA3AF;margin-right:auto;"><?= e($from) ?><?= e($to) ?></span>
</form>
</div>
<!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px;">
<div class="card" style="padding:20px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">إجمالي الجداول</div>
<div style="font-size:28px;font-weight:800;color:#111;"><?= $stats['total_schedules'] ?></div>
</div>
<div class="card" style="padding:20px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المتدربين المسجلين</div>
<div style="font-size:28px;font-weight:800;color:#0D7377;"><?= $stats['total_trainees'] ?></div>
</div>
<div class="card" style="padding:20px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">إجمالي المناطق</div>
<div style="font-size:28px;font-weight:800;color:#111;"><?= $stats['total_zones'] ?></div>
</div>
<div class="card" style="padding:20px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">نسبة الإشغال</div>
<div style="font-size:28px;font-weight:800;color:<?= $stats['total_zones'] > 0 ? '#059669' : '#9CA3AF' ?>;">
<?= $stats['total_zones'] > 0 ? round(($stats['occupied_zones'] / $stats['total_zones']) * 100) : 0 ?>%
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<!-- Schedules by Day -->
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;">توزيع الجداول حسب اليوم</h3>
<?php
$dayData = array_fill(0, 7, 0);
foreach ($schedulesByDay as $sd) $dayData[(int) $sd['day_of_week']] = (int) $sd['cnt'];
$maxDay = max($dayData) ?: 1;
?>
<div style="display:flex;flex-direction:column;gap:8px;">
<?php foreach ($dayNames as $d => $dName): ?>
<div style="display:flex;align-items:center;gap:10px;">
<span style="font-size:12px;font-weight:600;color:#374151;min-width:60px;"><?= e($dName) ?></span>
<div style="flex:1;background:#F3F4F6;border-radius:4px;height:24px;position:relative;">
<div style="height:100%;background:#0D7377;border-radius:4px;width:<?= round(($dayData[$d] / $maxDay) * 100) ?>%;transition:width .3s;"></div>
</div>
<span style="font-size:12px;font-weight:700;color:#111;min-width:24px;text-align:left;"><?= $dayData[$d] ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Occupancy by Hour -->
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;">الإشغال حسب الساعة</h3>
<?php
$hourData = array_fill(6, 16, 0);
foreach ($occupancyByHour as $oh) $hourData[(int) $oh['hour']] = (int) $oh['cnt'];
$maxHour = max($hourData) ?: 1;
?>
<div style="display:flex;align-items:flex-end;gap:4px;height:160px;">
<?php for ($h = 6; $h < 22; $h++): ?>
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;">
<div style="width:100%;background:<?= ($hourData[$h] / $maxHour) > 0.7 ? '#DC2626' : (($hourData[$h] / $maxHour) > 0.4 ? '#F59E0B' : '#10B981') ?>;border-radius:3px 3px 0 0;height:<?= round(($hourData[$h] / $maxHour) * 100) ?>%;min-height:<?= $hourData[$h] > 0 ? '4px' : '0' ?>;transition:height .3s;" title="<?= $hourData[$h] ?> جدول"></div>
<span style="font-size:9px;color:#9CA3AF;margin-top:4px;"><?= $h ?></span>
</div>
<?php endfor; ?>
</div>
<div style="display:flex;gap:12px;margin-top:12px;font-size:10px;color:#6B7280;">
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#10B981;margin-left:4px;"></span>منخفض</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#F59E0B;margin-left:4px;"></span>متوسط</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#DC2626;margin-left:4px;"></span>مرتفع</span>
</div>
</div>
</div>
<!-- Top Coaches -->
<?php if (!empty($topCoaches)): ?>
<div class="card" style="padding:20px;margin-top:20px;">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;">أكثر المدربين نشاطاً</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;">
<?php foreach ($topCoaches as $i => $coach): ?>
<div style="display:flex;align-items:center;gap:10px;padding:10px;background:#F9FAFB;border-radius:8px;">
<div style="width:28px;height:28px;border-radius:50%;background:#0D7377;color:white;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;"><?= $i + 1 ?></div>
<div>
<div style="font-size:13px;font-weight:600;color:#111;"><?= e($coach['coach_name']) ?></div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $coach['sessions'] ?> حصة</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تعديل شبكة — <?= e($grid->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:700px;margin:0 auto;">
<div style="margin-bottom:20px;">
<a href="/facility-grids/<?= (int) $grid->id ?>" style="color:#6B7280;text-decoration:none;font-size:13px;"><i data-lucide="arrow-right" style="width:14px;height:14px;vertical-align:middle;"></i> <?= e($grid->name_ar) ?></a>
</div>
<div class="card" style="padding:24px;">
<h2 style="margin:0 0 24px;font-size:18px;font-weight:800;"><i data-lucide="settings" style="width:20px;height:20px;vertical-align:middle;margin-left:8px;color:#0D7377;"></i> تعديل الشبكة</h2>
<form action="/facility-grids/<?= (int) $grid->id ?>/update" method="POST">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">المرفق</label>
<select name="facility_id" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= (int) $grid->facility_id === (int) $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">نوع الشبكة</label>
<select name="grid_type" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
<?php foreach ($gridTypes as $k => $v): ?>
<option value="<?= e($k) ?>" <?= $grid->grid_type === $k ? 'selected' : '' ?>><?= e($v) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الاسم بالعربية *</label>
<input type="text" name="name_ar" required value="<?= e($grid->name_ar) ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الاسم بالإنجليزية</label>
<input type="text" name="name_en" value="<?= e($grid->name_en ?? '') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">عدد الصفوف *</label>
<input type="number" name="rows_count" required min="1" max="50" value="<?= (int) $grid->rows_count ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">عدد الأعمدة *</label>
<input type="number" name="cols_count" required min="1" max="50" value="<?= (int) $grid->cols_count ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">مسمى الصف</label>
<input type="text" name="row_label_ar" value="<?= e($grid->row_label_ar ?? 'حارة') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">مسمى العمود</label>
<input type="text" name="col_label_ar" value="<?= e($grid->col_label_ar ?? 'قسم') ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الطول الفعلي (متر)</label>
<input type="number" name="physical_length" step="0.1" min="0" value="<?= $grid->physical_length ?? '' ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">العرض الفعلي (متر)</label>
<input type="number" name="physical_width" step="0.1" min="0" value="<?= $grid->physical_width ?? '' ?>" style="width:100%;padding:10px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;">
</div>
</div>
<div style="display:flex;gap:12px;">
<button type="submit" class="btn btn-primary" style="flex:1;padding:12px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i> حفظ التغييرات
</button>
<a href="/facility-grids/<?= (int) $grid->id ?>" class="btn btn-outline" style="padding:12px 24px;">إلغاء</a>
</div>
</form>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>شبكات المرافق<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/facility-grids/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء شبكة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (empty($grids)): ?>
<div class="card" style="padding:60px;text-align:center;">
<i data-lucide="grid-3x3" style="width:48px;height:48px;color:#9CA3AF;margin-bottom:16px;"></i>
<p style="font-size:16px;color:#6B7280;margin-bottom:16px;">لا توجد شبكات مرافق بعد</p>
<a href="/facility-grids/create" class="btn btn-primary">إنشاء أول شبكة</a>
</div>
<?php else: ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:20px;">
<?php foreach ($grids as $grid): ?>
<a href="/facility-grids/<?= (int) $grid['id'] ?>" class="card" style="text-decoration:none;color:inherit;padding:24px;transition:all .2s;border:2px solid transparent;" onmouseenter="this.style.borderColor='#0D7377'" onmouseleave="this.style.borderColor='transparent'">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<div style="width:44px;height:44px;border-radius:10px;background:<?= $grid['grid_type'] === 'pool' ? '#DBEAFE' : ($grid['grid_type'] === 'court' ? '#D1FAE5' : '#F3E8FF') ?>;display:flex;align-items:center;justify-content:center;">
<i data-lucide="<?= $grid['grid_type'] === 'pool' ? 'waves' : ($grid['grid_type'] === 'court' ? 'square' : 'grid-3x3') ?>" style="width:22px;height:22px;color:<?= $grid['grid_type'] === 'pool' ? '#2563EB' : ($grid['grid_type'] === 'court' ? '#059669' : '#7C3AED') ?>;"></i>
</div>
<div>
<h3 style="font-size:16px;font-weight:700;margin:0;"><?= e($grid['name_ar']) ?></h3>
<span style="font-size:12px;color:#6B7280;"><?= e($grid['facility_name'] ?? '') ?></span>
</div>
</div>
<div style="display:flex;gap:20px;font-size:13px;color:#374151;">
<span><i data-lucide="rows-3" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;color:#9CA3AF;"></i> <?= (int) $grid['rows_count'] ?> صفوف</span>
<span><i data-lucide="columns-3" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;color:#9CA3AF;"></i> <?= (int) $grid['cols_count'] ?> أعمدة</span>
</div>
<?php if (!empty($grid['physical_length'])): ?>
<div style="margin-top:8px;font-size:12px;color:#9CA3AF;">
<?= (float) $grid['physical_length'] ?>م × <?= (float) $grid['physical_width'] ?>م
</div>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
use App\Modules\FacilityGrids\Models\FacilityZoneSchedule;
$__template->layout('Layout.main');
$activityTypes = FacilityZoneSchedule::getActivityTypes();
?>
<?php $__template->section('title'); ?>جداول — <?= e($grid->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;">
<a href="/facility-grids/<?= (int) $grid->id ?>" style="color:#6B7280;text-decoration:none;font-size:13px;"><i data-lucide="arrow-right" style="width:14px;height:14px;vertical-align:middle;"></i> <?= e($grid->name_ar) ?></a>
<span style="color:#D1D5DB;">|</span>
<h2 style="margin:0;font-size:18px;font-weight:800;">كل الجداول</h2>
<span style="background:#E5E7EB;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:700;"><?= count($schedules) ?></span>
</div>
<?php if (empty($schedules)): ?>
<div class="card" style="padding:40px;text-align:center;">
<i data-lucide="calendar-off" style="width:40px;height:40px;color:#9CA3AF;margin-bottom:12px;"></i>
<p style="color:#6B7280;font-size:14px;">لا توجد جداول بعد. افتح <a href="/facility-grids/<?= (int) $grid->id ?>" style="color:#0D7377;font-weight:600;">الشبكة</a> واستخدم منشئ الجداول.</p>
</div>
<?php else: ?>
<!-- Group by day -->
<?php
$byDay = [];
foreach ($schedules as $s) $byDay[(int) $s['day_of_week']][] = $s;
?>
<?php foreach ($byDay as $day => $daySchedules): ?>
<div class="card" style="padding:16px;margin-bottom:12px;">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:700;color:#374151;">
<i data-lucide="calendar" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;color:#6B7280;"></i>
<?= e($dayNames[$day] ?? '') ?>
<span style="font-size:12px;font-weight:400;color:#9CA3AF;margin-right:8px;">(<?= count($daySchedules) ?> جدول)</span>
</h3>
<div style="display:grid;gap:8px;">
<?php foreach ($daySchedules as $sch): ?>
<div style="display:flex;align-items:center;gap:12px;padding:12px;background:#F9FAFB;border-radius:8px;border-right:4px solid <?= e($sch['color'] ?? '#3B82F6') ?>;">
<div style="flex:1;">
<div style="font-size:14px;font-weight:700;color:#111;"><?= e($sch['schedule_name']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">
<?= substr($sch['start_time'], 0, 5) ?><?= substr($sch['end_time'], 0, 5) ?>
<?php if (!empty($sch['coach_name'])): ?><?= e($sch['coach_name']) ?><?php endif; ?>
<?php if (!empty($sch['activity_type'])): ?><?= e($activityTypes[$sch['activity_type']] ?? $sch['activity_type']) ?><?php endif; ?>
</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;">
نوع التحديد: <?= e($sch['zone_selection_type']) ?>
• ساري من <?= e($sch['effective_from']) ?><?= $sch['effective_to'] ? ' حتى ' . e($sch['effective_to']) : ' (مفتوح)' ?>
</div>
</div>
<form method="POST" action="/facility-grids/<?= (int) $grid->id ?>/schedules/<?= (int) $sch['id'] ?>/delete" onsubmit="return confirm('حذف هذا الجدول؟');" style="margin:0;">
<?= csrf_field() ?>
<button type="submit" style="background:none;border:none;cursor:pointer;color:#DC2626;padding:6px;" title="حذف">
<i data-lucide="trash-2" style="width:16px;height:16px;"></i>
</button>
</form>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php $__template->endSection(); ?>
This diff is collapsed.
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
PermissionRegistry::register('facility_grids', [
'facility_grid.view' => ['ar' => 'عرض شبكات المرافق', 'en' => 'View Facility Grids'],
'facility_grid.manage' => ['ar' => 'إدارة شبكات المرافق', 'en' => 'Manage Facility Grids'],
]);
...@@ -9,15 +9,8 @@ return [ ...@@ -9,15 +9,8 @@ return [
['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'], ['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'],
['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'], ['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'],
// Pool Grid (interactive) // Pool Grid & Schedules — migrated to unified FacilityGrids module
['GET', '/pool/{id:\d+}/grid', 'PoolManagement\Controllers\PoolGridController@grid', ['auth'], 'pool.view'], // /pool/{id}/grid and /pool/{id}/schedules now redirect via FacilityGrids\Controllers\RedirectController
['POST', '/pool/{id:\d+}/book-cells', 'PoolManagement\Controllers\PoolGridController@bookCells', ['auth', 'csrf'], 'pool.book'],
['GET', '/api/pool/{id:\d+}/state', 'PoolManagement\Controllers\PoolGridController@apiState', ['auth'], 'pool.view'],
// Pool Schedules (recurring weekly)
['GET', '/pool/{id:\d+}/schedules', 'PoolManagement\Controllers\PoolScheduleController@index', ['auth'], 'pool.manage'],
['POST', '/pool/{id:\d+}/schedules', 'PoolManagement\Controllers\PoolScheduleController@store', ['auth', 'csrf'], 'pool.manage'],
['POST', '/pool/{id:\d+}/schedules/{sid:\d+}/delete','PoolManagement\Controllers\PoolScheduleController@delete', ['auth', 'csrf'], 'pool.manage'],
// Pool Bookings // Pool Bookings
['GET', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@create', ['auth'], 'pool.book'], ['GET', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@create', ['auth'], 'pool.book'],
......
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `facility_grids` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`facility_id` BIGINT UNSIGNED NOT NULL,
`name_ar` VARCHAR(300) NOT NULL,
`name_en` VARCHAR(300) NULL,
`grid_type` VARCHAR(20) NOT NULL DEFAULT 'generic' COMMENT 'generic, pool, court',
`rows_count` INT UNSIGNED NOT NULL DEFAULT 4,
`cols_count` INT UNSIGNED NOT NULL DEFAULT 6,
`row_label_ar` VARCHAR(50) NULL DEFAULT 'صف',
`col_label_ar` VARCHAR(50) NULL DEFAULT 'عمود',
`physical_length` DECIMAL(5,2) NULL COMMENT 'meters',
`physical_width` DECIMAL(5,2) NULL COMMENT 'meters',
`config_json` JSON NULL COMMENT 'type-specific: depths, operating_hours, safety_ratios',
`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_fg_facility` (`facility_id`),
INDEX `idx_fg_type` (`grid_type`),
CONSTRAINT `fk_fg_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 `facility_grids`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `facility_grid_zones` (
`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,
`zone_type` VARCHAR(20) NULL COMMENT 'lane, section, cell',
`width_meters` DECIMAL(4,2) NULL,
`max_occupants` INT UNSIGNED NOT NULL DEFAULT 5,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_fgz_grid` (`grid_id`),
UNIQUE KEY `uq_fgz_position` (`grid_id`, `row_index`, `col_index`),
CONSTRAINT `fk_fgz_grid` FOREIGN KEY (`grid_id`) REFERENCES `facility_grids`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `facility_grid_zones`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `facility_zone_schedules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`grid_id` BIGINT UNSIGNED NOT NULL,
`schedule_name` VARCHAR(255) NOT NULL,
`zone_selection_type` VARCHAR(20) NOT NULL COMMENT 'cells, row, column, all',
`zone_selection_json` JSON NOT NULL COMMENT 'selected positions',
`day_of_week` TINYINT UNSIGNED NOT NULL COMMENT '0=Sun...6=Sat',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`activity_type` VARCHAR(30) NULL,
`coach_id` BIGINT UNSIGNED NULL,
`coach_name` VARCHAR(200) NULL,
`academy_id` BIGINT UNSIGNED NULL,
`academy_name` VARCHAR(200) NULL,
`age_group` VARCHAR(50) NULL,
`gender` VARCHAR(10) NULL DEFAULT 'mixed',
`max_occupants` INT UNSIGNED NOT NULL DEFAULT 8,
`color` VARCHAR(20) NULL DEFAULT '#3B82F6',
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`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_fzs_grid` (`grid_id`),
INDEX `idx_fzs_day` (`day_of_week`),
INDEX `idx_fzs_active` (`is_active`, `effective_from`, `effective_to`),
CONSTRAINT `fk_fzs_grid` FOREIGN KEY (`grid_id`) REFERENCES `facility_grids`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `facility_zone_schedules`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `facility_zone_trainees` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`zone_id` BIGINT UNSIGNED NOT NULL,
`schedule_id` BIGINT UNSIGNED NULL,
`player_id` BIGINT UNSIGNED NULL,
`trainee_name` VARCHAR(300) NULL,
`assigned_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`assigned_by` BIGINT UNSIGNED NULL,
INDEX `idx_fzt_zone` (`zone_id`),
INDEX `idx_fzt_player` (`player_id`),
INDEX `idx_fzt_schedule` (`schedule_id`),
CONSTRAINT `fk_fzt_zone` FOREIGN KEY (`zone_id`) REFERENCES `facility_grid_zones`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `facility_zone_trainees`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `facility_zone_schedules`
ADD COLUMN `plan_month` VARCHAR(7) NULL COMMENT 'YYYY-MM format, the month this plan belongs to' AFTER `grid_id`,
ADD INDEX `idx_fzs_plan_month` (`plan_month`)",
'down' => "ALTER TABLE `facility_zone_schedules`
DROP INDEX `idx_fzs_plan_month`,
DROP COLUMN `plan_month`",
];
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