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'],
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
use App\Modules\Cashier\Services\PaymentRequestService;
final class PoolReservationService
{
private const LANE_CAPACITIES = [
'50m' => 12,
'25m' => 8,
'mix' => 24,
];
private const RESERVATION_TYPE_MAP = [
'lane_50m' => '50m',
'lane_25m' => '25m',
'lane_mix' => 'mix',
];
public static function getPricingOptions(): array
{
return [
'lane_rentals' => AcademyPricingService::getLaneRentalPricing(),
'session_cards' => AcademyPricingService::getSessionCardPricing(),
];
}
public static function checkCapacity(string $reservationType, int $participantCount): array
{
$laneType = self::RESERVATION_TYPE_MAP[$reservationType] ?? null;
if (!$laneType || $reservationType === 'cards') {
return ['fits' => true, 'overflow' => 0, 'lane_capacity' => 0, 'suggestions' => []];
}
$laneCapacity = self::LANE_CAPACITIES[$laneType] ?? 12;
$overflow = max(0, $participantCount - $laneCapacity);
$suggestions = [];
if ($overflow > 0) {
$suggestions[] = [
'type' => 'additional_lane',
'label_ar' => 'حجز حارة إضافية',
];
$suggestions[] = [
'type' => 'overflow_cards',
'label_ar' => 'إضافة ' . $overflow . ' كروت للمشاركين الزائدين',
'cards_needed' => $overflow,
];
}
return [
'fits' => $overflow === 0,
'lane_capacity' => $laneCapacity,
'overflow' => $overflow,
'suggestions' => $suggestions,
];
}
public static function create(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$bookerName = trim((string) ($data['booker_name'] ?? ''));
$bookerPhone = trim((string) ($data['booker_phone'] ?? ''));
$bookerType = (string) ($data['booker_type'] ?? 'freelance_coach');
$participantCount = max(1, (int) ($data['participant_count'] ?? 1));
$reservationType = (string) ($data['reservation_type'] ?? '');
$pricingId = (int) ($data['pricing_id'] ?? 0);
$sessionsTotal = (int) ($data['sessions_total'] ?? 0);
$unitPrice = (float) ($data['unit_price'] ?? 0);
$notes = trim((string) ($data['notes'] ?? ''));
$overflowType = $data['overflow_type'] ?? null;
$overflowCardsCount = (int) ($data['overflow_cards_count'] ?? 0);
$overflowAmount = (float) ($data['overflow_amount'] ?? 0);
$hasOverflow = ($overflowType !== null && ($overflowCardsCount > 0 || $overflowType === 'additional_lane'));
if ($bookerName === '') {
return ['success' => false, 'error' => 'اسم الحاجز مطلوب'];
}
if (!in_array($reservationType, ['lane_50m', 'lane_25m', 'lane_mix', 'cards'], true)) {
return ['success' => false, 'error' => 'نوع الحجز غير صالح'];
}
if ($sessionsTotal < 1) {
return ['success' => false, 'error' => 'عدد الحصص غير صالح'];
}
if ($unitPrice <= 0) {
return ['success' => false, 'error' => 'السعر غير صالح'];
}
$totalAmount = $unitPrice + $overflowAmount;
$programCode = str_starts_with($reservationType, 'lane_') ? 'SWIM-LANE-RENTAL' : 'SWIM-CARDS';
$program = $db->selectOne(
"SELECT id FROM sa_programs WHERE code = ? AND is_active = 1 LIMIT 1",
[$programCode]
);
if (!$program) {
return ['success' => false, 'error' => 'برنامج الإيجار غير موجود — يرجى تشغيل المايقريشن'];
}
$programId = (int) $program['id'];
$typeLabels = [
'lane_50m' => 'إيجار حارة 50م',
'lane_25m' => 'إيجار حارة 25م',
'lane_mix' => 'إيجار حارة ميكس',
'cards' => 'كروت حصص',
];
$typeLabel = $typeLabels[$reservationType] ?? $reservationType;
$groupName = $bookerName . ' — ' . $typeLabel . ' (' . $sessionsTotal . ' حصة)';
$groupCode = 'GRP-PR-' . strtoupper(substr(md5((string) microtime(true)), 0, 6));
$db->beginTransaction();
try {
$groupId = $db->insert('sa_groups', [
'code' => $groupCode,
'name_ar' => $groupName,
'program_id' => $programId,
'coach_id' => null,
'max_capacity' => $participantCount,
'current_count' => $participantCount,
'is_full' => 0,
'status' => 'active',
'source_type' => 'pool_reservation',
'is_archived' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
$reservationNumber = self::generateNumber();
$startDate = date('Y-m-d');
$expiryDate = date('Y-m-d', strtotime('+2 months'));
$reservationId = $db->insert('sa_pool_reservations', [
'reservation_number' => $reservationNumber,
'group_id' => $groupId,
'booker_name' => $bookerName,
'booker_phone' => $bookerPhone ?: null,
'booker_type' => $bookerType,
'participant_count' => $participantCount,
'reservation_type' => $reservationType,
'pricing_id' => $pricingId > 0 ? $pricingId : null,
'sessions_total' => $sessionsTotal,
'sessions_used' => 0,
'unit_price' => $unitPrice,
'total_amount' => $totalAmount,
'payment_status' => 'pending',
'status' => 'active',
'start_date' => $startDate,
'expiry_date' => $expiryDate,
'has_overflow' => $hasOverflow ? 1 : 0,
'overflow_type' => $hasOverflow ? $overflowType : null,
'overflow_cards_count' => $overflowCardsCount,
'overflow_amount' => $overflowAmount,
'notes' => $notes ?: null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$db->update('sa_groups', [
'pool_reservation_id' => $reservationId,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$groupId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل إنشاء الحجز: ' . $e->getMessage()];
}
if ($totalAmount > 0) {
PaymentRequestService::createRequest([
'member_id' => 0,
'payment_type' => 'pool_reservation',
'amount' => (string) $totalAmount,
'description_ar' => 'حجز سباحة — ' . $bookerName . ' — ' . $typeLabel . ' (' . $sessionsTotal . ' حصة)',
'related_entity_type' => 'sa_pool_reservations',
'related_entity_id' => $reservationId,
]);
}
return [
'success' => true,
'reservation_id' => $reservationId,
'reservation_number' => $reservationNumber,
'group_id' => $groupId,
'total_amount' => $totalAmount,
];
}
public static function recordSessionUsage(int $reservationId): array
{
$db = App::getInstance()->db();
$reservation = $db->selectOne(
"SELECT id, sessions_total, sessions_used, status FROM sa_pool_reservations WHERE id = ?",
[$reservationId]
);
if (!$reservation) {
return ['success' => false, 'error' => 'الحجز غير موجود'];
}
if ($reservation['status'] !== 'active') {
return ['success' => false, 'error' => 'الحجز غير نشط'];
}
$used = (int) $reservation['sessions_used'];
$total = (int) $reservation['sessions_total'];
if ($used >= $total) {
return ['success' => false, 'error' => 'تم استهلاك جميع الحصص'];
}
$newUsed = $used + 1;
$updates = [
'sessions_used' => $newUsed,
'updated_at' => date('Y-m-d H:i:s'),
];
if ($newUsed >= $total) {
$updates['status'] = 'completed';
}
$db->update('sa_pool_reservations', $updates, 'id = ?', [$reservationId]);
if ($newUsed >= $total) {
$db->update('sa_groups', [
'status' => 'completed',
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], 'pool_reservation_id = ?', [$reservationId]);
}
return [
'success' => true,
'sessions_used' => $newUsed,
'sessions_remaining' => $total - $newUsed,
];
}
public static function cancel(int $reservationId, string $reason = ''): array
{
$db = App::getInstance()->db();
$reservation = $db->selectOne(
"SELECT id, group_id, status FROM sa_pool_reservations WHERE id = ?",
[$reservationId]
);
if (!$reservation) {
return ['success' => false, 'error' => 'الحجز غير موجود'];
}
if ($reservation['status'] !== 'active') {
return ['success' => false, 'error' => 'الحجز غير نشط — لا يمكن الإلغاء'];
}
$db->update('sa_pool_reservations', [
'status' => 'cancelled',
'cancelled_at' => date('Y-m-d H:i:s'),
'cancellation_reason' => $reason ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$reservationId]);
$db->update('sa_groups', [
'status' => 'cancelled',
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $reservation['group_id']]);
return ['success' => true];
}
public static function generateNumber(): string
{
$db = App::getInstance()->db();
$prefix = 'PR-' . date('Ymd') . '-';
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(reservation_number, " . (strlen($prefix) + 1) . ") AS UNSIGNED)) as max_num
FROM sa_pool_reservations WHERE reservation_number LIKE ?",
[$prefix . '%']
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return $prefix . str_pad((string) $next, 4, '0', STR_PAD_LEFT);
}
}
<?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(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجز سباحة جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming/pool-reservations" class="btn btn-outline"><i data-lucide="list" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> قائمة الحجوزات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div id="reservationWizard">
<!-- Progress Bar -->
<div class="card" style="margin-bottom:16px;padding:16px;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:4px;">
<?php
$steps = ['بيانات الحاجز', 'نوع الحجز', 'التأكيد'];
foreach ($steps as $i => $label):
$num = $i + 1;
?>
<div style="display:flex;align-items:center;flex:1;<?= $i < count($steps) - 1 ? '' : 'flex:0;' ?>">
<div id="stepCircle<?= $num ?>" style="width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;background:<?= $num === 1 ? '#2563EB;color:#fff' : '#E5E7EB;color:#6B7280' ?>;flex-shrink:0;"><?= $num ?></div>
<div style="font-size:11px;margin-right:4px;margin-left:4px;color:#6B7280;white-space:nowrap;"><?= $label ?></div>
<?php if ($i < count($steps) - 1): ?>
<div style="flex:1;height:2px;background:#E5E7EB;margin:0 4px;"></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Step 1: Booker Info -->
<div id="step1" class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="user" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> بيانات الحاجز</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr;gap:14px;">
<div>
<label class="form-label">اسم المدرب / الحاجز <span style="color:#DC2626;">*</span></label>
<input type="text" id="prBookerName" class="form-input" style="padding:14px;font-size:15px;border-radius:10px;" placeholder="اسم المدرب الحر أو الشخص">
</div>
<div>
<label class="form-label">رقم التليفون</label>
<input type="text" id="prBookerPhone" class="form-input" style="padding:14px;font-size:15px;border-radius:10px;direction:ltr;text-align:right;" placeholder="اختياري">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div>
<label class="form-label">نوع الحاجز</label>
<select id="prBookerType" class="form-input" style="padding:14px;font-size:14px;border-radius:10px;">
<option value="freelance_coach">مدرب حر</option>
<option value="entity">مؤسسة</option>
<option value="individual">فرد</option>
</select>
</div>
<div>
<label class="form-label">عدد المشاركين <span style="color:#DC2626;">*</span></label>
<input type="number" id="prParticipants" class="form-input" style="padding:14px;font-size:18px;border-radius:10px;direction:ltr;text-align:center;font-weight:700;" value="1" min="1" max="100">
</div>
</div>
</div>
<button type="button" id="btnStep1Next" class="btn btn-primary" style="width:100%;margin-top:20px;padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;" disabled>
<i data-lucide="arrow-left" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> التالي — اختيار نوع الحجز
</button>
</div>
</div>
<!-- Step 2: Package Selection -->
<div id="step2" class="card" style="margin-bottom:16px;display:none;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="layers" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> اختيار نوع الحجز</h3>
</div>
<div style="padding:20px;">
<!-- Overflow Alert (hidden by default) -->
<div id="overflowAlert" style="display:none;padding:14px;background:#FEF3C7;border:1px solid #F59E0B;border-radius:10px;margin-bottom:16px;">
<div style="font-weight:700;color:#92400E;margin-bottom:8px;"><i data-lucide="alert-triangle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> <span id="overflowText"></span></div>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:6px;padding:10px 14px;background:#fff;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;">
<input type="radio" name="overflowOption" value="additional_lane"> حجز حارة إضافية
</label>
<label id="overflowCardsLabel" style="display:flex;align-items:center;gap:6px;padding:10px 14px;background:#fff;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;">
<input type="radio" name="overflowOption" value="overflow_cards"> <span id="overflowCardsText">إضافة كروت</span>
</label>
</div>
<div id="overflowCardsPicker" style="display:none;margin-top:12px;padding:12px;background:#fff;border-radius:8px;">
<label class="form-label" style="font-size:12px;">اختر باقة الكروت للزائدين:</label>
<div id="overflowCardsOptions" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-top:8px;"></div>
</div>
</div>
<!-- Lane Rentals Section -->
<div style="margin-bottom:20px;">
<h4 style="margin:0 0 12px;font-size:15px;font-weight:700;color:#1A1A2E;padding-bottom:8px;border-bottom:2px solid #2563EB;">
<i data-lucide="waves" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#2563EB;"></i> إيجار الحارات
</h4>
<div id="laneRentalGrid" style="display:grid;grid-template-columns:1fr;gap:12px;">
<div style="text-align:center;padding:20px;color:#9CA3AF;">جاري التحميل...</div>
</div>
</div>
<!-- Session Cards Section -->
<div style="margin-bottom:20px;">
<h4 style="margin:0 0 12px;font-size:15px;font-weight:700;color:#1A1A2E;padding-bottom:8px;border-bottom:2px solid #059669;">
<i data-lucide="credit-card" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#059669;"></i> الحصص والكروت
</h4>
<div id="sessionCardsGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;">
<div style="text-align:center;padding:20px;color:#9CA3AF;">جاري التحميل...</div>
</div>
</div>
<!-- Selection Summary -->
<div id="selectionSummary" style="display:none;padding:14px;background:#EFF6FF;border:1px solid #BFDBFE;border-radius:10px;margin-bottom:16px;">
<div style="font-size:13px;color:#1D4ED8;font-weight:600;">الاختيار:</div>
<div id="summaryText" style="font-size:15px;font-weight:700;margin-top:4px;"></div>
<div id="summaryPrice" style="font-size:22px;font-weight:800;color:#059669;margin-top:6px;"></div>
</div>
<button type="button" id="btnStep2Next" class="btn btn-primary" style="width:100%;padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;" disabled>
<i data-lucide="arrow-left" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> التالي — تأكيد الحجز
</button>
<button type="button" id="btnStep2Back" class="btn btn-outline" style="width:100%;margin-top:10px;padding:14px;font-size:14px;border-radius:10px;min-height:50px;">
<i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رجوع
</button>
</div>
</div>
<!-- Step 3: Confirmation -->
<div id="step3" class="card" style="margin-bottom:16px;display:none;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="check-circle" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تأكيد الحجز</h3>
</div>
<div style="padding:20px;">
<div id="confirmDetails" style="margin-bottom:20px;"></div>
<div>
<label class="form-label">ملاحظات (اختياري)</label>
<textarea id="prNotes" class="form-input" rows="2" style="padding:12px;font-size:14px;border-radius:10px;" placeholder="أي ملاحظات إضافية..."></textarea>
</div>
<button type="button" id="btnConfirm" class="btn btn-primary" style="width:100%;margin-top:16px;padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;">
<i data-lucide="banknote" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i> تأكيد وإرسال للخزينة
</button>
<button type="button" id="btnStep3Back" class="btn btn-outline" style="width:100%;margin-top:10px;padding:14px;font-size:14px;border-radius:10px;min-height:50px;">
<i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رجوع
</button>
<div id="step3Error" style="display:none;margin-top:12px;padding:12px;background:#FEF2F2;border-radius:8px;color:#DC2626;font-size:13px;"></div>
</div>
</div>
<!-- Success State -->
<div id="stepSuccess" class="card" style="margin-bottom:16px;display:none;">
<div style="padding:40px 20px;text-align:center;">
<div style="width:64px;height:64px;border-radius:50%;background:#ECFDF5;display:inline-flex;align-items:center;justify-content:center;margin-bottom:16px;">
<i data-lucide="check" style="width:32px;height:32px;color:#059669;"></i>
</div>
<h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#059669;">تم إنشاء الحجز بنجاح</h3>
<div id="successNumber" style="font-size:14px;color:#6B7280;margin-bottom:8px;"></div>
<div id="successAmount" style="font-size:20px;font-weight:800;color:#2563EB;margin-bottom:16px;"></div>
<div style="padding:12px;background:#FEF3C7;border-radius:8px;margin:16px auto;max-width:400px;">
<i data-lucide="clock" style="width:16px;height:16px;vertical-align:middle;color:#D97706;margin-left:4px;"></i>
<span style="color:#92400E;font-size:13px;">بانتظار التحصيل من الخزينة</span>
</div>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
<button type="button" id="btnNewReservation" class="btn btn-primary" style="padding:14px 24px;font-size:14px;border-radius:10px;">
<i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حجز جديد
</button>
<a id="btnViewReservation" href="#" class="btn btn-outline" style="padding:14px 24px;font-size:14px;border-radius:10px;">
<i data-lucide="eye" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض الحجز
</a>
</div>
</div>
</div>
</div>
<!-- Recent Reservations -->
<?php if (!empty($recentReservations)): ?>
<div class="card" style="margin-top:16px;">
<div style="padding:12px 20px;border-bottom:1px solid #E5E7EB;">
<h4 style="margin:0;font-size:14px;font-weight:600;color:#374151;">آخر الحجوزات</h4>
</div>
<div style="padding:0;">
<?php foreach ($recentReservations as $r): ?>
<a href="/sa/swimming/pool-reservations/<?= (int) $r['id'] ?>" style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid #F3F4F6;text-decoration:none;color:inherit;min-height:56px;">
<div>
<div style="font-weight:600;font-size:14px;color:#1A1A2E;"><?= e($r['booker_name']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">
<?= e($r['reservation_number']) ?> · <?php
$typeLabels = ['lane_50m' => 'حارة 50م', 'lane_25m' => 'حارة 25م', 'lane_mix' => 'حارة ميكس', 'cards' => 'كروت'];
echo $typeLabels[$r['reservation_type']] ?? $r['reservation_type'];
?> · <?= (int) $r['sessions_used'] ?>/<?= (int) $r['sessions_total'] ?> حصة
</div>
</div>
<div style="text-align:left;flex-shrink:0;margin-right:12px;">
<div style="font-size:13px;font-weight:700;color:#2563EB;"><?= number_format((float) $r['total_amount'], 0) ?> ج.م</div>
<?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="font-size:10px;padding:2px 6px;border-radius:4px;background:<?= $c ?>20;color:<?= $c ?>;"><?= $l ?></span>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<script>
(function() {
var csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?= e($_SESSION['_csrf_token'] ?? '') ?>';
var state = {
bookerName: '',
bookerPhone: '',
bookerType: 'freelance_coach',
participantCount: 1,
reservationType: '',
pricingId: 0,
sessionsTotal: 0,
unitPrice: 0,
overflowType: null,
overflowCardsCount: 0,
overflowAmount: 0,
totalAmount: 0
};
var pricingData = null;
var laneCapacities = { '50m': 12, '25m': 8, 'mix': 24 };
var typeLabels = { lane_50m: 'إيجار حارة 50م', lane_25m: 'إيجار حارة 25م', lane_mix: 'إيجار حارة ميكس', cards: 'كروت حصص' };
function setStep(n) {
for (var i = 1; i <= 3; i++) {
var el = document.getElementById('step' + i);
if (el) el.style.display = (i === n) ? '' : 'none';
}
document.getElementById('stepSuccess').style.display = 'none';
for (var j = 1; j <= 3; j++) {
var c = document.getElementById('stepCircle' + j);
if (j < n) { c.style.background = '#059669'; c.style.color = '#fff'; }
else if (j === n) { c.style.background = '#2563EB'; c.style.color = '#fff'; }
else { c.style.background = '#E5E7EB'; c.style.color = '#6B7280'; }
}
}
// Step 1 logic
var nameInput = document.getElementById('prBookerName');
var phoneInput = document.getElementById('prBookerPhone');
var typeSelect = document.getElementById('prBookerType');
var participantsInput = document.getElementById('prParticipants');
var btnStep1 = document.getElementById('btnStep1Next');
function checkStep1() {
btnStep1.disabled = nameInput.value.trim() === '';
}
nameInput.addEventListener('input', checkStep1);
btnStep1.addEventListener('click', function() {
state.bookerName = nameInput.value.trim();
state.bookerPhone = phoneInput.value.trim();
state.bookerType = typeSelect.value;
state.participantCount = Math.max(1, parseInt(participantsInput.value) || 1);
setStep(2);
loadPricing();
});
// Step 2 logic
function loadPricing() {
if (pricingData) { renderPricing(); return; }
fetch('/api/sa/swimming/pool-reservations/pricing', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
pricingData = data.data;
renderPricing();
}
});
}
function renderPricing() {
renderLaneRentals();
renderSessionCards();
}
function renderLaneRentals() {
var container = document.getElementById('laneRentalGrid');
var lanes = pricingData.lane_rentals;
if (!lanes || !lanes.length) {
container.innerHTML = '<div style="color:#9CA3AF;text-align:center;padding:20px;">لا توجد أسعار حارات</div>';
return;
}
var grouped = {};
lanes.forEach(function(l) {
var key = l.lane_type;
if (!grouped[key]) grouped[key] = [];
grouped[key].push(l);
});
var laneNames = { '50m': 'حارة 50 متر', '25m': 'حارة 25 متر', 'mix': 'حارة ميكس (25م/50م)' };
var html = '';
Object.keys(grouped).forEach(function(laneType) {
var items = grouped[laneType];
html += '<div style="border:2px solid #E5E7EB;border-radius:12px;padding:14px;transition:border-color .2s;" class="lane-group" data-lane-type="' + laneType + '">';
html += '<div style="font-weight:700;font-size:14px;margin-bottom:10px;color:#1A1A2E;"><i data-lucide="waves" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;color:#2563EB;"></i> ' + (laneNames[laneType] || laneType) + '</div>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px;">';
items.forEach(function(item) {
var resType = 'lane_' + laneType;
html += '<label style="display:block;cursor:pointer;text-align:center;padding:10px 8px;border:2px solid #E5E7EB;border-radius:8px;transition:all .2s;" class="pricing-option" data-type="' + resType + '" data-pricing-id="' + item.id + '" data-sessions="' + item.sessions_per_month + '" data-price="' + item.price + '">';
html += '<input type="radio" name="packageChoice" style="display:none;">';
html += '<div style="font-size:20px;font-weight:800;color:#2563EB;">' + item.sessions_per_month + '</div>';
html += '<div style="font-size:10px;color:#6B7280;">حصة/شهر</div>';
html += '<div style="font-size:13px;font-weight:700;color:#059669;margin-top:4px;">' + Number(item.price).toLocaleString() + ' ج.م</div>';
html += '</label>';
});
html += '</div></div>';
});
container.innerHTML = html;
if (typeof lucide !== 'undefined') lucide.createIcons();
bindPricingClicks();
}
function renderSessionCards() {
var container = document.getElementById('sessionCardsGrid');
var cards = pricingData.session_cards;
if (!cards || !cards.length) {
container.innerHTML = '<div style="color:#9CA3AF;text-align:center;padding:20px;">لا توجد أسعار كروت</div>';
return;
}
var html = '';
cards.forEach(function(card) {
html += '<label style="display:block;cursor:pointer;text-align:center;padding:12px 8px;border:2px solid #E5E7EB;border-radius:10px;transition:all .2s;" class="pricing-option" data-type="cards" data-pricing-id="' + card.id + '" data-sessions="' + card.total_sessions + '" data-price="' + card.price + '">';
html += '<input type="radio" name="packageChoice" style="display:none;">';
html += '<div style="font-size:22px;font-weight:800;color:#059669;">' + card.total_sessions + '</div>';
html += '<div style="font-size:10px;color:#6B7280;">حصة</div>';
html += '<div style="font-size:13px;font-weight:700;color:#1A1A2E;margin-top:4px;">' + Number(card.price).toLocaleString() + ' ج.م</div>';
html += '<div style="font-size:10px;color:#9CA3AF;">' + Math.round(card.price_per_session) + ' ج.م/حصة</div>';
html += '</label>';
});
container.innerHTML = html;
bindPricingClicks();
}
function bindPricingClicks() {
document.querySelectorAll('.pricing-option').forEach(function(el) {
el.addEventListener('click', function() {
document.querySelectorAll('.pricing-option').forEach(function(o) {
o.style.borderColor = '#E5E7EB';
o.style.background = '';
});
this.style.borderColor = '#2563EB';
this.style.background = '#EFF6FF';
state.reservationType = this.dataset.type;
state.pricingId = parseInt(this.dataset.pricingId);
state.sessionsTotal = parseInt(this.dataset.sessions);
state.unitPrice = parseFloat(this.dataset.price);
state.overflowType = null;
state.overflowCardsCount = 0;
state.overflowAmount = 0;
checkOverflow();
updateSummary();
document.getElementById('btnStep2Next').disabled = false;
});
});
}
function checkOverflow() {
var alertEl = document.getElementById('overflowAlert');
if (state.reservationType === 'cards') {
alertEl.style.display = 'none';
return;
}
var laneType = state.reservationType.replace('lane_', '');
var capacity = laneCapacities[laneType] || 12;
var overflow = state.participantCount - capacity;
if (overflow <= 0) {
alertEl.style.display = 'none';
return;
}
document.getElementById('overflowText').textContent =
'عدد المشاركين (' + state.participantCount + ') يتجاوز سعة الحارة (' + capacity + ') بـ ' + overflow + ' شخص';
document.getElementById('overflowCardsText').textContent = 'إضافة ' + overflow + ' كروت للزائدين';
alertEl.style.display = '';
document.querySelectorAll('input[name="overflowOption"]').forEach(function(radio) {
radio.checked = false;
radio.addEventListener('change', function() {
state.overflowType = this.value;
if (this.value === 'overflow_cards') {
state.overflowCardsCount = overflow;
showOverflowCardsPicker(overflow);
} else {
state.overflowCardsCount = 0;
state.overflowAmount = state.unitPrice;
document.getElementById('overflowCardsPicker').style.display = 'none';
}
updateSummary();
});
});
}
function showOverflowCardsPicker(count) {
var picker = document.getElementById('overflowCardsPicker');
var container = document.getElementById('overflowCardsOptions');
picker.style.display = '';
var cards = pricingData.session_cards || [];
var html = '';
cards.forEach(function(card) {
var totalForOverflow = card.price * count;
html += '<label style="display:block;cursor:pointer;text-align:center;padding:8px;border:2px solid #E5E7EB;border-radius:8px;font-size:12px;" class="overflow-card-option" data-price="' + totalForOverflow + '" data-sessions="' + card.total_sessions + '">';
html += '<input type="radio" name="overflowCardChoice" style="display:none;">';
html += '<div style="font-weight:700;">' + card.total_sessions + ' حصة</div>';
html += '<div style="color:#059669;font-weight:600;">' + Number(totalForOverflow).toLocaleString() + ' ج.م</div>';
html += '<div style="color:#9CA3AF;">(' + count + ' × ' + Number(card.price).toLocaleString() + ')</div>';
html += '</label>';
});
container.innerHTML = html;
container.querySelectorAll('.overflow-card-option').forEach(function(el) {
el.addEventListener('click', function() {
container.querySelectorAll('.overflow-card-option').forEach(function(o) {
o.style.borderColor = '#E5E7EB'; o.style.background = '';
});
this.style.borderColor = '#059669'; this.style.background = '#ECFDF5';
state.overflowAmount = parseFloat(this.dataset.price);
updateSummary();
});
});
}
function updateSummary() {
var summaryEl = document.getElementById('selectionSummary');
if (!state.reservationType) { summaryEl.style.display = 'none'; return; }
state.totalAmount = state.unitPrice + (state.overflowAmount || 0);
var label = (typeLabels[state.reservationType] || state.reservationType) + ' — ' + state.sessionsTotal + ' حصة';
if (state.overflowType === 'additional_lane') {
label += ' + حارة إضافية';
} else if (state.overflowType === 'overflow_cards' && state.overflowAmount > 0) {
label += ' + ' + state.overflowCardsCount + ' كروت';
}
document.getElementById('summaryText').textContent = label;
document.getElementById('summaryPrice').textContent = Number(state.totalAmount).toLocaleString() + ' ج.م';
summaryEl.style.display = '';
}
document.getElementById('btnStep2Next').addEventListener('click', function() {
setStep(3);
renderConfirmation();
});
document.getElementById('btnStep2Back').addEventListener('click', function() { setStep(1); });
// Step 3: Confirmation
function renderConfirmation() {
var html = '<table style="width:100%;font-size:14px;border-collapse:collapse;">';
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">الاسم</td><td style="padding:10px 0;font-weight:600;">' + escHtml(state.bookerName) + '</td></tr>';
if (state.bookerPhone) {
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">التليفون</td><td style="padding:10px 0;">' + escHtml(state.bookerPhone) + '</td></tr>';
}
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">عدد المشاركين</td><td style="padding:10px 0;font-weight:600;">' + state.participantCount + '</td></tr>';
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">نوع الحجز</td><td style="padding:10px 0;font-weight:600;">' + (typeLabels[state.reservationType] || '') + '</td></tr>';
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">عدد الحصص</td><td style="padding:10px 0;font-weight:600;">' + state.sessionsTotal + ' حصة</td></tr>';
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">سعر الباقة</td><td style="padding:10px 0;font-weight:600;">' + Number(state.unitPrice).toLocaleString() + ' ج.م</td></tr>';
if (state.overflowAmount > 0) {
var ovLabel = state.overflowType === 'additional_lane' ? 'حارة إضافية' : 'كروت إضافية (' + state.overflowCardsCount + ')';
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">' + ovLabel + '</td><td style="padding:10px 0;font-weight:600;">' + Number(state.overflowAmount).toLocaleString() + ' ج.م</td></tr>';
}
html += '<tr><td style="padding:12px 0;font-weight:700;font-size:16px;color:#1A1A2E;">الإجمالي</td><td style="padding:12px 0;font-weight:800;font-size:20px;color:#059669;">' + Number(state.totalAmount).toLocaleString() + ' ج.م</td></tr>';
html += '</table>';
document.getElementById('confirmDetails').innerHTML = html;
}
document.getElementById('btnStep3Back').addEventListener('click', function() { setStep(2); });
document.getElementById('btnConfirm').addEventListener('click', function() {
var btn = this;
btn.disabled = true;
btn.textContent = 'جاري الإنشاء...';
document.getElementById('step3Error').style.display = 'none';
fetch('/api/sa/swimming/pool-reservations/store', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
_csrf_token: csrfToken,
booker_name: state.bookerName,
booker_phone: state.bookerPhone,
booker_type: state.bookerType,
participant_count: state.participantCount,
reservation_type: state.reservationType,
pricing_id: state.pricingId,
sessions_total: state.sessionsTotal,
unit_price: state.unitPrice,
notes: document.getElementById('prNotes').value.trim(),
overflow_type: state.overflowType,
overflow_cards_count: state.overflowCardsCount,
overflow_amount: state.overflowAmount
})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
showSuccess(data);
} else {
var errEl = document.getElementById('step3Error');
errEl.textContent = data.error || 'حدث خطأ';
errEl.style.display = '';
btn.disabled = false;
btn.innerHTML = '<i data-lucide="banknote" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i> تأكيد وإرسال للخزينة';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}).catch(function() {
var errEl = document.getElementById('step3Error');
errEl.textContent = 'خطأ في الاتصال';
errEl.style.display = '';
btn.disabled = false;
btn.innerHTML = '<i data-lucide="banknote" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i> تأكيد وإرسال للخزينة';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
});
function showSuccess(data) {
for (var i = 1; i <= 3; i++) { document.getElementById('step' + i).style.display = 'none'; }
document.getElementById('stepSuccess').style.display = '';
for (var j = 1; j <= 3; j++) {
var c = document.getElementById('stepCircle' + j);
c.style.background = '#059669'; c.style.color = '#fff';
}
document.getElementById('successNumber').textContent = 'رقم الحجز: ' + data.reservation_number;
document.getElementById('successAmount').textContent = Number(data.total_amount).toLocaleString() + ' ج.م';
document.getElementById('btnViewReservation').href = '/sa/swimming/pool-reservations/' + data.reservation_id;
}
document.getElementById('btnNewReservation').addEventListener('click', function() {
state = { bookerName:'', bookerPhone:'', bookerType:'freelance_coach', participantCount:1, reservationType:'', pricingId:0, sessionsTotal:0, unitPrice:0, overflowType:null, overflowCardsCount:0, overflowAmount:0, totalAmount:0 };
nameInput.value = ''; phoneInput.value = ''; participantsInput.value = '1';
typeSelect.value = 'freelance_coach';
document.getElementById('prNotes').value = '';
document.getElementById('btnStep1Next').disabled = true;
document.getElementById('btnStep2Next').disabled = true;
document.getElementById('selectionSummary').style.display = 'none';
document.getElementById('overflowAlert').style.display = 'none';
document.querySelectorAll('.pricing-option').forEach(function(o) { o.style.borderColor='#E5E7EB'; o.style.background=''; });
var btn = document.getElementById('btnConfirm');
btn.disabled = false;
btn.innerHTML = '<i data-lucide="banknote" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i> تأكيد وإرسال للخزينة';
setStep(1);
if (typeof lucide !== 'undefined') lucide.createIcons();
});
function escHtml(str) {
var d = document.createElement('div'); d.textContent = str; return d.innerHTML;
}
})();
</script>
<?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