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

fsgdrndfh

parent af806c60
......@@ -13,29 +13,21 @@ MenuRegistry::register('sports_activities', [
'label_ar' => 'الأنشطة الرياضية',
'label_en' => 'Sports Activities',
'icon' => 'activity',
'route' => '/disciplines',
'route' => '/sports-dashboard',
'permission' => 'discipline.view',
'parent' => null,
'order' => 395,
'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Sports Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 0],
['label_ar' => 'الأنشطة الرياضية', 'label_en' => 'Disciplines', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 1],
['label_ar' => 'الملاعب والمرافق', 'label_en' => 'Facilities', 'route' => '/facilities', 'permission' => 'facility.view', 'order' => 2],
['label_ar' => 'المراية', 'label_en' => 'Mirror Display', 'route' => '/mirror', 'permission' => 'facility.mirror', 'order' => 2.5],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/academies', 'permission' => 'academy.view', 'order' => 3],
['label_ar' => 'عقود الأكاديميات', 'label_en' => 'Academy Contracts', 'route' => '/academy-contracts', 'permission' => 'academy_contract.view', 'order' => 3.5],
['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],
['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],
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 1],
['label_ar' => 'الأنشطة والأكاديميات', 'label_en' => 'Disciplines & Academies', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 2],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/coaches', 'permission' => 'coach.view', 'order' => 3],
['label_ar' => 'المجموعات واللاعبين', 'label_en' => 'Groups & Players', 'route' => '/training-groups', 'permission' => 'training_group.view', 'order' => 4],
['label_ar' => 'الحصص والحضور', 'label_en' => 'Sessions & Attendance', 'route' => '/sessions', 'permission' => 'session.view', 'order' => 5],
['label_ar' => 'المرافق والشبكات', 'label_en' => 'Facilities & Grids', 'route' => '/facility-grids', 'permission' => 'facility_grid.view', 'order' => 6],
['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 7],
['label_ar' => 'التأجير المؤسسي', 'label_en' => 'Corporate Rentals', 'route' => '/rentals', 'permission' => 'rental.view', 'order' => 8],
['label_ar' => 'الاشتراكات والتسعير', 'label_en' => 'Subscriptions & Pricing', 'route' => '/activity-subscriptions', 'permission' => 'activity_sub.view', 'order' => 9],
['label_ar' => 'العقود والتسويات', 'label_en' => 'Contracts & Settlements', 'route' => '/academy-contracts', 'permission' => 'academy_contract.view', 'order' => 10],
],
]);
......
<?php
declare(strict_types=1);
return [
// 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'],
// Mirror routes have been migrated to the unified FacilityGrids module.
// Old /mirror/* paths now redirect to /facility-grids/* via FacilityGrids\Controllers\RedirectController.
// Facility Dashboard
return [
// Facility Dashboard (per-facility stats page — still relevant)
['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(); ?>
<?php
use App\Modules\FacilityGrids\Models\FacilityZoneSchedule;
$__template->layout('Layout.main');
$dayNames = FacilityZoneSchedule::getDayNames();
$activityTypes = FacilityZoneSchedule::getActivityTypes();
$rows = (int) $grid->rows_count;
$cols = (int) $grid->cols_count;
$zones = $state['zones'] ?? [];
$schedules = $state['schedules'] ?? [];
$currentTime = $state['time'] ?? date('H:i');
$currentHour = (int) substr($currentTime, 0, 2);
// Operating hours (6 AM to 10 PM)
$startHour = 6;
$endHour = 22;
// Can user edit? (permission-based)
$canEdit = true; // Controller already authorized
$planMonth = $_GET['month'] ?? date('Y-m');
?>
<?php $__template->section('title'); ?>مراية — <?= e($grid->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.hour-tabs {
display: flex; gap: 0; overflow-x: auto; border-bottom: 2px solid #E5E7EB;
padding-bottom: 0; margin-bottom: 0; scrollbar-width: thin;
}
.hour-tab {
padding: 10px 16px; font-size: 13px; font-weight: 600;
cursor: pointer; border: none; background: white; color: #6B7280;
border-bottom: 3px solid transparent; white-space: nowrap;
transition: all .15s;
}
.hour-tab:hover { color: #0D7377; background: #F0FDFA; }
.hour-tab.active { color: #0D7377; border-bottom-color: #0D7377; background: #F0FDFA; }
.hour-tab.has-schedule { position: relative; }
.hour-tab.has-schedule::after {
content: ''; position: absolute; top: 6px; left: 6px;
width: 6px; height: 6px; border-radius: 50%; background: #10B981;
}
.pool-grid {
display: grid; gap: 6px; direction: ltr;
}
.pool-box {
border-radius: 10px; border: 2px solid #E5E7EB; padding: 10px;
min-height: 110px; position: relative; cursor: pointer;
transition: all .2s; background: white;
}
.pool-box:hover { box-shadow: 0 4px 16px rgba(0,0,0,.1); transform: translateY(-1px); }
.pool-box.occupied {
border-color: var(--box-color, #3B82F6);
background: color-mix(in srgb, var(--box-color, #3B82F6) 6%, white);
}
.pool-box.available { border-color: #D1FAE5; background: #F0FDF4; border-style: dashed; }
.pool-box .box-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 6px; gap: 6px;
}
.pool-box .box-coach { font-size: 12px; font-weight: 700; color: #111; }
.pool-box .box-meta { font-size: 10px; color: #6B7280; margin-top: 2px; }
.pool-box .box-badge {
font-size: 9px; font-weight: 700; padding: 2px 6px;
border-radius: 4px; display: inline-block;
}
.pool-box .box-badge.male { background: #DBEAFE; color: #1D4ED8; }
.pool-box .box-badge.female { background: #FCE7F3; color: #BE185D; }
.pool-box .trainee-list { margin-top: 8px; border-top: 1px solid #F3F4F6; padding-top: 6px; }
.pool-box .trainee-item {
display: flex; align-items: center; justify-content: space-between;
font-size: 11px; padding: 3px 0; color: #374151;
}
.pool-box .trainee-item .remove-btn {
color: #DC2626; cursor: pointer; opacity: 0; transition: opacity .15s;
background: none; border: none; padding: 0;
}
.pool-box:hover .trainee-item .remove-btn { opacity: 1; }
.pool-box .box-count {
position: absolute; top: -8px; right: -8px;
background: #1F2937; color: white; border-radius: 50%;
width: 22px; height: 22px; font-size: 10px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.pool-box .box-empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 100%; color: #9CA3AF; font-size: 12px;
}
.row-label, .col-label {
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700; color: #374151;
background: #F9FAFB; border-radius: 6px; padding: 8px 4px;
}
.plan-header {
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 12px; margin-bottom: 16px;
padding: 16px 20px; background: white; border-radius: 12px;
border: 1px solid #E5E7EB;
}
.month-nav { display: flex; align-items: center; gap: 8px; }
.month-nav button {
background: #F3F4F6; border: none; border-radius: 6px;
padding: 6px 10px; cursor: pointer; font-size: 14px; color: #374151;
}
.month-nav button:hover { background: #E5E7EB; }
.month-nav .month-label { font-size: 16px; font-weight: 800; color: #111; min-width: 140px; text-align: center; }
/* Modal */
.modal-overlay {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4);
z-index: 1000; align-items: center; justify-content: center;
}
.modal-overlay.active { display: flex; }
.modal-box {
background: white; border-radius: 16px; padding: 24px;
max-width: 480px; width: 90%; max-height: 90vh; overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,.2);
}
.modal-box h3 { margin: 0 0 16px; font-size: 16px; font-weight: 800; }
.modal-box label { display: block; font-size: 12px; font-weight: 600; color: #374151; margin-bottom: 4px; }
.modal-box input, .modal-box select { width: 100%; padding: 9px 12px; border: 1px solid #D1D5DB; border-radius: 8px; font-size: 13px; margin-bottom: 12px; }
.modal-box .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* Drag */
.pool-box.drag-over { border-color: #F59E0B !important; background: #FFFBEB !important; }
.trainee-item[draggable="true"] { cursor: grab; }
.trainee-item[draggable="true"]:active { cursor: grabbing; }
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Plan Header -->
<div class="plan-header">
<div style="display:flex;align-items:center;gap:12px;">
<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>
<h2 style="margin:0;font-size:20px;font-weight:800;">مراية <?= e($grid->name_ar) ?></h2>
<span style="font-size:12px;color:#6B7280;"><?= $rows ?> حارات × <?= $cols ?> أقسام</span>
</div>
</div>
<div class="month-nav">
<button onclick="changeMonth(-1)" title="الشهر السابق"><i data-lucide="chevron-right" style="width:16px;height:16px;"></i></button>
<span class="month-label" id="monthLabel"><?= arabic_date($planMonth . '-01', 'F Y') ?></span>
<button onclick="changeMonth(1)" title="الشهر التالي"><i data-lucide="chevron-left" style="width:16px;height:16px;"></i></button>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="gridDate" value="<?= e($date) ?>" style="padding:8px 12px;border:1px solid #D1D5DB;border-radius:8px;font-size:13px;" onchange="reloadForDate()">
<a href="/facility-grids/<?= (int) $grid->id ?>/schedules" class="btn btn-outline" style="font-size:12px;padding:8px 12px;">
<i data-lucide="list" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> كل الجداول
</a>
</div>
</div>
<!-- Hour Tabs -->
<div class="card" style="padding:0;margin-bottom:16px;">
<div class="hour-tabs" id="hourTabs">
<?php for ($h = $startHour; $h < $endHour; $h++):
$hStart = sprintf('%02d:00', $h);
$hEnd = sprintf('%02d:00', $h + 1);
$hasSchedule = false;
foreach ($schedules as $sch) {
if ($sch['start'] === substr($hStart, 0, 5)) { $hasSchedule = true; break; }
}
$isActive = ($h === $currentHour);
?>
<button class="hour-tab <?= $isActive ? 'active' : '' ?> <?= $hasSchedule ? 'has-schedule' : '' ?>"
data-hour="<?= $h ?>"
onclick="selectHour(<?= $h ?>)">
<?= $hStart ?><?= $hEnd ?>
</button>
<?php endfor; ?>
</div>
</div>
<!-- Pool Grid for Selected Hour -->
<div class="card" style="padding:20px;" id="gridCard">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<div style="font-size:14px;font-weight:700;color:#374151;">
<i data-lucide="clock" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#6B7280;"></i>
<span id="selectedHourLabel"><?= sprintf('%02d:00', $currentHour) ?><?= sprintf('%02d:00', $currentHour + 1) ?></span>
</div>
<div style="display:flex;gap:8px;font-size:11px;color:#6B7280;">
<span><span style="display:inline-block;width:10px;height:10px;border-radius:3px;background:#F0FDF4;border:1px dashed #86EFAC;vertical-align:middle;margin-left:4px;"></span>متاح</span>
<span><span style="display:inline-block;width:10px;height:10px;border-radius:3px;background:#DBEAFE;border:1px solid #3B82F6;vertical-align:middle;margin-left:4px;"></span>مشغول</span>
<span style="color:#1F2937;font-weight:600;">Box: أقصى ٥ | حارة طولية: ٨ | حارة عرضية: ٦</span>
</div>
</div>
<div class="pool-grid" id="poolGrid" style="grid-template-columns: 50px repeat(<?= $cols ?>, 1fr);">
<!-- Corner -->
<div></div>
<!-- Column Headers -->
<?php for ($c = 0; $c < $cols; $c++): ?>
<div class="col-label"><?= e($grid->col_label_ar ?? 'قسم') ?> <?= $c + 1 ?></div>
<?php endfor; ?>
<!-- Rows -->
<?php for ($r = 0; $r < $rows; $r++): ?>
<div class="row-label"><?= e($grid->row_label_ar ?? 'حارة') ?> <?= $r + 1 ?></div>
<?php for ($c = 0; $c < $cols; $c++):
$key = $r . ':' . $c;
$zone = $zones[$key] ?? null;
$status = $zone['status'] ?? 'available';
$schedule = $zone['schedule'] ?? null;
$trainees = $zone['trainees'] ?? [];
$traineeCount = $zone['trainee_count'] ?? 0;
$cellColor = $schedule['color'] ?? '#3B82F6';
$maxOcc = $zone['max_occupants'] ?? 5;
// Check if this is part of active schedule for current hour
$isCurrentHour = false;
if ($schedule) {
$schStart = (int) substr($schedule['time'], 0, 2);
$isCurrentHour = ($schStart === $currentHour);
}
?>
<div class="pool-box <?= ($status === 'occupied' && $isCurrentHour) ? 'occupied' : 'available' ?>"
style="--box-color:<?= e($cellColor) ?>;"
data-row="<?= $r ?>" data-col="<?= $c ?>"
data-zone-id="<?= $zone['zone_id'] ?? 0 ?>"
ondragover="dragOver(event)" ondragleave="dragLeave(event)" ondrop="dropTrainee(event, <?= $zone['zone_id'] ?? 0 ?>)"
onclick="openBoxModal(<?= $r ?>, <?= $c ?>, this)">
<?php if ($status === 'occupied' && $isCurrentHour && $schedule): ?>
<?php if ($traineeCount > 0): ?>
<span class="box-count"><?= $traineeCount ?>/<?= $maxOcc ?></span>
<?php endif; ?>
<div class="box-header">
<span class="box-coach"><?= e($schedule['coach'] ?: $schedule['academy'] ?: $schedule['name']) ?></span>
<span class="box-badge <?= e($schedule['gender'] ?? 'male') ?>"><?= ($schedule['gender'] ?? 'male') === 'female' ? 'بنات' : 'ولاد' ?></span>
</div>
<div class="box-meta">
<?php if (!empty($schedule['age_group'])): ?><?= e($schedule['age_group']) ?><?php endif; ?>
<?= e($activityTypes[$schedule['activity_type'] ?? ''] ?? '') ?>
</div>
<?php if (!empty($trainees)): ?>
<div class="trainee-list">
<?php foreach ($trainees as $t): ?>
<div class="trainee-item" draggable="true" data-trainee-id="<?= $t['id'] ?>" ondragstart="dragStart(event, <?= $t['id'] ?>)">
<span><?= e($t['name']) ?></span>
<button class="remove-btn" onclick="event.stopPropagation();removeTrainee(<?= $t['id'] ?>)" title="إزالة">
<i data-lucide="x" style="width:12px;height:12px;"></i>
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="box-empty">
<i data-lucide="plus-circle" style="width:20px;height:20px;margin-bottom:4px;"></i>
<span>متاح</span>
</div>
<?php endif; ?>
</div>
<?php endfor; ?>
<?php endfor; ?>
</div>
</div>
<!-- Box Edit/Create Modal -->
<div class="modal-overlay" id="boxModal">
<div class="modal-box">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<h3 id="modalTitle">تعيين خانة</h3>
<button onclick="closeModal()" style="background:none;border:none;cursor:pointer;color:#6B7280;"><i data-lucide="x" style="width:20px;height:20px;"></i></button>
</div>
<form id="boxForm" onsubmit="saveBox(event)">
<input type="hidden" name="zone_selection_type" value="cells">
<input type="hidden" name="zone_selection_json" id="modalSelJson" value="[]">
<input type="hidden" name="plan_month" value="<?= e($planMonth) ?>">
<div class="form-row">
<div>
<label>اسم المدرب / الأكاديمية *</label>
<input type="text" name="schedule_name" id="modalName" required placeholder="مثال: ك. أحمد">
</div>
<div>
<label>نوع النشاط</label>
<select name="activity_type" id="modalActivity">
<?php foreach ($activityTypes as $k => $v): ?>
<option value="<?= e($k) ?>"><?= e($v) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div>
<label>الفئة العمرية</label>
<input type="text" name="age_group" id="modalAge" placeholder="مثال: 6-9 سنوات">
</div>
<div>
<label>الجنس *</label>
<select name="gender" id="modalGender">
<option value="male">ولاد</option>
<option value="female">بنات</option>
</select>
</div>
</div>
<div class="form-row">
<div>
<label>اليوم *</label>
<select name="day_of_week" id="modalDay">
<?php foreach ($dayNames as $d => $name): ?>
<option value="<?= $d ?>" <?= $d === (int) date('w', strtotime($date)) ? 'selected' : '' ?>><?= e($name) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label>اللون</label>
<input type="color" name="color" id="modalColor" value="#3B82F6" style="height:38px;padding:4px;">
</div>
</div>
<input type="hidden" name="start_time" id="modalStart">
<input type="hidden" name="end_time" id="modalEnd">
<input type="hidden" name="effective_from" value="<?= e($planMonth) ?>-01">
<input type="hidden" name="coach_name" id="modalCoachName">
<div style="margin-top:16px;padding-top:12px;border-top:1px solid #E5E7EB;">
<label>إضافة متدرب</label>
<div style="display:flex;gap:8px;">
<input type="text" id="newTraineeName" placeholder="اسم المتدرب" style="flex:1;margin-bottom:0;">
<button type="button" onclick="addTraineeToBox()" class="btn btn-outline" style="padding:8px 14px;font-size:12px;margin-bottom:0;">
<i data-lucide="user-plus" style="width:14px;height:14px;"></i>
</button>
</div>
</div>
<div style="display:flex;gap:12px;margin-top:20px;">
<button type="submit" class="btn btn-primary" style="flex:1;padding:10px;">
<i data-lucide="save" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ
</button>
<button type="button" onclick="closeModal()" class="btn btn-outline" style="padding:10px 20px;">إلغاء</button>
</div>
</form>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
const GRID_ID = <?= (int) $grid->id ?>;
const ROWS = <?= $rows ?>;
const COLS = <?= $cols ?>;
const CSRF = document.querySelector('meta[name="csrf-token"]')?.content || '';
const PLAN_MONTH = '<?= e($planMonth) ?>';
let selectedHour = <?= $currentHour ?>;
let gridState = <?= json_encode($state, JSON_UNESCAPED_UNICODE) ?>;
let draggedTraineeId = null;
// ─── HOUR SELECTION ───
function selectHour(h) {
selectedHour = h;
document.querySelectorAll('.hour-tab').forEach(t => t.classList.remove('active'));
document.querySelector(`.hour-tab[data-hour="${h}"]`)?.classList.add('active');
document.getElementById('selectedHourLabel').textContent =
String(h).padStart(2,'0') + ':00–' + String(h+1).padStart(2,'0') + ':00';
refreshGrid();
}
function refreshGrid() {
const date = document.getElementById('gridDate').value;
fetch(`/api/facility-grids/${GRID_ID}/state?date=${date}&time=${String(selectedHour).padStart(2,'0')}:30`)
.then(r => r.json())
.then(data => {
if (data.success) {
gridState = data.data;
renderPoolGrid();
}
});
}
function renderPoolGrid() {
const zones = gridState.zones || {};
document.querySelectorAll('.pool-box').forEach(box => {
const r = parseInt(box.dataset.row);
const c = parseInt(box.dataset.col);
const key = r + ':' + c;
const zone = zones[key];
if (!zone) return;
const sch = zone.schedule;
const isOccupied = zone.status === 'occupied' && sch;
box.className = 'pool-box ' + (isOccupied ? 'occupied' : 'available');
if (isOccupied) box.style.setProperty('--box-color', sch.color || '#3B82F6');
let html = '';
if (isOccupied) {
const count = zone.trainee_count || 0;
const max = zone.max_occupants || 5;
if (count > 0) html += `<span class="box-count">${count}/${max}</span>`;
html += `<div class="box-header">
<span class="box-coach">${sch.coach || sch.academy || sch.name}</span>
<span class="box-badge ${sch.gender || 'male'}">${(sch.gender||'male')==='female'?'بنات':'ولاد'}</span>
</div>`;
html += `<div class="box-meta">${sch.age_group ? sch.age_group + ' • ' : ''}${sch.activity_type||''}</div>`;
if (zone.trainees && zone.trainees.length) {
html += '<div class="trainee-list">';
zone.trainees.forEach(t => {
html += `<div class="trainee-item" draggable="true" data-trainee-id="${t.id}" ondragstart="dragStart(event,${t.id})">
<span>${t.name}</span>
<button class="remove-btn" onclick="event.stopPropagation();removeTrainee(${t.id})" title="إزالة">
<i data-lucide="x" style="width:12px;height:12px;"></i>
</button>
</div>`;
});
html += '</div>';
}
} else {
html = '<div class="box-empty"><i data-lucide="plus-circle" style="width:20px;height:20px;margin-bottom:4px;"></i><span>متاح</span></div>';
}
box.innerHTML = html;
});
lucide.createIcons();
}
// ─── MONTH NAVIGATION ───
function changeMonth(delta) {
const parts = PLAN_MONTH.split('-');
let y = parseInt(parts[0]), m = parseInt(parts[1]) + delta;
if (m > 12) { m = 1; y++; }
if (m < 1) { m = 12; y--; }
const newMonth = y + '-' + String(m).padStart(2, '0');
window.location.href = `/facility-grids/${GRID_ID}?month=${newMonth}&date=${newMonth}-01`;
}
function reloadForDate() {
const date = document.getElementById('gridDate').value;
window.location.href = `/facility-grids/${GRID_ID}?date=${date}&month=${PLAN_MONTH}`;
}
// ─── BOX MODAL ───
function openBoxModal(row, col, el) {
const key = row + ':' + col;
const zone = gridState.zones[key];
document.getElementById('modalSelJson').value = JSON.stringify([{row, col}]);
document.getElementById('modalStart').value = String(selectedHour).padStart(2,'0') + ':00';
document.getElementById('modalEnd').value = String(selectedHour + 1).padStart(2,'0') + ':00';
if (zone && zone.schedule) {
document.getElementById('modalTitle').textContent = 'تعديل خانة';
document.getElementById('modalName').value = zone.schedule.name || '';
document.getElementById('modalActivity').value = zone.schedule.activity_type || '';
document.getElementById('modalAge').value = zone.schedule.age_group || '';
document.getElementById('modalGender').value = zone.schedule.gender || 'male';
document.getElementById('modalColor').value = zone.schedule.color || '#3B82F6';
} else {
document.getElementById('modalTitle').textContent = 'تعيين خانة جديدة';
document.getElementById('boxForm').reset();
document.getElementById('modalSelJson').value = JSON.stringify([{row, col}]);
document.getElementById('modalStart').value = String(selectedHour).padStart(2,'0') + ':00';
document.getElementById('modalEnd').value = String(selectedHour + 1).padStart(2,'0') + ':00';
}
document.getElementById('boxModal').classList.add('active');
}
function closeModal() {
document.getElementById('boxModal').classList.remove('active');
}
function saveBox(e) {
e.preventDefault();
const form = document.getElementById('boxForm');
const formData = new FormData(form);
formData.append('_token', CSRF);
formData.set('coach_name', formData.get('schedule_name'));
fetch(`/facility-grids/${GRID_ID}/schedules`, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeModal();
refreshGrid();
} else {
alert(data.message || 'حدث خطأ');
}
});
}
// ─── TRAINEES ───
function addTraineeToBox() {
const name = document.getElementById('newTraineeName').value.trim();
if (!name) return;
const selJson = document.getElementById('modalSelJson').value;
const cells = JSON.parse(selJson);
if (!cells.length) return;
const key = cells[0].row + ':' + cells[0].col;
const zone = gridState.zones[key];
if (!zone) return;
const formData = new FormData();
formData.append('_token', CSRF);
formData.append('zone_id', zone.zone_id);
formData.append('trainee_name', name);
fetch(`/facility-grids/${GRID_ID}/trainees`, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
document.getElementById('newTraineeName').value = '';
refreshGrid();
} else {
alert(data.message || 'خطأ');
}
});
}
function removeTrainee(traineeId) {
if (!confirm('إزالة المتدرب؟')) return;
const formData = new FormData();
formData.append('_token', CSRF);
fetch(`/facility-grids/${GRID_ID}/trainees/${traineeId}/remove`, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
})
.then(r => r.json())
.then(data => { if (data.success) refreshGrid(); });
}
// ─── DRAG & DROP (Move Trainee) ───
function dragStart(e, traineeId) {
draggedTraineeId = traineeId;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', traineeId);
e.stopPropagation();
}
function dragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('drag-over');
}
function dragLeave(e) {
e.currentTarget.classList.remove('drag-over');
}
function dropTrainee(e, targetZoneId) {
e.preventDefault();
e.currentTarget.classList.remove('drag-over');
if (!draggedTraineeId || !targetZoneId) return;
const formData = new FormData();
formData.append('_token', CSRF);
formData.append('target_zone_id', targetZoneId);
fetch(`/facility-grids/${GRID_ID}/trainees/${draggedTraineeId}/move`, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) refreshGrid();
else alert(data.message || 'لا يمكن النقل');
});
draggedTraineeId = null;
}
// Auto-refresh every 30s
setInterval(refreshGrid, 30000);
</script>
<?php $__template->endSection(); ?>
<?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 [
['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'],
// Pool Grid (interactive)
['GET', '/pool/{id:\d+}/grid', 'PoolManagement\Controllers\PoolGridController@grid', ['auth'], 'pool.view'],
['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 Grid & Schedules — migrated to unified FacilityGrids module
// /pool/{id}/grid and /pool/{id}/schedules now redirect via FacilityGrids\Controllers\RedirectController
// Pool Bookings
['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