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; ...@@ -6,7 +6,10 @@ namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller; use App\Core\Controller;
use App\Core\Request; use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration; 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\PoolGridService;
use App\Modules\PoolManagement\Services\PoolOccupancyOptimizer; use App\Modules\PoolManagement\Services\PoolOccupancyOptimizer;
...@@ -22,16 +25,89 @@ class PoolGridController extends Controller ...@@ -22,16 +25,89 @@ class PoolGridController extends Controller
$date = trim((string) $request->get('date', date('Y-m-d'))); $date = trim((string) $request->get('date', date('Y-m-d')));
$gridState = PoolGridService::getGridState((int) $id, $date); $gridState = PoolGridService::getGridState((int) $id, $date);
$utilization = PoolOccupancyOptimizer::getUtilizationMetrics((int) $id, $date, $date); $utilization = PoolOccupancyOptimizer::getUtilizationMetrics((int) $id, $date, $date);
$schedules = PoolSchedule::getActiveForPool((int) $id, $date);
return $this->view('PoolManagement.Views.grid', [ return $this->view('PoolManagement.Views.grid', [
'pool' => $pool, 'pool' => $pool,
'gridState' => $gridState, 'gridState' => $gridState,
'utilization' => $utilization, 'utilization' => $utilization,
'date' => $date, 'date' => $date,
'schedules' => $schedules,
'bookingTypes'=> PoolConfiguration::getBookingTypes(), '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 public function apiState(Request $request, string $id): Response
{ {
$date = trim((string) $request->get('date', date('Y-m-d'))); $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 @@ ...@@ -2,17 +2,31 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
['GET', '/pool', 'PoolManagement\Controllers\PoolConfigController@index', ['auth'], 'pool.view'], // Pool Configuration
['GET', '/pool/config/create', 'PoolManagement\Controllers\PoolConfigController@create', ['auth'], 'pool.manage'], ['GET', '/pool', 'PoolManagement\Controllers\PoolConfigController@index', ['auth'], 'pool.view'],
['POST', '/pool/config', 'PoolManagement\Controllers\PoolConfigController@store', ['auth', 'csrf'], 'pool.manage'], ['GET', '/pool/config/create', 'PoolManagement\Controllers\PoolConfigController@create', ['auth'], 'pool.manage'],
['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'], ['POST', '/pool/config', 'PoolManagement\Controllers\PoolConfigController@store', ['auth', 'csrf'], 'pool.manage'],
['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'], ['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'],
['GET', '/pool/{id:\d+}/grid', 'PoolManagement\Controllers\PoolGridController@grid', ['auth'], 'pool.view'], ['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'],
['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'], // Pool Grid (interactive)
['POST', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@store', ['auth', 'csrf'], 'pool.book'], ['GET', '/pool/{id:\d+}/grid', 'PoolManagement\Controllers\PoolGridController@grid', ['auth'], 'pool.view'],
['GET', '/pool/bookings', 'PoolManagement\Controllers\PoolBookingController@index', ['auth'], 'pool.view'], ['POST', '/pool/{id:\d+}/book-cells', 'PoolManagement\Controllers\PoolGridController@bookCells', ['auth', 'csrf'], 'pool.book'],
['GET', '/pool/bookings/{id:\d+}', 'PoolManagement\Controllers\PoolBookingController@show', ['auth'], 'pool.view'], ['GET', '/api/pool/{id:\d+}/state', 'PoolManagement\Controllers\PoolGridController@apiState', ['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 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'],
]; ];
This diff is collapsed.
This diff is collapsed.
...@@ -9,41 +9,55 @@ ...@@ -9,41 +9,55 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<?php if (!empty($pools)): ?> <?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): ?> <?php foreach ($pools as $p): ?>
<div class="card" style="overflow:hidden;"> <div class="card" style="overflow:hidden;border-top:4px solid #0D7377;">
<a href="/pool/<?= (int) $p['id'] ?>/grid" style="text-decoration:none;color:inherit;display:block;"> <div style="padding:20px;">
<div style="padding:20px;display:flex;align-items:start;gap:15px;"> <div style="display:flex;align-items:start;gap:15px;margin-bottom: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;"> <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:28px;height:28px;color:#0D7377;"></i> <i data-lucide="waves" style="width:30px;height:30px;color:#0D7377;"></i>
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<h3 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($p['name_ar']) ?></h3> <h3 style="margin:0 0 6px;font-size:17px;font-weight:700;color:#1A1A2E;"><?= e($p['name_ar']) ?></h3>
<div style="display:flex;gap:12px;font-size:12px;color:#6B7280;"> <div style="display:flex;gap:10px;font-size:12px;color:#6B7280;flex-wrap:wrap;">
<span><?= e($p['length_meters']) ?>م × <?= e($p['width_meters']) ?>م</span> <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><?= (int) $p['total_lanes_lengthwise'] ?> حارة</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>حد أقصى: <?= (int) $p['max_total_swimmers'] ?> سبّاح</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>
</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;"> <!-- Mini pool visual -->
<i data-lucide="grid-3x3" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> فتح الشبكة <div style="border:2px solid #0D737730;border-radius:8px;padding:6px;background:#F0FDFA;margin-bottom:15px;">
</span> <div style="display:flex;gap:2px;">
<a href="/pool/<?= (int) $p['id'] ?>/book" class="btn btn-sm btn-outline" style="font-size:12px;padding:5px 12px;"> <?php for ($i = 1; $i <= (int) $p['total_lanes_lengthwise']; $i++): ?>
<i data-lucide="plus" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></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> </a>
</div> </div>
</a> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;"> <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> <i data-lucide="waves" style="width:56px;height:56px;color:#D1D5DB;margin-bottom:15px;"></i>
<h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد حمامات سباحة</h3> <h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد حمامات سباحة مكوّنة</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">أضف مرفق من نوع "حمام سباحة" أولاً، ثم قم بتكوينه هنا.</p> <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> <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> </div>
<?php endif; ?> <?php endif; ?>
......
This diff is collapsed.
<?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