Commit 5c9ce3c8 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Pool management complete overhaul: interactive 2D grid, schedules, dashboard

Grid rewrite:
- Fully interactive lanes×timeslots grid with click/drag cell selection
- Click individual cells, drag-select rectangles, row headers (full lane),
  column headers (full time slot)
- Inline booking panel slides up from bottom when cells are selected —
  no separate page needed, book directly from the grid
- Recurring schedules shown as purple cells on the grid
- Real-time refresh every 30s via AJAX
- Visual pool dimensions display with lane numbers

New: Weekly Schedules (pool_schedules):
- Create recurring weekly templates (coach, academy, age group, gender, lanes)
- Effective date range, color-coded, max swimmers per schedule
- Auto-displayed on the grid for the matching day of week
- Manage page with create form + list of all schedules with delete

New: Pool Dashboard (/pool/{id}/dashboard):
- KPI cards: today/week/month bookings, swimmers, revenue, utilization %
- Heatmap: bookings per hour per day of week (last 30 days) with color intensity
- Booking type breakdown with progress bars
- Lane utilization bars (hours used vs available)
- Peak hours chart with AI recommendations for increasing occupancy
- Low-utilization detection + suggestions

Updated pool index:
- Cards with mini pool visual, 3 action buttons (grid, dashboard, schedules)
- Better visual hierarchy and information density
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d11abbce
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
use App\Modules\PoolManagement\Services\PoolOccupancyOptimizer;
class PoolDashboardController extends Controller
{
public function show(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$db = App::getInstance()->db();
$today = date('Y-m-d');
$weekStart = date('Y-m-d', strtotime('monday this week'));
$weekEnd = date('Y-m-d', strtotime('sunday this week'));
$monthStart = date('Y-m-01');
$monthEnd = date('Y-m-t');
// Today stats
$todayStats = $db->selectOne(
"SELECT COUNT(*) AS bookings, COALESCE(SUM(expected_swimmers), 0) AS swimmers, COALESCE(SUM(total_amount), 0) AS revenue
FROM pool_bookings WHERE pool_config_id = ? AND booking_date = ? AND status NOT IN ('cancelled')",
[(int) $id, $today]
);
// Week stats
$weekStats = $db->selectOne(
"SELECT COUNT(*) AS bookings, COALESCE(SUM(expected_swimmers), 0) AS swimmers, COALESCE(SUM(total_amount), 0) AS revenue
FROM pool_bookings WHERE pool_config_id = ? AND booking_date BETWEEN ? AND ? AND status NOT IN ('cancelled')",
[(int) $id, $weekStart, $weekEnd]
);
// Month stats
$monthStats = $db->selectOne(
"SELECT COUNT(*) AS bookings, COALESCE(SUM(expected_swimmers), 0) AS swimmers, COALESCE(SUM(total_amount), 0) AS revenue
FROM pool_bookings WHERE pool_config_id = ? AND booking_date BETWEEN ? AND ? AND status NOT IN ('cancelled')",
[(int) $id, $monthStart, $monthEnd]
);
// Heatmap data: bookings per hour per day of week (last 30 days)
$heatmapRaw = $db->select(
"SELECT DAYOFWEEK(booking_date) AS dow, HOUR(start_time) AS hr, COUNT(*) AS cnt
FROM pool_bookings
WHERE pool_config_id = ? AND booking_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND status NOT IN ('cancelled')
GROUP BY dow, hr",
[(int) $id]
);
$heatmap = [];
foreach ($heatmapRaw as $row) {
$heatmap[(int) $row['dow']][(int) $row['hr']] = (int) $row['cnt'];
}
// Booking type breakdown (this month)
$typeBreakdown = $db->select(
"SELECT booking_type, COUNT(*) AS cnt, COALESCE(SUM(expected_swimmers), 0) AS swimmers
FROM pool_bookings
WHERE pool_config_id = ? AND booking_date BETWEEN ? AND ? AND status NOT IN ('cancelled')
GROUP BY booking_type ORDER BY cnt DESC",
[(int) $id, $monthStart, $monthEnd]
);
// Lane utilization (this month)
$laneUtilRaw = $db->select(
"SELECT lanes_json, TIMESTAMPDIFF(MINUTE, start_time, end_time) AS dur_min
FROM pool_bookings
WHERE pool_config_id = ? AND booking_date BETWEEN ? AND ? AND status NOT IN ('cancelled')",
[(int) $id, $monthStart, $monthEnd]
);
$lanes = $pool->getLanes();
$laneMinutes = [];
foreach ($lanes as $l) $laneMinutes[(int) $l['lane_number']] = 0;
foreach ($laneUtilRaw as $row) {
$bookedLanes = json_decode($row['lanes_json'] ?? '[]', true);
if (is_array($bookedLanes)) {
foreach ($bookedLanes as $ln) {
if (isset($laneMinutes[$ln])) {
$laneMinutes[$ln] += (int) $row['dur_min'];
}
}
}
}
$hours = $pool->getOperatingHours();
$dailyMinutes = (strtotime($hours['end'] ?? '22:00') - strtotime($hours['start'] ?? '06:00')) / 60;
$daysInMonth = (int) date('t');
$totalAvailableMinutes = $dailyMinutes * $daysInMonth;
// Peak hours (top 5 busiest hours this month)
$peakHours = $db->select(
"SELECT HOUR(start_time) AS hr, COUNT(*) AS cnt
FROM pool_bookings
WHERE pool_config_id = ? AND booking_date BETWEEN ? AND ? AND status NOT IN ('cancelled')
GROUP BY hr ORDER BY cnt DESC LIMIT 5",
[(int) $id, $monthStart, $monthEnd]
);
return $this->view('PoolManagement.Views.dashboard', [
'pool' => $pool,
'lanes' => $lanes,
'todayStats' => $todayStats,
'weekStats' => $weekStats,
'monthStats' => $monthStats,
'heatmap' => $heatmap,
'typeBreakdown' => $typeBreakdown,
'laneMinutes' => $laneMinutes,
'totalAvailableMinutes' => $totalAvailableMinutes,
'peakHours' => $peakHours,
'bookingTypes' => PoolConfiguration::getBookingTypes(),
]);
}
}
......@@ -6,7 +6,10 @@ namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
use App\Modules\PoolManagement\Models\PoolBooking;
use App\Modules\PoolManagement\Models\PoolSchedule;
use App\Modules\PoolManagement\Services\PoolGridService;
use App\Modules\PoolManagement\Services\PoolOccupancyOptimizer;
......@@ -22,16 +25,89 @@ class PoolGridController extends Controller
$date = trim((string) $request->get('date', date('Y-m-d')));
$gridState = PoolGridService::getGridState((int) $id, $date);
$utilization = PoolOccupancyOptimizer::getUtilizationMetrics((int) $id, $date, $date);
$schedules = PoolSchedule::getActiveForPool((int) $id, $date);
return $this->view('PoolManagement.Views.grid', [
'pool' => $pool,
'gridState' => $gridState,
'utilization' => $utilization,
'date' => $date,
'schedules' => $schedules,
'bookingTypes'=> PoolConfiguration::getBookingTypes(),
]);
}
public function bookCells(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$date = trim((string) $request->post('booking_date', date('Y-m-d')));
$cellsJson = trim((string) $request->post('cells_json', '[]'));
$bookingType = trim((string) $request->post('booking_type', 'free_swim'));
$bookerName = trim((string) $request->post('booker_name', ''));
$swimmers = max(1, (int) $request->post('expected_swimmers', 1));
$notes = trim((string) $request->post('notes', ''));
$cells = json_decode($cellsJson, true);
if (!is_array($cells) || empty($cells)) {
return $this->redirect('/pool/' . $id . '/grid?date=' . $date)->withError('يجب تحديد خلايا من الشبكة');
}
if ($bookerName === '') {
return $this->redirect('/pool/' . $id . '/grid?date=' . $date)->withError('اسم الحاجز مطلوب');
}
// Group cells by contiguous time ranges per lane set
$laneSet = [];
$minTime = '99:99';
$maxTime = '00:00';
foreach ($cells as $cell) {
$lane = (int) ($cell['lane'] ?? 0);
$start = $cell['start'] ?? '';
$end = $cell['end'] ?? '';
if ($lane > 0) $laneSet[$lane] = true;
if ($start < $minTime) $minTime = $start;
if ($end > $maxTime) $maxTime = $end;
}
$selectedLanes = array_keys($laneSet);
if (empty($selectedLanes) || $minTime >= $maxTime) {
return $this->redirect('/pool/' . $id . '/grid?date=' . $date)->withError('تحديد غير صالح');
}
// Check availability
$availability = PoolGridService::checkLaneAvailability((int) $id, $date, $minTime, $maxTime, $selectedLanes);
if (!$availability['available']) {
return $this->redirect('/pool/' . $id . '/grid?date=' . $date)->withError('بعض الخلايا المحددة غير متاحة');
}
$selectionMode = count($selectedLanes) === count($pool->getLanes()) ? 'full_pool' : (count($selectedLanes) === 1 ? 'lane' : 'multi_lane');
$bookingCode = 'PB-' . date('ymd') . '-' . strtoupper(bin2hex(random_bytes(3)));
PoolBooking::create([
'booking_code' => $bookingCode,
'pool_config_id' => (int) $id,
'booking_date' => $date,
'start_time' => $minTime,
'end_time' => $maxTime,
'booking_type' => $bookingType,
'selection_mode' => $selectionMode,
'lanes_json' => json_encode($selectedLanes),
'booker_type' => 'member',
'booker_name' => $bookerName,
'expected_swimmers' => $swimmers,
'unit_rate' => 0,
'total_amount' => 0,
'payment_status' => 'pending',
'status' => 'confirmed',
'notes' => $notes ?: null,
]);
return $this->redirect('/pool/' . $id . '/grid?date=' . $date)->withSuccess('تم الحجز — كود: ' . $bookingCode);
}
public function apiState(Request $request, string $id): Response
{
$date = trim((string) $request->get('date', date('Y-m-d')));
......
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
use App\Modules\PoolManagement\Models\PoolSchedule;
class PoolScheduleController extends Controller
{
public function index(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$schedules = PoolSchedule::getAllForPool((int) $id);
return $this->view('PoolManagement.Views.schedules', [
'pool' => $pool,
'schedules' => $schedules,
]);
}
public function store(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$name = trim((string) $request->post('schedule_name', ''));
$dayOfWeek = (int) $request->post('day_of_week', 0);
$startTime = trim((string) $request->post('start_time', ''));
$endTime = trim((string) $request->post('end_time', ''));
$lanes = $request->post('lanes', []);
$bookingType = trim((string) $request->post('booking_type', 'academy_session'));
$coachName = trim((string) $request->post('coach_name', '')) ?: null;
$ageGroup = trim((string) $request->post('age_group', '')) ?: null;
$gender = trim((string) $request->post('gender', 'mixed'));
$maxSwimmers = max(1, (int) $request->post('max_swimmers', 8));
$effectiveFrom = trim((string) $request->post('effective_from', date('Y-m-d')));
$effectiveTo = trim((string) $request->post('effective_to', '')) ?: null;
$color = trim((string) $request->post('color', '#8B5CF6'));
if (!is_array($lanes)) $lanes = [];
$lanes = array_map('intval', $lanes);
$errors = [];
if (empty($name)) $errors[] = 'اسم الجدول مطلوب';
if (empty($startTime) || empty($endTime)) $errors[] = 'الوقت مطلوب';
if ($startTime >= $endTime) $errors[] = 'وقت البداية يجب أن يكون قبل النهاية';
if (empty($lanes)) $errors[] = 'يجب اختيار حارة واحدة على الأقل';
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
return $this->redirect('/pool/' . $id . '/schedules');
}
PoolSchedule::create([
'pool_config_id' => (int) $id,
'schedule_name' => $name,
'day_of_week' => $dayOfWeek,
'start_time' => $startTime,
'end_time' => $endTime,
'lanes_json' => json_encode($lanes),
'booking_type' => $bookingType,
'coach_name' => $coachName,
'age_group' => $ageGroup,
'gender' => $gender,
'max_swimmers' => $maxSwimmers,
'color' => $color,
'effective_from' => $effectiveFrom,
'effective_to' => $effectiveTo,
'is_active' => 1,
]);
return $this->redirect('/pool/' . $id . '/schedules')->withSuccess('تم إنشاء الجدول الأسبوعي بنجاح');
}
public function delete(Request $request, string $poolId, string $scheduleId): Response
{
$db = App::getInstance()->db();
$db->update('pool_schedules', ['is_active' => 0], 'id = ? AND pool_config_id = ?', [(int) $scheduleId, (int) $poolId]);
return $this->redirect('/pool/' . $poolId . '/schedules')->withSuccess('تم حذف الجدول');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Models;
use App\Core\Model;
use App\Core\App;
class PoolSchedule extends Model
{
protected static string $table = 'pool_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 = [
'pool_config_id',
'schedule_name',
'day_of_week',
'start_time',
'end_time',
'lanes_json',
'booking_type',
'coach_id',
'coach_name',
'academy_id',
'academy_name',
'age_group',
'gender',
'max_swimmers',
'color',
'effective_from',
'effective_to',
'is_active',
'notes',
];
public function getLanes(): array
{
$decoded = json_decode($this->lanes_json ?? '[]', true);
return is_array($decoded) ? $decoded : [];
}
public static function getActiveForPool(int $poolConfigId, ?string $date = null): array
{
$db = App::getInstance()->db();
$date = $date ?: date('Y-m-d');
$dayOfWeek = (int) date('w', strtotime($date));
return $db->select(
"SELECT * FROM pool_schedules
WHERE pool_config_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",
[$poolConfigId, $dayOfWeek, $date, $date]
);
}
public static function getAllForPool(int $poolConfigId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT ps.*, c.full_name_ar AS coach_full_name, a.name_ar AS acad_name
FROM pool_schedules ps
LEFT JOIN coaches c ON c.id = ps.coach_id
LEFT JOIN academies a ON a.id = ps.academy_id
WHERE ps.pool_config_id = ? AND ps.is_active = 1
ORDER BY ps.day_of_week ASC, ps.start_time ASC",
[$poolConfigId]
);
}
public static function getDayNames(): array
{
return [
0 => 'الأحد',
1 => 'الإثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
6 => 'السبت',
];
}
}
......@@ -2,17 +2,31 @@
declare(strict_types=1);
return [
['GET', '/pool', 'PoolManagement\Controllers\PoolConfigController@index', ['auth'], 'pool.view'],
['GET', '/pool/config/create', 'PoolManagement\Controllers\PoolConfigController@create', ['auth'], 'pool.manage'],
['POST', '/pool/config', 'PoolManagement\Controllers\PoolConfigController@store', ['auth', 'csrf'], 'pool.manage'],
['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'],
['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'],
['GET', '/pool/{id:\d+}/grid', 'PoolManagement\Controllers\PoolGridController@grid', ['auth'], 'pool.view'],
['GET', '/api/pool/{id:\d+}/state', 'PoolManagement\Controllers\PoolGridController@apiState', ['auth'], 'pool.view'],
['GET', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@create', ['auth'], 'pool.book'],
['POST', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@store', ['auth', 'csrf'], 'pool.book'],
['GET', '/pool/bookings', 'PoolManagement\Controllers\PoolBookingController@index', ['auth'], 'pool.view'],
['GET', '/pool/bookings/{id:\d+}', 'PoolManagement\Controllers\PoolBookingController@show', ['auth'], 'pool.view'],
['POST', '/pool/bookings/{id:\d+}/cancel','PoolManagement\Controllers\PoolBookingController@cancel', ['auth', 'csrf'], 'pool.manage'],
['POST', '/pool/bookings/{id:\d+}/checkin','PoolManagement\Controllers\PoolBookingController@checkin', ['auth', 'csrf'], 'pool.manage'],
// Pool Configuration
['GET', '/pool', 'PoolManagement\Controllers\PoolConfigController@index', ['auth'], 'pool.view'],
['GET', '/pool/config/create', 'PoolManagement\Controllers\PoolConfigController@create', ['auth'], 'pool.manage'],
['POST', '/pool/config', 'PoolManagement\Controllers\PoolConfigController@store', ['auth', 'csrf'], 'pool.manage'],
['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'],
['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'],
// 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 Bookings
['GET', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@create', ['auth'], 'pool.book'],
['POST', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@store', ['auth', 'csrf'], 'pool.book'],
['GET', '/pool/bookings', 'PoolManagement\Controllers\PoolBookingController@index', ['auth'], 'pool.view'],
['GET', '/pool/bookings/{id:\d+}', 'PoolManagement\Controllers\PoolBookingController@show', ['auth'], 'pool.view'],
['POST', '/pool/bookings/{id:\d+}/cancel', 'PoolManagement\Controllers\PoolBookingController@cancel', ['auth', 'csrf'], 'pool.manage'],
['POST', '/pool/bookings/{id:\d+}/checkin', 'PoolManagement\Controllers\PoolBookingController@checkin', ['auth', 'csrf'], 'pool.manage'],
// Pool Dashboard
['GET', '/pool/{id:\d+}/dashboard', 'PoolManagement\Controllers\PoolDashboardController@show', ['auth'], 'pool.view'],
];
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>لوحة تحكم: <?= e($pool->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/<?= (int) $pool->id ?>/grid" class="btn btn-primary"><i data-lucide="grid-3x3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الشبكة التفاعلية</a>
<a href="/pool/<?= (int) $pool->id ?>/schedules" class="btn btn-outline"><i data-lucide="repeat" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الجداول</a>
<a href="/pool" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$dayNames = [1 => 'الأحد', 2 => 'الإثنين', 3 => 'الثلاثاء', 4 => 'الأربعاء', 5 => 'الخميس', 6 => 'الجمعة', 7 => 'السبت'];
$hours = $pool->getOperatingHours();
$startHr = (int) substr($hours['start'] ?? '06:00', 0, 2);
$endHr = (int) substr($hours['end'] ?? '22:00', 0, 2);
?>
<!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #0D7377;">
<div style="font-size:28px;font-weight:700;color:#0D7377;"><?= (int) $todayStats['bookings'] ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">حجوزات اليوم</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;"><?= (int) $todayStats['swimmers'] ?> سبّاح</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #2563EB;">
<div style="font-size:28px;font-weight:700;color:#2563EB;"><?= (int) $weekStats['bookings'] ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">حجوزات الأسبوع</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;"><?= (int) $weekStats['swimmers'] ?> سبّاح</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #059669;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= number_format((float) $monthStats['revenue'], 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">إيرادات الشهر (ج.م)</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;"><?= (int) $monthStats['bookings'] ?> حجز</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #D97706;">
<?php
$totalLaneMin = array_sum($laneMinutes);
$totalPoolMin = $totalAvailableMinutes * count($lanes);
$utilPct = $totalPoolMin > 0 ? round(($totalLaneMin / $totalPoolMin) * 100) : 0;
?>
<div style="font-size:28px;font-weight:700;color:#D97706;"><?= $utilPct ?>%</div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">نسبة الاستخدام (الشهر)</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;"><?= round($totalLaneMin / 60) ?> ساعة مستخدمة</div>
</div>
</div>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Heatmap -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="flame" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">خريطة الازدحام (آخر 30 يوم)</h3>
</div>
<div style="padding:15px;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:10px;">
<thead>
<tr>
<th style="padding:4px;text-align:right;font-size:10px;color:#9CA3AF;"></th>
<?php for ($h = $startHr; $h < $endHr; $h++): ?>
<th style="padding:4px;text-align:center;font-size:10px;color:#6B7280;direction:ltr;"><?= sprintf('%02d', $h) ?></th>
<?php endfor; ?>
</tr>
</thead>
<tbody>
<?php foreach ($dayNames as $dow => $dayName): ?>
<tr>
<td style="padding:4px 8px;font-size:11px;color:#374151;font-weight:600;white-space:nowrap;"><?= e($dayName) ?></td>
<?php for ($h = $startHr; $h < $endHr; $h++):
$cnt = $heatmap[$dow][$h] ?? 0;
$maxCnt = 10;
$intensity = min(1, $cnt / $maxCnt);
if ($cnt === 0) {
$bg = '#F3F4F6';
$fg = '#9CA3AF';
} elseif ($intensity < 0.3) {
$bg = '#D1FAE5';
$fg = '#059669';
} elseif ($intensity < 0.6) {
$bg = '#FEF3C7';
$fg = '#D97706';
} elseif ($intensity < 0.85) {
$bg = '#FED7AA';
$fg = '#EA580C';
} else {
$bg = '#FEE2E2';
$fg = '#DC2626';
}
?>
<td style="padding:2px;">
<div style="width:100%;height:28px;background:<?= $bg ?>;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:<?= $fg ?>;" title="<?= e($dayName) ?> <?= sprintf('%02d:00', $h) ?><?= $cnt ?> حجز">
<?= $cnt > 0 ? $cnt : '' ?>
</div>
</td>
<?php endfor; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div style="margin-top:10px;display:flex;gap:8px;align-items:center;font-size:10px;color:#6B7280;">
<span>أقل</span>
<span style="width:16px;height:12px;background:#F3F4F6;border-radius:2px;display:inline-block;"></span>
<span style="width:16px;height:12px;background:#D1FAE5;border-radius:2px;display:inline-block;"></span>
<span style="width:16px;height:12px;background:#FEF3C7;border-radius:2px;display:inline-block;"></span>
<span style="width:16px;height:12px;background:#FED7AA;border-radius:2px;display:inline-block;"></span>
<span style="width:16px;height:12px;background:#FEE2E2;border-radius:2px;display:inline-block;"></span>
<span>أكثر</span>
</div>
</div>
</div>
<!-- Breakdown by Type -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="pie-chart" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">حسب نوع النشاط</h3>
</div>
<div style="padding:15px 20px;">
<?php if (!empty($typeBreakdown)):
$typeColors = ['lap_swimming'=>'#0D7377','lessons'=>'#2563EB','free_swim'=>'#059669','academy_session'=>'#7C3AED','competition'=>'#D97706','maintenance'=>'#EF4444'];
$totalBookings = array_sum(array_column($typeBreakdown, 'cnt'));
?>
<?php foreach ($typeBreakdown as $tb):
$pct = $totalBookings > 0 ? round(((int)$tb['cnt'] / $totalBookings) * 100) : 0;
$color = $typeColors[$tb['booking_type']] ?? '#6B7280';
?>
<div style="margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;">
<span style="color:#374151;font-weight:600;"><?= e($bookingTypes[$tb['booking_type']] ?? $tb['booking_type']) ?></span>
<span style="color:#6B7280;"><?= (int) $tb['cnt'] ?> حجز (<?= (int) $tb['swimmers'] ?> سبّاح)</span>
</div>
<div style="height:8px;background:#F3F4F6;border-radius:4px;overflow:hidden;">
<div style="height:100%;width:<?= $pct ?>%;background:<?= $color ?>;border-radius:4px;transition:width 0.5s;"></div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="color:#9CA3AF;font-size:13px;text-align:center;padding:20px 0;">لا توجد بيانات هذا الشهر</p>
<?php endif; ?>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Lane Utilization -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="bar-chart-3" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">استخدام الحارات (هذا الشهر)</h3>
</div>
<div style="padding:20px;">
<?php foreach ($lanes as $lane):
$laneNum = (int) $lane['lane_number'];
$minutes = $laneMinutes[$laneNum] ?? 0;
$lanePct = $totalAvailableMinutes > 0 ? round(($minutes / $totalAvailableMinutes) * 100) : 0;
$laneHrs = round($minutes / 60, 1);
$barColor = $lanePct > 70 ? '#059669' : ($lanePct > 40 ? '#D97706' : '#EF4444');
?>
<div style="margin-bottom:14px;">
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;">
<span style="font-weight:600;color:#1A1A2E;"><?= e($lane['label_ar'] ?? ('حارة ' . $laneNum)) ?></span>
<span style="color:#6B7280;"><?= $laneHrs ?> ساعة — <?= $lanePct ?>%</span>
</div>
<div style="height:12px;background:#F3F4F6;border-radius:6px;overflow:hidden;">
<div style="height:100%;width:<?= $lanePct ?>%;background:<?= $barColor ?>;border-radius:6px;transition:width 0.5s;"></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Peak Hours + Recommendations -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="trending-up" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">أوقات الذروة</h3>
</div>
<div style="padding:20px;">
<?php if (!empty($peakHours)): ?>
<?php foreach ($peakHours as $i => $ph):
$hr = (int) $ph['hr'];
$barW = (int) $ph['cnt'];
$maxBar = (int) ($peakHours[0]['cnt'] ?? 1);
$pctBar = round(($barW / max($maxBar, 1)) * 100);
?>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<span style="min-width:50px;font-size:13px;font-weight:700;color:#1A1A2E;direction:ltr;"><?= sprintf('%02d:00', $hr) ?></span>
<div style="flex:1;height:20px;background:#F3F4F6;border-radius:6px;overflow:hidden;">
<div style="height:100%;width:<?= $pctBar ?>%;background:linear-gradient(90deg, #DC2626, #F59E0B);border-radius:6px;"></div>
</div>
<span style="font-size:12px;color:#6B7280;min-width:40px;"><?= (int) $ph['cnt'] ?> حجز</span>
</div>
<?php endforeach; ?>
<div style="margin-top:20px;padding:12px;background:#FEF3C7;border:1px solid #FDE68A;border-radius:8px;">
<div style="font-size:12px;font-weight:600;color:#92400E;margin-bottom:6px;"><i data-lucide="lightbulb" style="width:13px;height:13px;vertical-align:middle;margin-left:4px;"></i> توصيات لزيادة الإشغال</div>
<ul style="margin:0;padding:0 20px;font-size:11px;color:#92400E;line-height:1.8;">
<?php
$lowHours = [];
for ($h = $startHr; $h < $endHr; $h++) {
$found = false;
foreach ($peakHours as $ph) { if ((int)$ph['hr'] === $h) { $found = true; break; } }
if (!$found) $lowHours[] = sprintf('%02d:00', $h);
}
if (count($lowHours) > 3): ?>
<li>أوقات منخفضة الإشغال: <?= implode('، ', array_slice($lowHours, 0, 4)) ?> — ننصح بخصومات أو دروس مجانية</li>
<?php endif; ?>
<?php if ($utilPct < 50): ?>
<li>الإشغال الكلي <?= $utilPct ?>% — إمكانية إضافة حصص أكاديمية أو فتح حجز مؤسسي</li>
<?php endif; ?>
<li>استخدم الجداول الأسبوعية لضمان استمرارية الحجوزات في الأوقات الهادئة</li>
</ul>
</div>
<?php else: ?>
<p style="color:#9CA3AF;font-size:13px;text-align:center;padding:20px 0;">لا توجد بيانات كافية</p>
<?php endif; ?>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
......@@ -2,7 +2,7 @@
<?php $__template->section('title'); ?>حمام السباحة: <?= e($pool->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/<?= (int) $pool->id ?>/book?date=<?= e($date) ?>" class="btn btn-primary"><i data-lucide="plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حجز جديد</a>
<a href="/pool/<?= (int) $pool->id ?>/schedules" class="btn btn-outline"><i data-lucide="repeat" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الجداول الأسبوعية</a>
<a href="/pool/config/<?= (int) $pool->id ?>/edit" class="btn btn-outline"><i data-lucide="settings" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الإعدادات</a>
<a href="/pool" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة</a>
<?php $__template->endSection(); ?>
......@@ -13,130 +13,157 @@
$lanes = $gridState['lanes'] ?? [];
$timeSlots = $gridState['time_slots'] ?? [];
$cells = $gridState['cells'] ?? [];
$schedules = $schedules ?? [];
$statusColors = [
'available' => '#10B981',
'booked' => '#3B82F6',
'in_progress' => '#F59E0B',
'maintenance' => '#EF4444',
'available' => '#10B981',
'booked' => '#3B82F6',
'in_progress' => '#F59E0B',
'maintenance' => '#EF4444',
'scheduled' => '#8B5CF6',
];
$bookingTypeLabels = [
'lap_swimming' => 'سباحة حرة',
'lessons' => 'دروس',
'free_swim' => 'ترفيهية',
'academy_session' => 'أكاديمية',
'competition' => 'مسابقة',
'maintenance' => 'صيانة',
];
?>
<!-- Date Selector + Stats -->
<div style="display:flex;gap:15px;margin-bottom:20px;align-items:stretch;">
<!-- Date Navigation -->
<div class="card" style="padding:15px;display:flex;align-items:center;gap:10px;flex:0 0 auto;">
<style>
.pool-grid-wrap { user-select:none; }
.pool-grid { border-collapse:collapse; width:100%; }
.pool-grid th, .pool-grid td { padding:0; margin:0; }
.pool-grid th { font-size:11px; color:#6B7280; font-weight:600; }
.pool-cell {
width:100%; height:52px; border:1px solid #E5E7EB; border-radius:4px;
cursor:pointer; transition:all 0.12s; position:relative;
display:flex; align-items:center; justify-content:center;
font-size:10px; font-weight:600; margin:1px;
}
.pool-cell:hover { transform:scale(1.04); z-index:2; box-shadow:0 2px 8px rgba(0,0,0,0.15); }
.pool-cell.selected { outline:3px solid #0D7377; outline-offset:-1px; z-index:3; }
.pool-cell.available { background:#10B98115; border-color:#10B98140; color:#059669; }
.pool-cell.booked { background:#3B82F615; border-color:#3B82F640; color:#2563EB; cursor:default; }
.pool-cell.in_progress { background:#F59E0B15; border-color:#F59E0B40; color:#D97706; cursor:default; }
.pool-cell.maintenance { background:#EF444415; border-color:#EF444440; color:#DC2626; cursor:default; }
.pool-cell.scheduled { background:#8B5CF615; border-color:#8B5CF640; color:#7C3AED; cursor:default; }
.pool-cell .cell-label { font-size:9px; position:absolute; bottom:2px; left:2px; right:2px; text-align:center; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.pool-cell .cell-count { font-size:14px; font-weight:700; }
.lane-header { cursor:pointer; padding:8px 6px !important; border-radius:6px; transition:background 0.15s; text-align:center; }
.lane-header:hover { background:#F0FDFA; }
.time-header { cursor:pointer; padding:6px 2px !important; border-radius:6px; transition:background 0.15s; min-width:60px; text-align:center; }
.time-header:hover { background:#F0FDFA; }
.booking-panel { position:fixed; bottom:0; left:0; right:0; background:#fff; box-shadow:0 -4px 20px rgba(0,0,0,0.15); padding:20px 30px; z-index:100; transform:translateY(100%); transition:transform 0.3s ease; border-top:3px solid #0D7377; }
.booking-panel.visible { transform:translateY(0); }
.selection-info { display:flex; gap:20px; align-items:center; flex-wrap:wrap; }
.selection-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; background:#0D737715; border:1px solid #0D737740; border-radius:20px; font-size:13px; color:#0D7377; font-weight:600; }
</style>
<!-- Date Navigation + Stats Bar -->
<div style="display:flex;gap:12px;margin-bottom:15px;align-items:stretch;">
<div class="card" style="padding:12px 16px;display:flex;align-items:center;gap:8px;flex:0 0 auto;">
<?php
$prevDate = date('Y-m-d', strtotime($date . ' -1 day'));
$nextDate = date('Y-m-d', strtotime($date . ' +1 day'));
$dayNames = ['الأحد','الإثنين','الثلاثاء','الأربعاء','الخميس','الجمعة','السبت'];
$dayName = $dayNames[(int) date('w', strtotime($date))];
?>
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= $prevDate ?>" class="btn btn-sm btn-outline">&rarr;</a>
<form method="GET" action="/pool/<?= (int) $pool->id ?>/grid" style="display:flex;gap:8px;">
<input type="date" name="date" value="<?= e($date) ?>" class="form-input" style="direction:ltr;font-size:13px;" onchange="this.form.submit()">
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= $prevDate ?>" class="btn btn-sm btn-outline" style="padding:4px 8px;">&rarr;</a>
<form method="GET" action="/pool/<?= (int) $pool->id ?>/grid" style="display:flex;gap:6px;align-items:center;">
<input type="date" name="date" value="<?= e($date) ?>" class="form-input" style="direction:ltr;font-size:12px;padding:4px 8px;width:130px;" onchange="this.form.submit()">
</form>
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= $nextDate ?>" class="btn btn-sm btn-outline">&larr;</a>
<a href="/pool/<?= (int) $pool->id ?>/grid" class="btn btn-sm btn-outline">اليوم</a>
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= $nextDate ?>" class="btn btn-sm btn-outline" style="padding:4px 8px;">&larr;</a>
<a href="/pool/<?= (int) $pool->id ?>/grid" class="btn btn-sm btn-outline" style="padding:4px 8px;font-size:11px;">اليوم</a>
<span style="font-size:13px;font-weight:600;color:#1A1A2E;margin-right:8px;"><?= e($dayName) ?></span>
</div>
<!-- Stats -->
<div class="card" style="padding:15px;flex:1;display:flex;gap:25px;align-items:center;">
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#0D7377;"><?= $utilization['occupancy_pct'] ?? 0 ?>%</div>
<div style="font-size:11px;color:#6B7280;">الإشغال</div>
</div>
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#2563EB;"><?= $utilization['total_bookings'] ?? 0 ?></div>
<div style="font-size:11px;color:#6B7280;">حجز</div>
<div class="card" style="padding:12px 16px;flex:1;display:flex;gap:20px;align-items:center;">
<div style="text-align:center;"><span style="font-size:18px;font-weight:700;color:#0D7377;"><?= $utilization['occupancy_pct'] ?? 0 ?>%</span><div style="font-size:10px;color:#6B7280;">إشغال</div></div>
<div style="text-align:center;"><span style="font-size:18px;font-weight:700;color:#2563EB;"><?= $utilization['total_bookings'] ?? 0 ?></span><div style="font-size:10px;color:#6B7280;">حجز</div></div>
<div style="text-align:center;"><span style="font-size:18px;font-weight:700;color:#D97706;"><?= $utilization['total_swimmers'] ?? 0 ?></span><div style="font-size:10px;color:#6B7280;">سبّاح</div></div>
<div style="text-align:center;"><span style="font-size:18px;font-weight:700;color:#059669;"><?= number_format($utilization['total_revenue'] ?? 0, 0) ?></span><div style="font-size:10px;color:#6B7280;">ج.م</div></div>
<div style="margin-right:auto;display:flex;gap:8px;flex-wrap:wrap;">
<?php foreach ($statusColors as $st => $color): ?>
<span style="display:flex;align-items:center;gap:3px;font-size:10px;"><span style="width:10px;height:10px;border-radius:2px;background:<?= $color ?>;display:inline-block;"></span><?= ['available'=>'متاح','booked'=>'محجوز','in_progress'=>'جاري','maintenance'=>'صيانة','scheduled'=>'جدول'][$st] ?></span>
<?php endforeach; ?>
</div>
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#D97706;"><?= $utilization['total_swimmers'] ?? 0 ?></div>
<div style="font-size:11px;color:#6B7280;">سبّاح</div>
</div>
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#059669;"><?= number_format($utilization['total_revenue'] ?? 0, 0) ?></div>
<div style="font-size:11px;color:#6B7280;">إيرادات (ج.م)</div>
</div>
</div>
<!-- Legend -->
<div class="card" style="padding:15px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
<?php foreach ($statusColors as $st => $color): ?>
<span style="display:flex;align-items:center;gap:4px;font-size:11px;">
<span style="width:14px;height:14px;border-radius:3px;background:<?= $color ?>;display:inline-block;"></span>
<?= ['available'=>'متاح','booked'=>'محجوز','in_progress'=>'جاري','maintenance'=>'صيانة'][$st] ?>
</span>
<?php endforeach; ?>
</div>
</div>
<!-- Pool Grid -->
<div class="card" style="padding:0;overflow:auto;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="grid-3x3" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">شبكة الحارات × الأوقات</h3>
<span style="font-size:12px;color:#9CA3AF;margin-right:auto;"><?= e($pool->length_meters) ?>م × <?= e($pool->width_meters) ?>م — <?= count($lanes) ?> حارة</span>
</div>
<!-- Instructions -->
<div style="margin-bottom:12px;padding:10px 16px;background:#F0FDFA;border:1px solid #CCFBF1;border-radius:8px;font-size:12px;color:#0D7377;display:flex;gap:20px;align-items:center;">
<span><strong>اضغط</strong> على خلية لتحديدها</span>
<span><strong>اسحب</strong> لتحديد عدة خلايا</span>
<span><strong>رأس الحارة</strong> = تحديد صف كامل</span>
<span><strong>رأس الوقت</strong> = تحديد عمود كامل</span>
<span><strong>Esc</strong> = إلغاء التحديد</span>
</div>
<div style="overflow-x:auto;padding:10px;">
<table style="width:100%;border-collapse:collapse;min-width:<?= count($timeSlots) * 70 + 100 ?>px;" id="poolGrid">
<!-- The Grid -->
<div class="card pool-grid-wrap" style="padding:0;overflow:auto;">
<div style="padding:8px;overflow-x:auto;">
<table class="pool-grid" id="poolGrid">
<thead>
<tr>
<th style="padding:8px;font-size:12px;color:#6B7280;text-align:right;position:sticky;right:0;background:#fff;z-index:2;border-bottom:2px solid #E5E7EB;min-width:90px;">
الحارة
</th>
<?php foreach ($timeSlots as $slot): ?>
<th style="padding:6px 4px;font-size:11px;color:#6B7280;text-align:center;border-bottom:2px solid #E5E7EB;min-width:65px;direction:ltr;">
<?= e($slot['start']) ?>
<th style="min-width:70px;position:sticky;right:0;background:#fff;z-index:5;"></th>
<?php foreach ($timeSlots as $si => $slot): ?>
<th class="time-header" data-col="<?= $si ?>" onclick="selectColumn(<?= $si ?>)">
<div style="direction:ltr;font-size:10px;"><?= e($slot['start']) ?></div>
<div style="direction:ltr;font-size:9px;color:#9CA3AF;"><?= e($slot['end']) ?></div>
</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($lanes as $lane):
<?php foreach ($lanes as $li => $lane):
$laneNum = (int) $lane['lane_number'];
?>
<tr>
<td style="padding:8px;font-size:12px;font-weight:600;color:#1A1A2E;position:sticky;right:0;background:#fff;z-index:1;border-bottom:1px solid #F3F4F6;">
<?= e($lane['label_ar'] ?? ('حارة ' . $laneNum)) ?>
<div style="font-size:10px;color:#9CA3AF;font-weight:normal;"><?= e($lane['width_meters']) ?>م</div>
<tr data-lane="<?= $laneNum ?>">
<td class="lane-header" style="position:sticky;right:0;background:#fff;z-index:4;" data-row="<?= $li ?>" onclick="selectRow(<?= $li ?>)">
<div style="font-size:12px;font-weight:700;color:#0D7377;"><?= e($lane['label_ar'] ?? ('حارة ' . $laneNum)) ?></div>
<div style="font-size:9px;color:#9CA3AF;"><?= e($lane['width_meters']) ?>م</div>
</td>
<?php foreach ($timeSlots as $slot):
<?php foreach ($timeSlots as $si => $slot):
$slotStart = $slot['start'];
$cell = $cells[$laneNum][$slotStart] ?? ['status' => 'available', 'booking' => null];
$color = $statusColors[$cell['status']] ?? '#D1D5DB';
$booking = $cell['booking'];
$cellStatus = $cell['status'];
// Check if a recurring schedule covers this cell
$scheduleMatch = null;
if ($cellStatus === 'available') {
foreach ($schedules as $sch) {
$schLanes = json_decode($sch['lanes_json'] ?? '[]', true);
if (is_array($schLanes) && in_array($laneNum, $schLanes)) {
if ($sch['start_time'] <= $slotStart && $sch['end_time'] > $slotStart) {
$scheduleMatch = $sch;
$cellStatus = 'scheduled';
break;
}
}
}
}
?>
<td style="padding:3px;border-bottom:1px solid #F3F4F6;"
data-lane="<?= $laneNum ?>" data-slot="<?= e($slotStart) ?>" data-status="<?= e($cell['status']) ?>">
<div class="grid-cell" style="
height:40px;
border-radius:6px;
background:<?= $color ?>15;
border:2px solid <?= $color ?>40;
cursor:<?= $cell['status'] === 'available' ? 'pointer' : 'default' ?>;
display:flex;
align-items:center;
justify-content:center;
font-size:10px;
color:<?= $color ?>;
font-weight:600;
position:relative;
transition:all 0.15s;
"
<?php if ($booking): ?>
title="<?= e(($bookingTypes[$booking['type']] ?? '') . ' — ' . ($booking['booker'] ?? '') . ' (' . $booking['swimmers'] . ' سبّاح)') ?>"
<?php else: ?>
title="متاح — اضغط للحجز"
onclick="quickBook(<?= $laneNum ?>, '<?= e($slotStart) ?>', '<?= e($slot['end']) ?>')"
<?php endif; ?>
<td style="padding:2px;">
<div class="pool-cell <?= e($cellStatus) ?>"
data-row="<?= $li ?>" data-col="<?= $si ?>"
data-lane="<?= $laneNum ?>" data-slot="<?= e($slotStart) ?>" data-slot-end="<?= e($slot['end']) ?>"
data-status="<?= e($cellStatus) ?>"
<?php if ($booking): ?>
data-booking-id="<?= (int) $booking['id'] ?>"
title="<?= e(($bookingTypeLabels[$booking['type']] ?? '') . ' — ' . ($booking['booker'] ?? '') . ' (' . $booking['swimmers'] . ')') ?>"
<?php elseif ($scheduleMatch): ?>
title="<?= e(($scheduleMatch['schedule_name'] ?? '') . ' — ' . ($scheduleMatch['coach_name'] ?? '')) ?>"
<?php endif; ?>
>
<?php if ($booking): ?>
<?php if ($cell['status'] === 'in_progress'): ?>
<i data-lucide="activity" style="width:12px;height:12px;"></i>
<?php elseif ($cell['status'] === 'maintenance'): ?>
<i data-lucide="wrench" style="width:12px;height:12px;"></i>
<?php else: ?>
<?= $booking['swimmers'] ?>
<?php endif; ?>
<span class="cell-count"><?= (int) $booking['swimmers'] ?></span>
<span class="cell-label"><?= e($booking['booker'] ?? '') ?></span>
<?php elseif ($scheduleMatch): ?>
<span class="cell-count"><i data-lucide="repeat" style="width:12px;height:12px;"></i></span>
<span class="cell-label"><?= e($scheduleMatch['schedule_name'] ?? '') ?></span>
<?php endif; ?>
</div>
</td>
......@@ -149,64 +176,244 @@ $statusColors = [
</div>
<!-- Pool Dimensions Visual -->
<div class="card" style="margin-top:20px;padding:20px;">
<h4 style="margin:0 0 10px;font-size:14px;color:#6B7280;">المخطط التقريبي</h4>
<div style="position:relative;width:100%;max-width:600px;margin:0 auto;">
<div style="border:3px solid #0D7377;border-radius:12px;padding:10px;background:#F0FDFA;">
<div style="display:flex;gap:2px;">
<?php foreach ($lanes as $lane): ?>
<div style="flex:1;height:60px;background:#CCFBF1;border:1px dashed #0D737740;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;color:#0D7377;">
<?= (int) $lane['lane_number'] ?>
</div>
<?php endforeach; ?>
</div>
<div style="text-align:center;margin-top:8px;font-size:11px;color:#6B7280;">
<?= e($pool->length_meters) ?>م (طول) × <?= e($pool->width_meters) ?>م (عرض)
<?php if ($pool->depth_shallow || $pool->depth_deep): ?>
— العمق: <?= e($pool->depth_shallow ?? '?') ?>م إلى <?= e($pool->depth_deep ?? '?') ?>م
<?php endif; ?>
<div class="card" style="margin-top:15px;padding:15px 20px;">
<div style="display:flex;align-items:center;gap:15px;">
<div style="border:2px solid #0D7377;border-radius:8px;padding:6px;background:#F0FDFA;display:flex;gap:2px;flex:1;max-width:500px;">
<?php foreach ($lanes as $lane): ?>
<div style="flex:1;height:30px;background:#CCFBF1;border:1px dashed #0D737730;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;color:#0D7377;font-weight:600;">
<?= (int) $lane['lane_number'] ?>
</div>
<?php endforeach; ?>
</div>
<span style="font-size:12px;color:#6B7280;"><?= e($pool->length_meters) ?>م × <?= e($pool->width_meters) ?>م<?php if ($pool->depth_shallow || $pool->depth_deep): ?> — عمق: <?= e($pool->depth_shallow ?? '?') ?><?= e($pool->depth_deep ?? '?') ?>م<?php endif; ?></span>
</div>
</div>
<!-- Inline Booking Panel (slides up from bottom) -->
<div class="booking-panel" id="bookingPanel">
<form method="POST" action="/pool/<?= (int) $pool->id ?>/book-cells" id="cellBookingForm">
<?= csrf_field() ?>
<input type="hidden" name="booking_date" value="<?= e($date) ?>">
<input type="hidden" name="cells_json" id="cellsJsonInput" value="[]">
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap;">
<div class="selection-info">
<span class="selection-badge"><i data-lucide="grid-3x3" style="width:14px;height:14px;"></i> <span id="selCount">0</span> خلية محددة</span>
<span class="selection-badge"><i data-lucide="move-horizontal" style="width:14px;height:14px;"></i> <span id="selLanes">0</span> حارة</span>
<span class="selection-badge"><i data-lucide="clock" style="width:14px;height:14px;"></i> <span id="selTime"></span></span>
</div>
<div style="display:flex;gap:10px;align-items:center;flex:1;">
<select name="booking_type" class="form-input" style="width:130px;font-size:12px;padding:6px 10px;" required>
<?php foreach ($bookingTypeLabels as $k => $v): ?>
<option value="<?= e($k) ?>"><?= e($v) ?></option>
<?php endforeach; ?>
</select>
<input type="text" name="booker_name" placeholder="اسم الحاجز" class="form-input" style="width:160px;font-size:12px;padding:6px 10px;" required>
<input type="number" name="expected_swimmers" value="1" min="1" class="form-input" style="width:70px;font-size:12px;padding:6px 10px;direction:ltr;" placeholder="عدد">
<input type="text" name="notes" placeholder="ملاحظات" class="form-input" style="width:140px;font-size:12px;padding:6px 10px;">
</div>
<div style="display:flex;gap:8px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حجز
</button>
<button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 10px;" onclick="clearSelection()">
<i data-lucide="x" style="width:14px;height:14px;vertical-align:middle;"></i>
</button>
</div>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
// Auto-refresh every 30 seconds
setInterval(function() {
fetch('/api/pool/<?= (int) $pool->id ?>/state?date=<?= e($date) ?>')
.then(r => r.json())
.then(data => updateGrid(data))
.catch(() => {});
}, 30000);
});
var grid = document.getElementById('poolGrid');
var panel = document.getElementById('bookingPanel');
var cellsInput = document.getElementById('cellsJsonInput');
var selected = new Set();
var isDragging = false;
var dragStart = null;
function getCellKey(row, col) { return row + ':' + col; }
function updateGrid(data) {
var cells = data.cells || {};
var statusColors = {available:'#10B981', booked:'#3B82F6', in_progress:'#F59E0B', maintenance:'#EF4444'};
document.querySelectorAll('#poolGrid td[data-lane]').forEach(function(td) {
var lane = parseInt(td.getAttribute('data-lane'));
var slot = td.getAttribute('data-slot');
if (cells[lane] && cells[lane][slot]) {
var cell = cells[lane][slot];
var color = statusColors[cell.status] || '#D1D5DB';
var div = td.querySelector('.grid-cell');
if (div) {
div.style.background = color + '15';
div.style.borderColor = color + '40';
div.style.color = color;
td.setAttribute('data-status', cell.status);
function toggleCell(cell) {
var row = parseInt(cell.dataset.row);
var col = parseInt(cell.dataset.col);
var status = cell.dataset.status;
if (status !== 'available') return;
var key = getCellKey(row, col);
if (selected.has(key)) {
selected.delete(key);
cell.classList.remove('selected');
} else {
selected.add(key);
cell.classList.add('selected');
}
updatePanel();
}
function selectRange(r1, c1, r2, c2) {
var minR = Math.min(r1, r2), maxR = Math.max(r1, r2);
var minC = Math.min(c1, c2), maxC = Math.max(c1, c2);
for (var r = minR; r <= maxR; r++) {
for (var c = minC; c <= maxC; c++) {
var cell = grid.querySelector('.pool-cell[data-row="' + r + '"][data-col="' + c + '"]');
if (cell && cell.dataset.status === 'available') {
var key = getCellKey(r, c);
selected.add(key);
cell.classList.add('selected');
}
}
}
updatePanel();
}
// Mouse events for drag selection
grid.addEventListener('mousedown', function(e) {
var cell = e.target.closest('.pool-cell');
if (!cell || cell.dataset.status !== 'available') return;
e.preventDefault();
isDragging = true;
dragStart = { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col) };
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
clearSelection(true);
}
toggleCell(cell);
});
}
function quickBook(lane, startTime, endTime) {
window.location.href = '/pool/<?= (int) $pool->id ?>/book?date=<?= e($date) ?>&lane=' + lane + '&start=' + startTime + '&end=' + endTime;
}
</script>
grid.addEventListener('mouseover', function(e) {
if (!isDragging || !dragStart) return;
var cell = e.target.closest('.pool-cell');
if (!cell || cell.dataset.status !== 'available') return;
var row = parseInt(cell.dataset.row);
var col = parseInt(cell.dataset.col);
// Clear previous drag range, re-select from start
clearSelection(true);
selectRange(dragStart.row, dragStart.col, row, col);
});
document.addEventListener('mouseup', function() {
isDragging = false;
dragStart = null;
});
// Keyboard
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') clearSelection();
});
// Row/Column selection
window.selectRow = function(rowIdx) {
var cells = grid.querySelectorAll('.pool-cell[data-row="' + rowIdx + '"]');
var allSelected = true;
cells.forEach(function(c) {
if (c.dataset.status === 'available' && !c.classList.contains('selected')) allSelected = false;
});
cells.forEach(function(c) {
if (c.dataset.status !== 'available') return;
var key = getCellKey(parseInt(c.dataset.row), parseInt(c.dataset.col));
if (allSelected) {
selected.delete(key);
c.classList.remove('selected');
} else {
selected.add(key);
c.classList.add('selected');
}
});
updatePanel();
};
window.selectColumn = function(colIdx) {
var cells = grid.querySelectorAll('.pool-cell[data-col="' + colIdx + '"]');
var allSelected = true;
cells.forEach(function(c) {
if (c.dataset.status === 'available' && !c.classList.contains('selected')) allSelected = false;
});
cells.forEach(function(c) {
if (c.dataset.status !== 'available') return;
var key = getCellKey(parseInt(c.dataset.row), parseInt(c.dataset.col));
if (allSelected) {
selected.delete(key);
c.classList.remove('selected');
} else {
selected.add(key);
c.classList.add('selected');
}
});
updatePanel();
};
window.clearSelection = function(silent) {
selected.clear();
grid.querySelectorAll('.pool-cell.selected').forEach(function(c) { c.classList.remove('selected'); });
if (!silent) updatePanel();
};
function updatePanel() {
var count = selected.size;
document.getElementById('selCount').textContent = count;
if (count === 0) {
panel.classList.remove('visible');
cellsInput.value = '[]';
return;
}
panel.classList.add('visible');
// Calculate lanes and time range
var lanes = new Set(), minSlot = '99:99', maxSlotEnd = '00:00';
selected.forEach(function(key) {
var parts = key.split(':');
var cell = grid.querySelector('.pool-cell[data-row="' + parts[0] + '"][data-col="' + parts[1] + '"]');
if (cell) {
lanes.add(cell.dataset.lane);
if (cell.dataset.slot < minSlot) minSlot = cell.dataset.slot;
if (cell.dataset.slotEnd > maxSlotEnd) maxSlotEnd = cell.dataset.slotEnd;
}
});
document.getElementById('selLanes').textContent = lanes.size;
document.getElementById('selTime').textContent = minSlot + ' — ' + maxSlotEnd;
// Build cells payload
var payload = [];
selected.forEach(function(key) {
var parts = key.split(':');
var cell = grid.querySelector('.pool-cell[data-row="' + parts[0] + '"][data-col="' + parts[1] + '"]');
if (cell) {
payload.push({ lane: parseInt(cell.dataset.lane), start: cell.dataset.slot, end: cell.dataset.slotEnd });
}
});
cellsInput.value = JSON.stringify(payload);
}
// Auto-refresh grid state every 30s
setInterval(function() {
fetch('/api/pool/<?= (int) $pool->id ?>/state?date=<?= e($date) ?>')
.then(function(r) { return r.json(); })
.then(function(data) { refreshCells(data); })
.catch(function() {});
}, 30000);
function refreshCells(data) {
var cells = data.cells || {};
var colorMap = {available:'#10B981', booked:'#3B82F6', in_progress:'#F59E0B', maintenance:'#EF4444', scheduled:'#8B5CF6'};
grid.querySelectorAll('.pool-cell').forEach(function(el) {
var lane = parseInt(el.dataset.lane);
var slot = el.dataset.slot;
if (cells[lane] && cells[lane][slot]) {
var newStatus = cells[lane][slot].status || 'available';
if (el.dataset.status !== newStatus && !el.classList.contains('selected')) {
el.dataset.status = newStatus;
el.className = 'pool-cell ' + newStatus;
}
}
});
}
});
</script>
<?php $__template->endSection(); ?>
......@@ -9,41 +9,55 @@
<?php $__template->section('content'); ?>
<?php if (!empty($pools)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(340px, 1fr));gap:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(380px, 1fr));gap:20px;">
<?php foreach ($pools as $p): ?>
<div class="card" style="overflow:hidden;">
<a href="/pool/<?= (int) $p['id'] ?>/grid" style="text-decoration:none;color:inherit;display:block;">
<div style="padding:20px;display:flex;align-items:start;gap:15px;">
<div style="width:56px;height:56px;border-radius:12px;background:linear-gradient(135deg, #0D737715, #0D737730);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="waves" style="width:28px;height:28px;color:#0D7377;"></i>
<div class="card" style="overflow:hidden;border-top:4px solid #0D7377;">
<div style="padding:20px;">
<div style="display:flex;align-items:start;gap:15px;margin-bottom:15px;">
<div style="width:60px;height:60px;border-radius:14px;background:linear-gradient(135deg, #0D737720, #0D737740);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="waves" style="width:30px;height:30px;color:#0D7377;"></i>
</div>
<div style="flex:1;">
<h3 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($p['name_ar']) ?></h3>
<div style="display:flex;gap:12px;font-size:12px;color:#6B7280;">
<span><?= e($p['length_meters']) ?>م × <?= e($p['width_meters']) ?>م</span>
<span><?= (int) $p['total_lanes_lengthwise'] ?> حارة</span>
<span>حد أقصى: <?= (int) $p['max_total_swimmers'] ?> سبّاح</span>
<h3 style="margin:0 0 6px;font-size:17px;font-weight:700;color:#1A1A2E;"><?= e($p['name_ar']) ?></h3>
<div style="display:flex;gap:10px;font-size:12px;color:#6B7280;flex-wrap:wrap;">
<span style="display:inline-flex;align-items:center;gap:3px;"><i data-lucide="ruler" style="width:11px;height:11px;"></i> <?= e($p['length_meters']) ?>م × <?= e($p['width_meters']) ?>م</span>
<span style="display:inline-flex;align-items:center;gap:3px;"><i data-lucide="columns" style="width:11px;height:11px;"></i> <?= (int) $p['total_lanes_lengthwise'] ?> حارة</span>
<span style="display:inline-flex;align-items:center;gap:3px;"><i data-lucide="users" style="width:11px;height:11px;"></i> حد: <?= (int) $p['max_total_swimmers'] ?></span>
</div>
</div>
</div>
<div style="padding:0 20px 15px;display:flex;gap:10px;">
<span class="btn btn-sm btn-primary" style="font-size:12px;padding:5px 12px;">
<i data-lucide="grid-3x3" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> فتح الشبكة
</span>
<a href="/pool/<?= (int) $p['id'] ?>/book" class="btn btn-sm btn-outline" style="font-size:12px;padding:5px 12px;">
<i data-lucide="plus" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> حجز
<!-- Mini pool visual -->
<div style="border:2px solid #0D737730;border-radius:8px;padding:6px;background:#F0FDFA;margin-bottom:15px;">
<div style="display:flex;gap:2px;">
<?php for ($i = 1; $i <= (int) $p['total_lanes_lengthwise']; $i++): ?>
<div style="flex:1;height:20px;background:#CCFBF1;border:1px dashed #0D737720;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;color:#0D7377;font-weight:600;"><?= $i ?></div>
<?php endfor; ?>
</div>
</div>
<!-- Action buttons -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;">
<a href="/pool/<?= (int) $p['id'] ?>/grid" class="btn btn-primary" style="font-size:12px;padding:10px 0;text-align:center;display:block;">
<i data-lucide="grid-3x3" style="width:14px;height:14px;vertical-align:middle;margin-left:3px;"></i> الشبكة
</a>
<a href="/pool/<?= (int) $p['id'] ?>/dashboard" class="btn btn-outline" style="font-size:12px;padding:10px 0;text-align:center;display:block;">
<i data-lucide="bar-chart-2" style="width:14px;height:14px;vertical-align:middle;margin-left:3px;"></i> لوحة تحكم
</a>
<a href="/pool/<?= (int) $p['id'] ?>/schedules" class="btn btn-outline" style="font-size:12px;padding:10px 0;text-align:center;display:block;">
<i data-lucide="repeat" style="width:14px;height:14px;vertical-align:middle;margin-left:3px;"></i> جداول
</a>
</div>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<i data-lucide="waves" style="width:48px;height:48px;color:#D1D5DB;margin-bottom:15px;"></i>
<h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد حمامات سباحة</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">أضف مرفق من نوع "حمام سباحة" أولاً، ثم قم بتكوينه هنا.</p>
<a href="/pool/config/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة تكوين</a>
<i data-lucide="waves" style="width:56px;height:56px;color:#D1D5DB;margin-bottom:15px;"></i>
<h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد حمامات سباحة مكوّنة</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">أضف مرفق من نوع "حمام سباحة" ثم قم بتكوينه هنا لبدء إدارة الحارات والحجوزات.</p>
<a href="/pool/config/create" class="btn btn-primary" style="padding:12px 28px;"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة تكوين</a>
</div>
<?php endif; ?>
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الجداول الأسبوعية: <?= e($pool->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/<?= (int) $pool->id ?>/grid" class="btn btn-outline"><i data-lucide="grid-3x3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الشبكة</a>
<a href="/pool" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
use App\Modules\PoolManagement\Models\PoolSchedule;
use App\Modules\PoolManagement\Models\PoolConfiguration;
$dayNames = PoolSchedule::getDayNames();
$bookingTypes = PoolConfiguration::getBookingTypes();
$lanes = $pool->getLanes();
?>
<!-- Create Schedule Form -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="plus-circle" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">إضافة جدول أسبوعي</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/pool/<?= (int) $pool->id ?>/schedules">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اسم الجدول <span style="color:#DC2626;">*</span></label>
<input type="text" name="schedule_name" class="form-input" placeholder="مثال: أكاديمية السباحة - مبتدئين" required>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اليوم <span style="color:#DC2626;">*</span></label>
<select name="day_of_week" class="form-input" required>
<?php foreach ($dayNames as $num => $name): ?>
<option value="<?= $num ?>"><?= e($name) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">من <span style="color:#DC2626;">*</span></label>
<input type="time" name="start_time" class="form-input" required style="direction:ltr;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">إلى <span style="color:#DC2626;">*</span></label>
<input type="time" name="end_time" class="form-input" required style="direction:ltr;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">نوع النشاط</label>
<select name="booking_type" class="form-input">
<?php foreach ($bookingTypes as $k => $v): ?>
<option value="<?= e($k) ?>"><?= e($v) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">المدرب</label>
<input type="text" name="coach_name" class="form-input" placeholder="اسم المدرب">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الفئة العمرية</label>
<input type="text" name="age_group" class="form-input" placeholder="مثال: 6-10">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">النوع</label>
<select name="gender" class="form-input">
<option value="mixed">مختلط</option>
<option value="boys">بنين</option>
<option value="girls">بنات</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الحد الأقصى للسبّاحين</label>
<input type="number" name="max_swimmers" value="8" min="1" class="form-input" style="direction:ltr;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">سارٍ من <span style="color:#DC2626;">*</span></label>
<input type="date" name="effective_from" value="<?= date('Y-m-d') ?>" class="form-input" required style="direction:ltr;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">سارٍ حتى</label>
<input type="date" name="effective_to" class="form-input" style="direction:ltr;">
</div>
</div>
<div style="margin-bottom:15px;">
<label class="form-label" style="font-size:12px;">الحارات <span style="color:#DC2626;">*</span></label>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<?php foreach ($lanes as $lane): ?>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:12px;transition:all 0.15s;" class="lane-opt">
<input type="checkbox" name="lanes[]" value="<?= (int) $lane['lane_number'] ?>" style="accent-color:#0D7377;">
<?= e($lane['label_ar'] ?? ('حارة ' . $lane['lane_number'])) ?>
</label>
<?php endforeach; ?>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #8B5CF6;border-radius:8px;cursor:pointer;font-size:12px;background:#8B5CF610;color:#7C3AED;font-weight:600;">
<input type="checkbox" id="selectAllLanes" style="accent-color:#8B5CF6;"> الكل
</label>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<label class="form-label" style="font-size:12px;margin:0;">اللون:</label>
<input type="color" name="color" value="#8B5CF6" style="width:36px;height:30px;border:none;cursor:pointer;">
<div style="flex:1;"></div>
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:10px 24px;">
<i data-lucide="save" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ الجدول
</button>
</div>
</form>
</div>
</div>
<!-- Existing Schedules -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="repeat" style="width:18px;height:18px;color:#8B5CF6;"></i>
<h3 style="margin:0;color:#8B5CF6;font-size:15px;">الجداول الحالية</h3>
<span style="font-size:12px;color:#9CA3AF;margin-right:8px;"><?= count($schedules) ?> جدول</span>
</div>
<?php if (!empty($schedules)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">الاسم</th>
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">اليوم</th>
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">الوقت</th>
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">الحارات</th>
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">المدرب</th>
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">الفئة</th>
<th style="padding:10px 12px;text-align:right;font-weight:600;color:#374151;">الحد</th>
<th style="padding:10px 12px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($schedules as $sch):
$schLanes = json_decode($sch['lanes_json'] ?? '[]', true);
$schColor = $sch['color'] ?? '#8B5CF6';
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 12px;">
<span style="display:inline-block;width:10px;height:10px;border-radius:3px;background:<?= e($schColor) ?>;margin-left:6px;vertical-align:middle;"></span>
<strong><?= e($sch['schedule_name']) ?></strong>
</td>
<td style="padding:10px 12px;"><?= e($dayNames[(int) $sch['day_of_week']] ?? '') ?></td>
<td style="padding:10px 12px;direction:ltr;text-align:right;"><?= e(substr($sch['start_time'], 0, 5)) ?><?= e(substr($sch['end_time'], 0, 5)) ?></td>
<td style="padding:10px 12px;"><?= is_array($schLanes) ? implode(', ', $schLanes) : '—' ?></td>
<td style="padding:10px 12px;"><?= e($sch['coach_full_name'] ?? $sch['coach_name'] ?? '—') ?></td>
<td style="padding:10px 12px;">
<?php if ($sch['age_group']): ?><span style="font-size:11px;padding:2px 8px;background:#F3F4F6;border-radius:8px;"><?= e($sch['age_group']) ?> سنة</span><?php endif; ?>
<?php if ($sch['gender'] && $sch['gender'] !== 'mixed'): ?><span style="font-size:11px;padding:2px 8px;background:<?= $sch['gender'] === 'boys' ? '#DBEAFE' : '#FCE7F3' ?>;border-radius:8px;margin-right:4px;"><?= $sch['gender'] === 'boys' ? 'بنين' : 'بنات' ?></span><?php endif; ?>
</td>
<td style="padding:10px 12px;text-align:center;"><?= (int) $sch['max_swimmers'] ?></td>
<td style="padding:10px 12px;text-align:center;">
<form method="POST" action="/pool/<?= (int) $pool->id ?>/schedules/<?= (int) $sch['id'] ?>/delete" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm" style="color:#DC2626;border:1px solid #FCA5A5;background:#FEF2F2;border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;" onclick="return confirm('حذف هذا الجدول؟')">
<i data-lucide="trash-2" style="width:12px;height:12px;vertical-align:middle;"></i>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px;text-align:center;color:#9CA3AF;">
<i data-lucide="calendar-off" style="width:36px;height:36px;margin-bottom:10px;"></i>
<p style="font-size:14px;margin:0;">لا توجد جداول أسبوعية — أضف جدولاً لحجز الحارات تلقائياً كل أسبوع</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var selectAll = document.getElementById('selectAllLanes');
var checkboxes = document.querySelectorAll('input[name="lanes[]"]');
selectAll.addEventListener('change', function() {
checkboxes.forEach(function(cb) { cb.checked = selectAll.checked; });
});
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `pool_schedules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`pool_config_id` BIGINT UNSIGNED NOT NULL,
`schedule_name` VARCHAR(255) NOT NULL,
`day_of_week` TINYINT UNSIGNED NOT NULL COMMENT '0=Sun,1=Mon...6=Sat',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`lanes_json` JSON NOT NULL COMMENT 'array of lane numbers',
`booking_type` VARCHAR(30) NOT NULL DEFAULT 'academy_session',
`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' COMMENT 'boys,girls,mixed',
`max_swimmers` 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_ps_pool` (`pool_config_id`),
INDEX `idx_ps_day` (`day_of_week`),
INDEX `idx_ps_active` (`is_active`, `effective_from`, `effective_to`),
CONSTRAINT `fk_ps_pool` FOREIGN KEY (`pool_config_id`) REFERENCES `pool_configurations`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `pool_schedules`",
];
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