Commit 3bea96d1 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(swimming): add pool reservation wizard — lane rentals & session cards

Creates a complete reservation system for freelance coaches and individuals
booking pool lanes or session cards. Includes:
- 3-step wizard (booker info → package selection → confirmation)
- Overflow handling when participant count exceeds lane capacity
- Reservations create real sa_groups records for Mirror grid integration
- Session tracking (used/remaining) with manual + automatic decrement
- Payment request integration with cashier module
- List view with search/filter and detail view with progress tracker

New tables: sa_pool_reservations, sa_groups.source_type/pool_reservation_id
New programs: SWIM-LANE-RENTAL, SWIM-CARDS (program_type='rental')
Updated pricing in sa_academy_pricing for lanes and cards
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d2162963
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Swimming;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\PoolReservationService;
class PoolReservationController extends Controller
{
public function list(Request $request): Response
{
$this->authorize('sa.pool_reservation.view');
$db = App::getInstance()->db();
$search = trim((string) $request->get('search', ''));
$status = trim((string) $request->get('status', ''));
$page = max(1, (int) $request->get('page', 1));
$limit = 20;
$offset = ($page - 1) * $limit;
$where = "1=1";
$params = [];
if ($search !== '') {
$where .= " AND (r.booker_name LIKE ? OR r.reservation_number LIKE ?)";
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
}
if ($status !== '' && in_array($status, ['active', 'completed', 'cancelled', 'expired'], true)) {
$where .= " AND r.status = ?";
$params[] = $status;
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as total FROM sa_pool_reservations r WHERE {$where}",
$params
);
$total = (int) ($countRow['total'] ?? 0);
$reservations = $db->select(
"SELECT r.*, g.name_ar as group_name
FROM sa_pool_reservations r
LEFT JOIN sa_groups g ON g.id = r.group_id
WHERE {$where}
ORDER BY r.created_at DESC
LIMIT {$limit} OFFSET {$offset}",
$params
);
return $this->view('SportsActivity.Views.swimming.pool-reservations.index', [
'reservations' => $reservations,
'total' => $total,
'page' => $page,
'totalPages' => (int) ceil($total / $limit),
'search' => $search,
'status' => $status,
]);
}
public function wizard(Request $request): Response
{
$this->authorize('sa.pool_reservation.create');
$db = App::getInstance()->db();
$recentReservations = $db->select(
"SELECT r.id, r.reservation_number, r.booker_name, r.reservation_type,
r.sessions_total, r.sessions_used, r.total_amount, r.status, r.payment_status,
r.created_at
FROM sa_pool_reservations r
ORDER BY r.created_at DESC LIMIT 10"
);
return $this->view('SportsActivity.Views.swimming.pool-reservations.wizard', [
'recentReservations' => $recentReservations,
]);
}
public function show(Request $request, string $id): Response
{
$this->authorize('sa.pool_reservation.view');
$db = App::getInstance()->db();
$reservation = $db->selectOne(
"SELECT r.*, g.name_ar as group_name, g.code as group_code
FROM sa_pool_reservations r
LEFT JOIN sa_groups g ON g.id = r.group_id
WHERE r.id = ?",
[(int) $id]
);
if (!$reservation) {
throw new \RuntimeException('الحجز غير موجود', 404);
}
$bookings = $db->select(
"SELECT b.id, b.booking_date, b.start_time, b.end_time, b.status,
fu.name_ar as unit_name
FROM sa_bookings b
LEFT JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
WHERE b.group_id = ?
ORDER BY b.booking_date DESC, b.start_time DESC
LIMIT 50",
[(int) $reservation['group_id']]
);
return $this->view('SportsActivity.Views.swimming.pool-reservations.show', [
'reservation' => $reservation,
'bookings' => $bookings,
]);
}
public function pricing(Request $request): Response
{
$this->authorize('sa.pool_reservation.create');
$options = PoolReservationService::getPricingOptions();
return $this->json(['success' => true, 'data' => $options]);
}
public function capacityCheck(Request $request): Response
{
$this->authorize('sa.pool_reservation.create');
$reservationType = (string) $request->post('reservation_type', '');
$participantCount = (int) $request->post('participant_count', 1);
$result = PoolReservationService::checkCapacity($reservationType, $participantCount);
return $this->json(['success' => true, 'data' => $result]);
}
public function store(Request $request): Response
{
$this->authorize('sa.pool_reservation.create');
$data = [
'booker_name' => $request->post('booker_name', ''),
'booker_phone' => $request->post('booker_phone', ''),
'booker_type' => $request->post('booker_type', 'freelance_coach'),
'participant_count' => $request->post('participant_count', 1),
'reservation_type' => $request->post('reservation_type', ''),
'pricing_id' => $request->post('pricing_id', 0),
'sessions_total' => $request->post('sessions_total', 0),
'unit_price' => $request->post('unit_price', 0),
'notes' => $request->post('notes', ''),
'overflow_type' => $request->post('overflow_type'),
'overflow_cards_count' => $request->post('overflow_cards_count', 0),
'overflow_amount' => $request->post('overflow_amount', 0),
];
$result = PoolReservationService::create($data);
if (!$result['success']) {
return $this->json($result, 422);
}
return $this->json($result);
}
public function recordSession(Request $request, string $id): Response
{
$this->authorize('sa.pool_reservation.manage');
$result = PoolReservationService::recordSessionUsage((int) $id);
if (!$result['success']) {
return $this->json($result, 422);
}
return $this->redirect('/sa/swimming/pool-reservations/' . $id)
->withSuccess('تم تسجيل حصة — المتبقي: ' . $result['sessions_remaining']);
}
public function cancel(Request $request, string $id): Response
{
$this->authorize('sa.pool_reservation.manage');
$reason = trim((string) $request->post('reason', ''));
$result = PoolReservationService::cancel((int) $id, $reason);
if (!$result['success']) {
return $this->redirect('/sa/swimming/pool-reservations/' . $id)
->withError($result['error']);
}
return $this->redirect('/sa/swimming/pool-reservations')
->withSuccess('تم إلغاء الحجز بنجاح');
}
}
...@@ -333,6 +333,16 @@ return [ ...@@ -333,6 +333,16 @@ return [
['GET', '/sa/swimming/assign/{id:\d+}', 'SportsActivity\Controllers\Swimming\SwimmingAssignmentController@index', ['auth'], 'sa.swimming.assign'], ['GET', '/sa/swimming/assign/{id:\d+}', 'SportsActivity\Controllers\Swimming\SwimmingAssignmentController@index', ['auth'], 'sa.swimming.assign'],
['POST', '/sa/swimming/assign/{id:\d+}/group', 'SportsActivity\Controllers\Swimming\SwimmingAssignmentController@assign', ['auth', 'csrf'], 'sa.swimming.assign'], ['POST', '/sa/swimming/assign/{id:\d+}/group', 'SportsActivity\Controllers\Swimming\SwimmingAssignmentController@assign', ['auth', 'csrf'], 'sa.swimming.assign'],
// ─── Pool Reservations ──────────────────────────────────────────────────────
['GET', '/sa/swimming/pool-reservations', 'SportsActivity\Controllers\Swimming\PoolReservationController@list', ['auth'], 'sa.pool_reservation.view'],
['GET', '/sa/swimming/pool-reservations/wizard', 'SportsActivity\Controllers\Swimming\PoolReservationController@wizard', ['auth'], 'sa.pool_reservation.create'],
['GET', '/sa/swimming/pool-reservations/{id:\d+}', 'SportsActivity\Controllers\Swimming\PoolReservationController@show', ['auth'], 'sa.pool_reservation.view'],
['POST', '/sa/swimming/pool-reservations/{id:\d+}/use-session', 'SportsActivity\Controllers\Swimming\PoolReservationController@recordSession', ['auth', 'csrf'], 'sa.pool_reservation.manage'],
['POST', '/sa/swimming/pool-reservations/{id:\d+}/cancel', 'SportsActivity\Controllers\Swimming\PoolReservationController@cancel', ['auth', 'csrf'], 'sa.pool_reservation.manage'],
['GET', '/api/sa/swimming/pool-reservations/pricing', 'SportsActivity\Controllers\Swimming\PoolReservationController@pricing', ['auth'], 'sa.pool_reservation.create'],
['POST', '/api/sa/swimming/pool-reservations/capacity-check', 'SportsActivity\Controllers\Swimming\PoolReservationController@capacityCheck', ['auth', 'csrf'], 'sa.pool_reservation.create'],
['POST', '/api/sa/swimming/pool-reservations/store', 'SportsActivity\Controllers\Swimming\PoolReservationController@store', ['auth', 'csrf'], 'sa.pool_reservation.create'],
// ─── Academy Pricing ──────────────────────────────────────────────────────── // ─── Academy Pricing ────────────────────────────────────────────────────────
['GET', '/sa/academy-pricing', 'SportsActivity\Controllers\AcademyPricingController@index', ['auth'], 'sa.pricing.view'], ['GET', '/sa/academy-pricing', 'SportsActivity\Controllers\AcademyPricingController@index', ['auth'], 'sa.pricing.view'],
['GET', '/sa/academy-pricing/academies', 'SportsActivity\Controllers\AcademyPricingController@academies', ['auth'], 'sa.pricing.view'], ['GET', '/sa/academy-pricing/academies', 'SportsActivity\Controllers\AcademyPricingController@academies', ['auth'], 'sa.pricing.view'],
......
This diff is collapsed.
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجوزات السباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming/pool-reservations/wizard" class="btn btn-primary"><i data-lucide="plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حجز جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:16px;padding:16px;">
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="search" class="form-input" style="padding:10px;font-size:13px;border-radius:8px;" value="<?= e($search) ?>" placeholder="اسم الحاجز أو رقم الحجز...">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-input" style="padding:10px;font-size:13px;border-radius:8px;">
<option value="">الكل</option>
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="completed" <?= $status === 'completed' ? 'selected' : '' ?>>مكتمل</option>
<option value="cancelled" <?= $status === 'cancelled' ? 'selected' : '' ?>>ملغي</option>
<option value="expired" <?= $status === 'expired' ? 'selected' : '' ?>>منتهي</option>
</select>
</div>
<button type="submit" class="btn btn-outline" style="padding:10px 16px;font-size:13px;border-radius:8px;">بحث</button>
</form>
</div>
<!-- Stats -->
<?php
$activeCount = 0; $totalRevenue = 0;
foreach ($reservations as $r) {
if ($r['status'] === 'active') $activeCount++;
$totalRevenue += (float) $r['total_amount'];
}
?>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:16px;">
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:24px;font-weight:800;color:#2563EB;"><?= $total ?></div>
<div style="font-size:12px;color:#6B7280;">إجمالي الحجوزات</div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:24px;font-weight:800;color:#059669;"><?= $activeCount ?></div>
<div style="font-size:12px;color:#6B7280;">نشطة</div>
</div>
</div>
<!-- Table -->
<div class="card">
<div style="overflow-x:auto;">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم الحجز</th>
<th>الحاجز</th>
<th>النوع</th>
<th>الحصص</th>
<th>المبلغ</th>
<th>الحالة</th>
<th>الدفع</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php if (empty($reservations)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد حجوزات</td></tr>
<?php else: ?>
<?php foreach ($reservations as $r): ?>
<tr>
<td><a href="/sa/swimming/pool-reservations/<?= (int) $r['id'] ?>" style="color:#2563EB;font-weight:600;text-decoration:none;"><?= e($r['reservation_number']) ?></a></td>
<td>
<div style="font-weight:600;"><?= e($r['booker_name']) ?></div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $r['participant_count'] ?> مشارك</div>
</td>
<td><?php
$typeLabels = ['lane_50m' => 'حارة 50م', 'lane_25m' => 'حارة 25م', 'lane_mix' => 'ميكس', 'cards' => 'كروت'];
echo $typeLabels[$r['reservation_type']] ?? $r['reservation_type'];
?></td>
<td>
<span style="font-weight:700;"><?= (int) $r['sessions_used'] ?></span>/<span style="color:#6B7280;"><?= (int) $r['sessions_total'] ?></span>
</td>
<td style="font-weight:600;"><?= number_format((float) $r['total_amount'], 0) ?> ج.م</td>
<td><?php
$sc = ['active' => '#059669', 'completed' => '#6B7280', 'cancelled' => '#DC2626', 'expired' => '#D97706'];
$sl = ['active' => 'نشط', 'completed' => 'مكتمل', 'cancelled' => 'ملغي', 'expired' => 'منتهي'];
$c = $sc[$r['status']] ?? '#6B7280'; $l = $sl[$r['status']] ?? $r['status'];
?><span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= $c ?>15;color:<?= $c ?>;"><?= $l ?></span></td>
<td><?php
$pc = ['paid' => '#059669', 'pending' => '#D97706', 'unpaid' => '#DC2626', 'partial' => '#2563EB'];
$pl = ['paid' => 'مدفوع', 'pending' => 'معلق', 'unpaid' => 'غير مدفوع', 'partial' => 'جزئي'];
$pcc = $pc[$r['payment_status']] ?? '#6B7280'; $pll = $pl[$r['payment_status']] ?? $r['payment_status'];
?><span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= $pcc ?>15;color:<?= $pcc ?>;"><?= $pll ?></span></td>
<td style="font-size:12px;color:#6B7280;"><?= date('Y-m-d', strtotime($r['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<div style="padding:12px 20px;border-top:1px solid #E5E7EB;display:flex;justify-content:center;gap:6px;">
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
<a href="?page=<?= $p ?>&search=<?= urlencode($search) ?>&status=<?= urlencode($status) ?>"
style="padding:6px 12px;border-radius:6px;font-size:13px;text-decoration:none;<?= $p === $page ? 'background:#2563EB;color:#fff;font-weight:700;' : 'background:#F3F4F6;color:#374151;' ?>"><?= $p ?></a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجز <?= e($reservation['reservation_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming/pool-reservations" 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
$r = $reservation;
$typeLabels = ['lane_50m' => 'إيجار حارة 50م', 'lane_25m' => 'إيجار حارة 25م', 'lane_mix' => 'إيجار حارة ميكس', 'cards' => 'كروت حصص'];
$statusColors = ['active' => '#059669', 'completed' => '#6B7280', 'cancelled' => '#DC2626', 'expired' => '#D97706'];
$statusLabels = ['active' => 'نشط', 'completed' => 'مكتمل', 'cancelled' => 'ملغي', 'expired' => 'منتهي'];
$payColors = ['paid' => '#059669', 'pending' => '#D97706', 'unpaid' => '#DC2626', 'partial' => '#2563EB'];
$payLabels = ['paid' => 'مدفوع', 'pending' => 'معلق', 'unpaid' => 'غير مدفوع', 'partial' => 'جزئي'];
$sessionsUsed = (int) $r['sessions_used'];
$sessionsTotal = (int) $r['sessions_total'];
$sessionsRemaining = $sessionsTotal - $sessionsUsed;
$progress = $sessionsTotal > 0 ? round(($sessionsUsed / $sessionsTotal) * 100) : 0;
?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;">
<!-- Info Card -->
<div class="card" style="padding:20px;">
<h4 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#1A1A2E;">بيانات الحجز</h4>
<table style="width:100%;font-size:13px;">
<tr><td style="padding:8px 0;color:#6B7280;width:120px;">رقم الحجز</td><td style="padding:8px 0;font-weight:600;"><?= e($r['reservation_number']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الحاجز</td><td style="padding:8px 0;font-weight:600;"><?= e($r['booker_name']) ?></td></tr>
<?php if ($r['booker_phone']): ?><tr><td style="padding:8px 0;color:#6B7280;">التليفون</td><td style="padding:8px 0;"><?= e($r['booker_phone']) ?></td></tr><?php endif; ?>
<tr><td style="padding:8px 0;color:#6B7280;">النوع</td><td style="padding:8px 0;font-weight:600;"><?= $typeLabels[$r['reservation_type']] ?? $r['reservation_type'] ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">المشاركين</td><td style="padding:8px 0;"><?= (int) $r['participant_count'] ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">من</td><td style="padding:8px 0;"><?= e($r['start_date']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">حتى</td><td style="padding:8px 0;"><?= e($r['expiry_date'] ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الحالة</td><td style="padding:8px 0;"><span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= $statusColors[$r['status']] ?? '#6B7280' ?>15;color:<?= $statusColors[$r['status']] ?? '#6B7280' ?>;"><?= $statusLabels[$r['status']] ?? $r['status'] ?></span></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الدفع</td><td style="padding:8px 0;"><span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= $payColors[$r['payment_status']] ?? '#6B7280' ?>15;color:<?= $payColors[$r['payment_status']] ?? '#6B7280' ?>;"><?= $payLabels[$r['payment_status']] ?? $r['payment_status'] ?></span></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">المبلغ</td><td style="padding:8px 0;font-weight:800;font-size:16px;color:#059669;"><?= number_format((float) $r['total_amount'], 0) ?> ج.م</td></tr>
<?php if ($r['notes']): ?><tr><td style="padding:8px 0;color:#6B7280;">ملاحظات</td><td style="padding:8px 0;"><?= e($r['notes']) ?></td></tr><?php endif; ?>
<tr><td style="padding:8px 0;color:#6B7280;">المجموعة</td><td style="padding:8px 0;"><a href="/sa/groups" style="color:#2563EB;text-decoration:none;"><?= e($r['group_name'] ?? $r['group_code'] ?? '—') ?></a></td></tr>
</table>
</div>
<!-- Sessions Progress Card -->
<div class="card" style="padding:20px;">
<h4 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#1A1A2E;">استهلاك الحصص</h4>
<div style="text-align:center;margin-bottom:20px;">
<div style="font-size:48px;font-weight:800;color:#2563EB;"><?= $sessionsRemaining ?></div>
<div style="font-size:13px;color:#6B7280;">حصة متبقية من <?= $sessionsTotal ?></div>
</div>
<div style="background:#E5E7EB;border-radius:8px;height:12px;overflow:hidden;margin-bottom:8px;">
<div style="background:#2563EB;height:100%;width:<?= $progress ?>%;border-radius:8px;transition:width .3s;"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:#6B7280;">
<span>مستخدم: <?= $sessionsUsed ?></span>
<span><?= $progress ?>%</span>
</div>
<?php if ($r['status'] === 'active'): ?>
<div style="margin-top:20px;display:flex;gap:10px;flex-wrap:wrap;">
<form method="post" action="/sa/swimming/pool-reservations/<?= (int) $r['id'] ?>/use-session" style="display:inline;">
<?= \App\Core\CSRF::field() ?>
<button type="submit" class="btn btn-primary" style="padding:10px 20px;font-size:13px;border-radius:8px;" onclick="return confirm('تسجيل حصة مستخدمة؟')">
<i data-lucide="minus-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تسجيل حصة
</button>
</form>
<form method="post" action="/sa/swimming/pool-reservations/<?= (int) $r['id'] ?>/cancel" style="display:inline;">
<?= \App\Core\CSRF::field() ?>
<button type="submit" class="btn btn-outline" style="padding:10px 20px;font-size:13px;border-radius:8px;color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إلغاء هذا الحجز؟')">
<i data-lucide="x-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إلغاء الحجز
</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
<!-- Scheduled Sessions -->
<div class="card">
<div style="padding:14px 20px;border-bottom:1px solid #E5E7EB;">
<h4 style="margin:0;font-size:14px;font-weight:600;">الحصص المجدولة</h4>
</div>
<?php if (empty($bookings)): ?>
<div style="padding:40px;text-align:center;color:#9CA3AF;">
<i data-lucide="calendar-x" style="width:32px;height:32px;margin-bottom:8px;"></i>
<div>لم يتم جدولة حصص بعد — اسحب المجموعة في المراية لتحديد المواعيد</div>
</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="data-table" style="width:100%;">
<thead><tr><th>التاريخ</th><th>الوقت</th><th>المرفق</th><th>الحالة</th></tr></thead>
<tbody>
<?php foreach ($bookings as $bk): ?>
<tr>
<td><?= e($bk['booking_date']) ?></td>
<td><?= e(substr($bk['start_time'], 0, 5)) ?><?= e(substr($bk['end_time'], 0, 5)) ?></td>
<td><?= e($bk['unit_name'] ?? '—') ?></td>
<td><?php
$bsc = ['confirmed'=>'#2563EB','checked_in'=>'#059669','completed'=>'#6B7280','cancelled'=>'#DC2626','pending'=>'#D97706'];
$bsl = ['confirmed'=>'مؤكد','checked_in'=>'حاضر','completed'=>'مكتمل','cancelled'=>'ملغي','pending'=>'معلق'];
$bcc = $bsc[$bk['status']] ?? '#6B7280'; $bll = $bsl[$bk['status']] ?? $bk['status'];
?><span style="padding:2px 6px;border-radius:4px;font-size:11px;background:<?= $bcc ?>15;color:<?= $bcc ?>;"><?= $bll ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
...@@ -52,6 +52,7 @@ MenuRegistry::register('sports_activity', [ ...@@ -52,6 +52,7 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'مدربين السباحة', 'label_en' => 'Swimming Coaches','route' => '/sa/swimming/coaches','permission' => 'sa.swimming.coach_manage','order' => 31.5], ['label_ar' => 'مدربين السباحة', 'label_en' => 'Swimming Coaches','route' => '/sa/swimming/coaches','permission' => 'sa.swimming.coach_manage','order' => 31.5],
['label_ar' => 'تسجيل لاعب سباحة', 'label_en' => 'Register Swimmer','route' => '/sa/swimming/register','permission' => 'sa.swimming.register','order' => 32], ['label_ar' => 'تسجيل لاعب سباحة', 'label_en' => 'Register Swimmer','route' => '/sa/swimming/register','permission' => 'sa.swimming.register','order' => 32],
['label_ar' => 'تعيين في مجموعة', 'label_en' => 'Assign to Group', 'route' => '/sa/swimming/assign','permission' => 'sa.swimming.assign', 'order' => 33], ['label_ar' => 'تعيين في مجموعة', 'label_en' => 'Assign to Group', 'route' => '/sa/swimming/assign','permission' => 'sa.swimming.assign', 'order' => 33],
['label_ar' => 'حجوزات السباحة', 'label_en' => 'Pool Reservations','route' => '/sa/swimming/pool-reservations','permission' => 'sa.pool_reservation.view','order' => 34],
], ],
]); ]);
...@@ -125,6 +126,9 @@ PermissionRegistry::register('sports_activity', [ ...@@ -125,6 +126,9 @@ PermissionRegistry::register('sports_activity', [
'sa.coach_assessment.view' => ['ar' => 'عرض تقييم اللاعبين', 'en' => 'View Player Assessments'], 'sa.coach_assessment.view' => ['ar' => 'عرض تقييم اللاعبين', 'en' => 'View Player Assessments'],
'sa.coach_assessment.manage' => ['ar' => 'إدارة تقييم اللاعبين', 'en' => 'Manage Player Assessments'], 'sa.coach_assessment.manage' => ['ar' => 'إدارة تقييم اللاعبين', 'en' => 'Manage Player Assessments'],
'sa.swimming.coach_manage' => ['ar' => 'إدارة مدربين السباحة', 'en' => 'Manage Swimming Coaches'], 'sa.swimming.coach_manage' => ['ar' => 'إدارة مدربين السباحة', 'en' => 'Manage Swimming Coaches'],
'sa.pool_reservation.view' => ['ar' => 'عرض حجوزات السباحة', 'en' => 'View Pool Reservations'],
'sa.pool_reservation.create' => ['ar' => 'إنشاء حجز سباحة', 'en' => 'Create Pool Reservation'],
'sa.pool_reservation.manage' => ['ar' => 'إدارة حجوزات السباحة', 'en' => 'Manage Pool Reservations'],
]); ]);
// ─── Event Listeners ──────────────────────────────────────────────────────── // ─── Event Listeners ────────────────────────────────────────────────────────
......
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `sa_pool_reservations` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`reservation_number` VARCHAR(50) NOT NULL,
`group_id` BIGINT UNSIGNED NOT NULL,
`booker_name` VARCHAR(300) NOT NULL,
`booker_phone` VARCHAR(30) NULL,
`booker_national_id` VARCHAR(14) NULL,
`booker_type` VARCHAR(30) NOT NULL DEFAULT 'freelance_coach' COMMENT 'freelance_coach, entity, individual',
`participant_count` INT UNSIGNED NOT NULL DEFAULT 1,
`reservation_type` VARCHAR(30) NOT NULL COMMENT 'lane_50m, lane_25m, lane_mix, cards',
`pricing_id` BIGINT UNSIGNED NULL,
`sessions_total` INT UNSIGNED NOT NULL,
`sessions_used` INT UNSIGNED NOT NULL DEFAULT 0,
`sessions_remaining` INT UNSIGNED GENERATED ALWAYS AS (`sessions_total` - `sessions_used`) STORED,
`unit_price` DECIMAL(10,2) NOT NULL,
`total_amount` DECIMAL(10,2) NOT NULL,
`payment_status` VARCHAR(20) NOT NULL DEFAULT 'unpaid' COMMENT 'unpaid, pending, paid, partial',
`payment_request_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, completed, cancelled, expired',
`start_date` DATE NOT NULL,
`expiry_date` DATE NULL,
`cancelled_at` TIMESTAMP NULL,
`cancellation_reason` TEXT NULL,
`has_overflow` TINYINT(1) NOT NULL DEFAULT 0,
`overflow_type` VARCHAR(30) NULL COMMENT 'additional_lane, overflow_cards',
`overflow_cards_count` INT UNSIGNED NOT NULL DEFAULT 0,
`overflow_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`created_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uq_spr_number` (`reservation_number`),
INDEX `idx_spr_group` (`group_id`),
INDEX `idx_spr_booker` (`booker_name`(100)),
INDEX `idx_spr_type` (`reservation_type`),
INDEX `idx_spr_status` (`status`),
INDEX `idx_spr_dates` (`start_date`, `expiry_date`),
CONSTRAINT `fk_spr_group` FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `sa_pool_reservations`",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `sa_groups`
ADD COLUMN `source_type` VARCHAR(30) NULL DEFAULT NULL COMMENT 'NULL=normal, pool_reservation=created by reservation wizard' AFTER `status`,
ADD COLUMN `pool_reservation_id` BIGINT UNSIGNED NULL AFTER `source_type`,
ADD INDEX `idx_sa_grp_source` (`source_type`)
",
'down' => "
ALTER TABLE `sa_groups`
DROP INDEX `idx_sa_grp_source`,
DROP COLUMN `pool_reservation_id`,
DROP COLUMN `source_type`
",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$discipline = $db->selectOne(
"SELECT id FROM sa_disciplines WHERE code = 'SWIMMING' AND is_archived = 0 LIMIT 1"
);
if (!$discipline) {
$discipline = $db->selectOne(
"SELECT id FROM sa_disciplines WHERE name_ar LIKE '%سباحة%' AND is_archived = 0 LIMIT 1"
);
}
$disciplineId = $discipline ? (int) $discipline['id'] : 1;
$existing = $db->selectOne(
"SELECT id FROM sa_programs WHERE code = 'SWIM-LANE-RENTAL'"
);
if (!$existing) {
$db->insert('sa_programs', [
'code' => 'SWIM-LANE-RENTAL',
'name_ar' => 'إيجار حارات السباحة',
'name_en' => 'Pool Lane Rental',
'discipline_id' => $disciplineId,
'program_type' => 'rental',
'session_duration_minutes' => 60,
'sessions_per_week' => 0,
'monthly_fee_member' => 0,
'monthly_fee_nonmember' => 0,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
$existing2 = $db->selectOne(
"SELECT id FROM sa_programs WHERE code = 'SWIM-CARDS'"
);
if (!$existing2) {
$db->insert('sa_programs', [
'code' => 'SWIM-CARDS',
'name_ar' => 'كروت حصص السباحة',
'name_en' => 'Pool Session Cards',
'discipline_id' => $disciplineId,
'program_type' => 'rental',
'session_duration_minutes' => 60,
'sessions_per_week' => 0,
'monthly_fee_member' => 0,
'monthly_fee_nonmember' => 0,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$laneRentals = [
['50m', 12, 4100],
['50m', 8, 2750],
['50m', 4, 1400],
['50m', 1, 410],
['25m', 12, 3000],
['25m', 8, 2100],
['25m', 4, 1200],
['25m', 1, 350],
['mix', 24, 6350],
];
foreach ($laneRentals as [$laneType, $sessions, $price]) {
$existing = $db->selectOne(
"SELECT id FROM sa_academy_pricing WHERE category = 'lane_rental' AND lane_type = ? AND sessions_per_month = ? LIMIT 1",
[$laneType, $sessions]
);
if ($existing) {
$db->update('sa_academy_pricing', [
'price_member' => $price,
'price_nonmember' => $price,
'is_active' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $existing['id']]);
} else {
$db->insert('sa_academy_pricing', [
'academy_code' => 'SWIMMING',
'academy_name_ar' => 'أكاديمية السباحة',
'discipline_code' => 'SWIMMING',
'category' => 'lane_rental',
'level_name_ar' => 'حارة ' . $laneType,
'lane_type' => $laneType,
'total_sessions' => $sessions,
'sessions_per_month' => $sessions,
'price_member' => $price,
'price_nonmember' => $price,
'billing_period' => 'monthly',
'is_active' => 1,
'effective_from' => '2025-09-01',
'sort_order' => 0,
'season' => '2025_2026',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
$sessionCards = [
[24, 1450],
[12, 800],
[8, 580],
[6, 520],
[4, 440],
[1, 140],
];
foreach ($sessionCards as [$sessions, $price]) {
$existing = $db->selectOne(
"SELECT id FROM sa_academy_pricing WHERE category = 'session_card' AND total_sessions = ? LIMIT 1",
[$sessions]
);
if ($existing) {
$db->update('sa_academy_pricing', [
'price_member' => $price,
'price_nonmember' => $price,
'is_active' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $existing['id']]);
} else {
$db->insert('sa_academy_pricing', [
'academy_code' => 'SWIMMING',
'academy_name_ar' => 'أكاديمية السباحة',
'discipline_code' => 'SWIMMING',
'category' => 'session_card',
'level_name_ar' => 'كارت ' . $sessions . ' حصة',
'total_sessions' => $sessions,
'price_member' => $price,
'price_nonmember' => $price,
'billing_period' => 'per_card',
'is_active' => 1,
'effective_from' => '2025-09-01',
'sort_order' => 0,
'season' => '2025_2026',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
};
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