Commit a5018bba authored by Mahmoud Aglan's avatar Mahmoud Aglan

Gap sportsactivity

parent d63df4a2
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\BookingPassService;
class BookingPassController extends Controller
{
public function passes(Request $request, string $bookingId): Response
{
$db = App::getInstance()->db();
$booking = $db->selectOne(
"SELECT b.*, fu.name_ar as unit_name, f.name_ar as facility_name
FROM sa_bookings b
INNER JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
INNER JOIN sa_facilities f ON f.id = fu.facility_id
WHERE b.id = ?",
[(int) $bookingId]
);
if (!$booking) {
return $this->redirect('/sa/bookings')->withError('الحجز غير موجود');
}
$passes = BookingPassService::getBookingPasses((int) $bookingId);
return $this->view('SportsActivity.Views.bookings.passes', [
'booking' => $booking,
'passes' => $passes,
]);
}
public function usePass(Request $request): Response
{
$passNumber = trim((string) $request->post('pass_number', ''));
$usedByName = trim((string) $request->post('used_by_name', ''));
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
if ($passNumber === '') {
return $this->json(['success' => false, 'error' => 'أدخل رقم الباس']);
}
$result = BookingPassService::usePass($passNumber, $usedByName ?: null, $employeeId);
return $this->json($result);
}
public function validatePass(Request $request): Response
{
$passNumber = trim((string) $request->get('pass_number', ''));
if ($passNumber === '') {
return $this->json(['valid' => false, 'error' => 'أدخل رقم الباس']);
}
$result = BookingPassService::validatePass($passNumber);
return $this->json($result);
}
}
...@@ -101,7 +101,7 @@ class BookingWizardController extends Controller ...@@ -101,7 +101,7 @@ class BookingWizardController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$units = $db->select( $units = $db->select(
"SELECT fu.id, fu.code, fu.name_ar, fu.unit_type, fu.booking_mode, fu.max_capacity, "SELECT fu.id, fu.code, fu.name_ar, fu.unit_type, fu.booking_mode, fu.max_capacity, fu.expected_capacity,
f.id as facility_id, f.name_ar as facility_name, f.facility_type, f.operating_hours_json f.id as facility_id, f.name_ar as facility_name, f.facility_type, f.operating_hours_json
FROM sa_facility_units fu FROM sa_facility_units fu
JOIN sa_facilities f ON f.id = fu.facility_id JOIN sa_facilities f ON f.id = fu.facility_id
...@@ -122,12 +122,13 @@ class BookingWizardController extends Controller ...@@ -122,12 +122,13 @@ class BookingWizardController extends Controller
]; ];
} }
$grouped[$fId]['units'][] = [ $grouped[$fId]['units'][] = [
'id' => (int) $u['id'], 'id' => (int) $u['id'],
'code' => $u['code'], 'code' => $u['code'],
'name' => $u['name_ar'], 'name' => $u['name_ar'],
'unit_type' => $u['unit_type'], 'unit_type' => $u['unit_type'],
'booking_mode' => $u['booking_mode'], 'booking_mode' => $u['booking_mode'],
'max_capacity' => (int) $u['max_capacity'], 'max_capacity' => (int) $u['max_capacity'],
'expected_capacity' => (int) ($u['expected_capacity'] ?? 1),
]; ];
} }
...@@ -229,11 +230,12 @@ class BookingWizardController extends Controller ...@@ -229,11 +230,12 @@ class BookingWizardController extends Controller
} }
return $this->json([ return $this->json([
'success' => true, 'success' => true,
'unit_name' => $unit['name_ar'], 'unit_name' => $unit['name_ar'],
'booking_mode' => $unit['booking_mode'], 'booking_mode' => $unit['booking_mode'],
'max_capacity' => (int) $unit['max_capacity'], 'max_capacity' => (int) $unit['max_capacity'],
'slots' => $slots, 'expected_capacity' => (int) ($unit['expected_capacity'] ?? 1),
'slots' => $slots,
]); ]);
} }
...@@ -262,22 +264,32 @@ class BookingWizardController extends Controller ...@@ -262,22 +264,32 @@ class BookingWizardController extends Controller
$nationalId = trim((string) $request->post('national_id', '')); $nationalId = trim((string) $request->post('national_id', ''));
$isMember = (bool) $request->post('is_member', false); $isMember = (bool) $request->post('is_member', false);
$memberId = (int) $request->post('member_id', 0); $memberId = (int) $request->post('member_id', 0);
$organizationName = trim((string) $request->post('organization_name', ''));
$organizationContact = trim((string) $request->post('organization_contact', ''));
$participantMode = trim((string) $request->post('participant_mode', 'passes'));
$branch = App::getInstance()->currentBranch(); $branch = App::getInstance()->currentBranch();
$bookerType = $isMember ? 'member' : 'guest';
$bookerType = $request->post('booker_type', '');
if ($bookerType === '') {
$bookerType = $isMember ? 'member' : 'guest';
}
$bookingResult = BookingService::createHourlyBooking([ $bookingResult = BookingService::createHourlyBooking([
'facility_unit_id' => $unitId, 'facility_unit_id' => $unitId,
'booking_date' => $date, 'booking_date' => $date,
'start_time' => $startTime, 'start_time' => $startTime,
'end_time' => $endTime, 'end_time' => $endTime,
'participant_count' => $participants, 'participant_count' => $participants,
'spots_reserved' => $participants, 'spots_reserved' => $participants,
'booker_type' => $bookerType, 'booker_type' => $bookerType,
'booker_id' => $memberId > 0 ? $memberId : null, 'booker_id' => $memberId > 0 ? $memberId : null,
'booker_name' => $bookerName, 'booker_name' => $bookerName,
'notes' => null, 'organization_name' => $organizationName ?: null,
'branch_id' => $branch ? (int) $branch['id'] : null, 'organization_contact' => $organizationContact ?: null,
'participant_mode' => $participantMode,
'notes' => null,
'branch_id' => $branch ? (int) $branch['id'] : null,
]); ]);
if (!$bookingResult['success']) { if (!$bookingResult['success']) {
......
...@@ -63,6 +63,7 @@ class FacilityUnitController extends Controller ...@@ -63,6 +63,7 @@ class FacilityUnitController extends Controller
$unitType = trim((string) $request->post('unit_type', '')); $unitType = trim((string) $request->post('unit_type', ''));
$bookingMode = trim((string) $request->post('booking_mode', '')); $bookingMode = trim((string) $request->post('booking_mode', ''));
$maxCapacity = (int) $request->post('max_capacity', 1); $maxCapacity = (int) $request->post('max_capacity', 1);
$expectedCapacity = (int) $request->post('expected_capacity', 1);
$sortOrder = (int) $request->post('sort_order', 0); $sortOrder = (int) $request->post('sort_order', 0);
// Validation // Validation
...@@ -89,6 +90,9 @@ class FacilityUnitController extends Controller ...@@ -89,6 +90,9 @@ class FacilityUnitController extends Controller
if ($maxCapacity < 1) { if ($maxCapacity < 1) {
$errors[] = 'السعة القصوى يجب أن تكون 1 على الأقل'; $errors[] = 'السعة القصوى يجب أن تكون 1 على الأقل';
} }
if ($expectedCapacity < 1) {
$expectedCapacity = 1;
}
if (!empty($errors)) { if (!empty($errors)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -112,15 +116,16 @@ class FacilityUnitController extends Controller ...@@ -112,15 +116,16 @@ class FacilityUnitController extends Controller
} }
FacilityUnit::create([ FacilityUnit::create([
'facility_id' => (int) $fid, 'facility_id' => (int) $fid,
'code' => $code, 'code' => $code,
'name_ar' => $nameAr, 'name_ar' => $nameAr,
'name_en' => $nameEn ?: null, 'name_en' => $nameEn ?: null,
'unit_type' => $unitType, 'unit_type' => $unitType,
'booking_mode' => $bookingMode, 'booking_mode' => $bookingMode,
'max_capacity' => $maxCapacity, 'max_capacity' => $maxCapacity,
'sort_order' => $sortOrder, 'expected_capacity' => $expectedCapacity,
'is_active' => 1, 'sort_order' => $sortOrder,
'is_active' => 1,
]); ]);
return $this->redirect("/sa/facilities/{$fid}/units")->withSuccess('تم إضافة الوحدة بنجاح'); return $this->redirect("/sa/facilities/{$fid}/units")->withSuccess('تم إضافة الوحدة بنجاح');
...@@ -168,6 +173,7 @@ class FacilityUnitController extends Controller ...@@ -168,6 +173,7 @@ class FacilityUnitController extends Controller
$unitType = trim((string) $request->post('unit_type', '')); $unitType = trim((string) $request->post('unit_type', ''));
$bookingMode = trim((string) $request->post('booking_mode', '')); $bookingMode = trim((string) $request->post('booking_mode', ''));
$maxCapacity = (int) $request->post('max_capacity', 1); $maxCapacity = (int) $request->post('max_capacity', 1);
$expectedCapacity = (int) $request->post('expected_capacity', 1);
$sortOrder = (int) $request->post('sort_order', 0); $sortOrder = (int) $request->post('sort_order', 0);
// Validation // Validation
...@@ -194,6 +200,9 @@ class FacilityUnitController extends Controller ...@@ -194,6 +200,9 @@ class FacilityUnitController extends Controller
if ($maxCapacity < 1) { if ($maxCapacity < 1) {
$errors[] = 'السعة القصوى يجب أن تكون 1 على الأقل'; $errors[] = 'السعة القصوى يجب أن تكون 1 على الأقل';
} }
if ($expectedCapacity < 1) {
$expectedCapacity = 1;
}
if (!empty($errors)) { if (!empty($errors)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -217,13 +226,14 @@ class FacilityUnitController extends Controller ...@@ -217,13 +226,14 @@ class FacilityUnitController extends Controller
} }
$unit->update([ $unit->update([
'code' => $code, 'code' => $code,
'name_ar' => $nameAr, 'name_ar' => $nameAr,
'name_en' => $nameEn ?: null, 'name_en' => $nameEn ?: null,
'unit_type' => $unitType, 'unit_type' => $unitType,
'booking_mode' => $bookingMode, 'booking_mode' => $bookingMode,
'max_capacity' => $maxCapacity, 'max_capacity' => $maxCapacity,
'sort_order' => $sortOrder, 'expected_capacity' => $expectedCapacity,
'sort_order' => $sortOrder,
]); ]);
return $this->redirect("/sa/facilities/{$fid}/units")->withSuccess('تم تحديث الوحدة بنجاح'); return $this->redirect("/sa/facilities/{$fid}/units")->withSuccess('تم تحديث الوحدة بنجاح');
......
...@@ -455,17 +455,40 @@ class GroupController extends Controller ...@@ -455,17 +455,40 @@ class GroupController extends Controller
return $this->redirect('/sa/groups/' . $id)->withSuccess("تم حفظ الجدول ({$inserted} حصة)"); return $this->redirect('/sa/groups/' . $id)->withSuccess("تم حفظ الجدول ({$inserted} حصة)");
} }
public function forceEnroll(Request $request, string $id): Response
{
$this->authorize('sa.group.force_enroll');
$playerId = (int) $request->post('player_id', 0);
if ($playerId === 0) {
return $this->redirect('/sa/groups/' . $id)->withError('يرجى اختيار لاعب');
}
$result = EnrollmentService::forceEnroll((int) $id, $playerId);
if ($result['success']) {
$msg = 'تم تسجيل اللاعب (تجاوز السعة) — في انتظار الدفع';
if (!empty($result['request_number'])) {
$msg .= ' (طلب دفع: ' . $result['request_number'] . ')';
}
return $this->redirect('/sa/groups/' . $id)->withSuccess($msg);
}
return $this->redirect('/sa/groups/' . $id)->withError($result['error']);
}
public function transferPlayer(Request $request, string $id): Response public function transferPlayer(Request $request, string $id): Response
{ {
$playerId = (int) $request->post('player_id', 0); $playerId = (int) $request->post('player_id', 0);
$toGroupId = (int) $request->post('to_group_id', 0); $toGroupId = (int) $request->post('to_group_id', 0);
$reason = trim((string) $request->post('reason', '')); $reason = trim((string) $request->post('reason', ''));
$force = (bool) $request->post('force', false);
if ($playerId === 0 || $toGroupId === 0) { if ($playerId === 0 || $toGroupId === 0) {
return $this->redirect('/sa/groups/' . $id)->withError('يرجى تحديد اللاعب والمجموعة المنقول إليها'); return $this->redirect('/sa/groups/' . $id)->withError('يرجى تحديد اللاعب والمجموعة المنقول إليها');
} }
$result = GroupTransferService::transfer($playerId, (int) $id, $toGroupId, $reason); $result = GroupTransferService::transfer($playerId, (int) $id, $toGroupId, $reason, $force);
if ($result['success']) { if ($result['success']) {
return $this->redirect('/sa/groups/' . $id)->withSuccess('تم نقل اللاعب بنجاح'); return $this->redirect('/sa/groups/' . $id)->withSuccess('تم نقل اللاعب بنجاح');
......
...@@ -7,35 +7,64 @@ use App\Core\Controller; ...@@ -7,35 +7,64 @@ use App\Core\Controller;
use App\Core\Request; use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\App; use App\Core\App;
use App\Modules\SportsActivity\Models\Facility;
use App\Modules\SportsActivity\Services\MirrorStateService; use App\Modules\SportsActivity\Services\MirrorStateService;
class MirrorController extends Controller class MirrorController extends Controller
{ {
/**
* List all active facilities with links to mirror views.
*/
public function index(Request $request): Response public function index(Request $request): Response
{ {
$facilities = Facility::getActive(); $db = App::getInstance()->db();
$disciplines = $db->select(
"SELECT d.id, d.name_ar, d.name_en, COUNT(f.id) as facility_count
FROM sa_disciplines d
INNER JOIN sa_facilities f ON f.discipline_id = d.id AND f.is_active = 1 AND f.is_archived = 0
WHERE d.is_active = 1 AND d.is_archived = 0
GROUP BY d.id, d.name_ar, d.name_en
ORDER BY d.name_ar"
);
$facilities = $db->select(
"SELECT f.*, d.name_ar as discipline_name
FROM sa_facilities f
LEFT JOIN sa_disciplines d ON d.id = f.discipline_id
WHERE f.is_active = 1 AND f.is_archived = 0
ORDER BY d.name_ar, f.name_ar"
);
$grouped = [];
foreach ($facilities as $f) {
$dName = $f['discipline_name'] ?? 'أخرى';
$grouped[$dName][] = $f;
}
return $this->view('SportsActivity.Views.mirror.index', [ return $this->view('SportsActivity.Views.mirror.index', [
'facilities' => $facilities, 'facilities' => $facilities,
'disciplines' => $disciplines,
'grouped' => $grouped,
]); ]);
} }
/** public function byDiscipline(Request $request, string $id): Response
* Mirror view for a facility on today's date. {
*/ $today = date('Y-m-d');
return $this->renderDisciplineView((int) $id, $today);
}
public function byDisciplineDate(Request $request, string $id, string $date): Response
{
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return $this->redirect('/sa/mirror')->withError('تاريخ غير صالح');
}
return $this->renderDisciplineView((int) $id, $date);
}
public function facility(Request $request, string $id): Response public function facility(Request $request, string $id): Response
{ {
$today = date('Y-m-d'); $today = date('Y-m-d');
return $this->renderFacilityView((int) $id, $today); return $this->renderFacilityView((int) $id, $today);
} }
/**
* Mirror view for a facility on a specific date.
*/
public function facilityDate(Request $request, string $id, string $date): Response public function facilityDate(Request $request, string $id, string $date): Response
{ {
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
...@@ -44,9 +73,21 @@ class MirrorController extends Controller ...@@ -44,9 +73,21 @@ class MirrorController extends Controller
return $this->renderFacilityView((int) $id, $date); return $this->renderFacilityView((int) $id, $date);
} }
/** private function renderDisciplineView(int $disciplineId, string $date): Response
* Render the facility mirror grid. {
*/ $state = MirrorStateService::getMultiFacilityState($disciplineId, $date);
if (isset($state['error'])) {
return $this->redirect('/sa/mirror')->withError($state['error']);
}
return $this->view('SportsActivity.Views.mirror.discipline_view', [
'discipline' => $state['discipline'],
'facilityStates' => $state['facilities'],
'date' => $date,
]);
}
private function renderFacilityView(int $facilityId, string $date): Response private function renderFacilityView(int $facilityId, string $date): Response
{ {
$state = MirrorStateService::getFacilityState($facilityId, $date); $state = MirrorStateService::getFacilityState($facilityId, $date);
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\PoolTicketService;
use App\Modules\SportsActivity\Services\PoolGridService;
class PoolTicketController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$facilityId = (int) $request->get('facility_id', 0);
$date = trim((string) $request->get('date', date('Y-m-d')));
$facilities = $db->select(
"SELECT * FROM sa_facilities WHERE facility_type = 'pool' AND is_active = 1 AND is_archived = 0"
);
$tickets = [];
if ($facilityId > 0) {
$tickets = PoolTicketService::getActiveTickets($facilityId, $date);
}
return $this->view('SportsActivity.Views.pool-tickets.index', [
'facilities' => $facilities,
'tickets' => $tickets,
'facilityId' => $facilityId,
'date' => $date,
]);
}
public function issue(Request $request): Response
{
$data = [
'facility_id' => (int) $request->post('facility_id', 0),
'booking_date' => trim((string) $request->post('booking_date', date('Y-m-d'))),
'start_time' => trim((string) $request->post('start_time', '')),
'end_time' => trim((string) $request->post('end_time', '')),
'is_member' => (bool) $request->post('is_member', false),
'member_id' => $request->post('member_id') ? (int) $request->post('member_id') : null,
'player_id' => $request->post('player_id') ? (int) $request->post('player_id') : null,
'guest_name' => trim((string) $request->post('guest_name', '')),
'guest_national_id' => trim((string) $request->post('guest_national_id', '')),
];
if ($data['facility_id'] <= 0 || $data['start_time'] === '') {
return $this->json(['success' => false, 'error' => 'بيانات غير مكتملة']);
}
$result = PoolTicketService::issueTicket($data);
return $this->json($result);
}
public function cancel(Request $request, string $id): Response
{
$reason = trim((string) $request->post('reason', ''));
$result = PoolTicketService::cancelTicket((int) $id, $reason);
return $this->json($result);
}
public function entry(Request $request, string $id): Response
{
$result = PoolTicketService::recordEntry((int) $id);
return $this->json($result);
}
public function exit(Request $request, string $id): Response
{
$result = PoolTicketService::recordExit((int) $id);
return $this->json($result);
}
}
...@@ -177,18 +177,56 @@ class RegistrationWizardController extends Controller ...@@ -177,18 +177,56 @@ class RegistrationWizardController extends Controller
public function selectActivity(Request $request, string $id): Response public function selectActivity(Request $request, string $id): Response
{ {
$registrationId = (int) $id; $registrationId = (int) $id;
$programId = (int) $request->post('program_id', 0);
$groupId = (int) $request->post('group_id', 0); $groupId = (int) $request->post('group_id', 0);
$months = max(1, (int) $request->post('months', 1)); $months = max(1, (int) $request->post('months', 1));
$hasSibling = (bool) $request->post('has_sibling', false); $hasSibling = (bool) $request->post('has_sibling', false);
if ($groupId <= 0) { if ($programId > 0) {
return $this->json(['success' => false, 'error' => 'اختر مجموعة']); $result = RegistrationWizardService::selectProgram($registrationId, $programId, $months, $hasSibling);
} elseif ($groupId > 0) {
$result = RegistrationWizardService::selectGroup($registrationId, $groupId, $months, $hasSibling);
} else {
return $this->json(['success' => false, 'error' => 'اختر برنامج أو مجموعة']);
} }
$result = RegistrationWizardService::selectGroup($registrationId, $groupId, $months, $hasSibling);
return $this->json($result); return $this->json($result);
} }
public function getPrograms(Request $request, string $disciplineId): Response
{
$db = App::getInstance()->db();
$programs = $db->select(
"SELECT p.id, p.name_ar, p.name_en, p.description_ar,
COUNT(g.id) as group_count,
MIN(g.monthly_fee_member) as min_fee_member,
MIN(g.monthly_fee_nonmember) as min_fee_nonmember
FROM sa_programs p
LEFT JOIN sa_groups g ON g.program_id = p.id AND g.status = 'active' AND g.is_archived = 0
WHERE p.discipline_id = ? AND p.is_archived = 0
GROUP BY p.id, p.name_ar, p.name_en, p.description_ar
ORDER BY p.name_ar",
[(int) $disciplineId]
);
return $this->json(['success' => true, 'programs' => $programs]);
}
public function getDisciplines(Request $request): Response
{
$db = App::getInstance()->db();
$disciplines = $db->select(
"SELECT d.id, d.name_ar, d.name_en, d.icon
FROM sa_disciplines d
WHERE d.is_active = 1 AND d.is_archived = 0
ORDER BY d.name_ar"
);
return $this->json(['success' => true, 'disciplines' => $disciplines]);
}
public function submitPayment(Request $request, string $id): Response public function submitPayment(Request $request, string $id): Response
{ {
$registrationId = (int) $id; $registrationId = (int) $id;
......
...@@ -93,6 +93,7 @@ return [ ...@@ -93,6 +93,7 @@ return [
['GET', '/sa/groups/{id:\d+}/edit', 'SportsActivity\Controllers\GroupController@edit', ['auth'], 'sa.group.manage'], ['GET', '/sa/groups/{id:\d+}/edit', 'SportsActivity\Controllers\GroupController@edit', ['auth'], 'sa.group.manage'],
['POST', '/sa/groups/{id:\d+}', 'SportsActivity\Controllers\GroupController@update', ['auth', 'csrf'], 'sa.group.manage'], ['POST', '/sa/groups/{id:\d+}', 'SportsActivity\Controllers\GroupController@update', ['auth', 'csrf'], 'sa.group.manage'],
['POST', '/sa/groups/{id:\d+}/enroll', 'SportsActivity\Controllers\GroupController@enroll', ['auth', 'csrf'], 'sa.group.enroll'], ['POST', '/sa/groups/{id:\d+}/enroll', 'SportsActivity\Controllers\GroupController@enroll', ['auth', 'csrf'], 'sa.group.enroll'],
['POST', '/sa/groups/{id:\d+}/force-enroll', 'SportsActivity\Controllers\GroupController@forceEnroll', ['auth', 'csrf'], 'sa.group.force_enroll'],
['POST', '/sa/groups/{id:\d+}/remove-player', 'SportsActivity\Controllers\GroupController@removePlayer', ['auth', 'csrf'], 'sa.group.manage'], ['POST', '/sa/groups/{id:\d+}/remove-player', 'SportsActivity\Controllers\GroupController@removePlayer', ['auth', 'csrf'], 'sa.group.manage'],
['POST', '/sa/groups/{id:\d+}/transfer-player', 'SportsActivity\Controllers\GroupController@transferPlayer', ['auth', 'csrf'], 'sa.group.manage'], ['POST', '/sa/groups/{id:\d+}/transfer-player', 'SportsActivity\Controllers\GroupController@transferPlayer', ['auth', 'csrf'], 'sa.group.manage'],
['POST', '/sa/groups/{id:\d+}/schedule', 'SportsActivity\Controllers\GroupController@saveSchedule', ['auth', 'csrf'], 'sa.group.manage'], ['POST', '/sa/groups/{id:\d+}/schedule', 'SportsActivity\Controllers\GroupController@saveSchedule', ['auth', 'csrf'], 'sa.group.manage'],
...@@ -143,6 +144,8 @@ return [ ...@@ -143,6 +144,8 @@ return [
// Mirror (read-only display) // Mirror (read-only display)
['GET', '/sa/mirror', 'SportsActivity\Controllers\MirrorController@index', ['auth'], 'sa.mirror.view'], ['GET', '/sa/mirror', 'SportsActivity\Controllers\MirrorController@index', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/discipline/{id:\d+}', 'SportsActivity\Controllers\MirrorController@byDiscipline', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/discipline/{id:\d+}/{date}', 'SportsActivity\Controllers\MirrorController@byDisciplineDate', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/{id:\d+}', 'SportsActivity\Controllers\MirrorController@facility', ['auth'], 'sa.mirror.view'], ['GET', '/sa/mirror/{id:\d+}', 'SportsActivity\Controllers\MirrorController@facility', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/{id:\d+}/{date}', 'SportsActivity\Controllers\MirrorController@facilityDate', ['auth'], 'sa.mirror.view'], ['GET', '/sa/mirror/{id:\d+}/{date}', 'SportsActivity\Controllers\MirrorController@facilityDate', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/facility/{id:\d+}', 'SportsActivity\Controllers\MirrorController@facility', ['auth'], 'sa.mirror.view'], ['GET', '/sa/mirror/facility/{id:\d+}', 'SportsActivity\Controllers\MirrorController@facility', ['auth'], 'sa.mirror.view'],
...@@ -214,6 +217,10 @@ return [ ...@@ -214,6 +217,10 @@ return [
['GET', '/sa/registration/{id:\d+}/print-card', 'SportsActivity\Controllers\RegistrationWizardController@printCard', ['auth'], 'sa.registration.manage'], ['GET', '/sa/registration/{id:\d+}/print-card', 'SportsActivity\Controllers\RegistrationWizardController@printCard', ['auth'], 'sa.registration.manage'],
['POST', '/sa/registration/{id:\d+}/cancel', 'SportsActivity\Controllers\RegistrationWizardController@cancel', ['auth', 'csrf'], 'sa.registration.manage'], ['POST', '/sa/registration/{id:\d+}/cancel', 'SportsActivity\Controllers\RegistrationWizardController@cancel', ['auth', 'csrf'], 'sa.registration.manage'],
// Registration Wizard APIs
['GET', '/api/sa/registration/disciplines', 'SportsActivity\Controllers\RegistrationWizardController@getDisciplines', ['auth'], 'sa.registration.manage'],
['GET', '/api/sa/registration/programs/{id:\d+}', 'SportsActivity\Controllers\RegistrationWizardController@getPrograms', ['auth'], 'sa.registration.manage'],
// ─── SA Player Cards ──────────────────────────────────────────────────────── // ─── SA Player Cards ────────────────────────────────────────────────────────
['GET', '/sa/cards/report', 'SportsActivity\Controllers\SaCardController@report', ['auth'], 'sa.card.view'], ['GET', '/sa/cards/report', 'SportsActivity\Controllers\SaCardController@report', ['auth'], 'sa.card.view'],
['GET', '/sa/cards', 'SportsActivity\Controllers\SaCardController@index', ['auth'], 'sa.card.view'], ['GET', '/sa/cards', 'SportsActivity\Controllers\SaCardController@index', ['auth'], 'sa.card.view'],
...@@ -230,6 +237,18 @@ return [ ...@@ -230,6 +237,18 @@ return [
['GET', '/sa/gate/log', 'SportsActivity\Controllers\GateController@log', ['auth'], 'sa.gate.view'], ['GET', '/sa/gate/log', 'SportsActivity\Controllers\GateController@log', ['auth'], 'sa.gate.view'],
['GET', '/sa/gate/report', 'SportsActivity\Controllers\GateController@report', ['auth'], 'sa.gate.view'], ['GET', '/sa/gate/report', 'SportsActivity\Controllers\GateController@report', ['auth'], 'sa.gate.view'],
// Booking Passes
['GET', '/sa/bookings/{id:\d+}/passes', 'SportsActivity\Controllers\BookingPassController@passes', ['auth'], 'sa.booking.view'],
['POST', '/sa/booking-passes/use', 'SportsActivity\Controllers\BookingPassController@usePass', ['auth', 'csrf'], 'sa.booking.manage'],
['GET', '/api/sa/booking-passes/validate', 'SportsActivity\Controllers\BookingPassController@validatePass',['auth'], 'sa.booking.view'],
// Pool Tickets
['GET', '/sa/pool-tickets', 'SportsActivity\Controllers\PoolTicketController@index', ['auth'], 'sa.pool_ticket.view'],
['POST', '/sa/pool-tickets/issue', 'SportsActivity\Controllers\PoolTicketController@issue', ['auth', 'csrf'], 'sa.pool_ticket.issue'],
['POST', '/sa/pool-tickets/{id:\d+}/cancel', 'SportsActivity\Controllers\PoolTicketController@cancel', ['auth', 'csrf'], 'sa.pool_ticket.manage'],
['POST', '/sa/pool-tickets/{id:\d+}/entry', 'SportsActivity\Controllers\PoolTicketController@entry', ['auth', 'csrf'], 'sa.pool_ticket.manage'],
['POST', '/sa/pool-tickets/{id:\d+}/exit', 'SportsActivity\Controllers\PoolTicketController@exit', ['auth', 'csrf'], 'sa.pool_ticket.manage'],
// ─── Activity Browser ─────────────────────────────────────────────────────── // ─── Activity Browser ───────────────────────────────────────────────────────
['GET', '/sa/activities', 'SportsActivity\Controllers\DisciplineController@activityBrowser', ['auth'], 'sa.discipline.view'], ['GET', '/sa/activities', 'SportsActivity\Controllers\DisciplineController@activityBrowser', ['auth'], 'sa.discipline.view'],
......
...@@ -35,6 +35,28 @@ final class SaConstants ...@@ -35,6 +35,28 @@ final class SaConstants
// Booker types // Booker types
const BOOKER_MEMBER = 'member'; const BOOKER_MEMBER = 'member';
const BOOKER_GUEST = 'guest'; const BOOKER_GUEST = 'guest';
const BOOKER_ORGANIZATION = 'organization';
// Participant modes
const PARTICIPANT_MODE_FULL = 'full';
const PARTICIPANT_MODE_PARTIAL = 'partial';
const PARTICIPANT_MODE_PASSES = 'passes';
// Booking pass statuses
const PASS_ISSUED = 'issued';
const PASS_USED = 'used';
const PASS_EXPIRED = 'expired';
const PASS_CANCELLED = 'cancelled';
// Pool ticket statuses
const TICKET_ISSUED = 'issued';
const TICKET_ACTIVE = 'active';
const TICKET_COMPLETED = 'completed';
const TICKET_CANCELLED = 'cancelled';
// Pool access modes
const ACCESS_RESERVED = 'reserved';
const ACCESS_OPEN = 'open_access';
// Medical statuses // Medical statuses
const MEDICAL_FIT = 'fit'; const MEDICAL_FIT = 'fit';
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
use App\Modules\SportsActivity\SaConstants;
final class BookingPassService
{
public static function generatePasses(int $bookingId, int $count): array
{
$db = App::getInstance()->db();
$now = date('Y-m-d H:i:s');
$passes = [];
$booking = $db->selectOne("SELECT booking_number FROM sa_bookings WHERE id = ?", [$bookingId]);
$prefix = $booking ? $booking['booking_number'] : ('BK-' . str_pad((string) $bookingId, 5, '0', STR_PAD_LEFT));
for ($i = 1; $i <= $count; $i++) {
$passNumber = $prefix . '-P' . str_pad((string) $i, 2, '0', STR_PAD_LEFT);
$passId = $db->insert('sa_booking_passes', [
'booking_id' => $bookingId,
'pass_number' => $passNumber,
'status' => SaConstants::PASS_ISSUED,
'created_at' => $now,
'updated_at' => $now,
]);
$passes[] = ['id' => $passId, 'pass_number' => $passNumber];
}
$db->update('sa_bookings', [
'passes_total' => $count,
'updated_at' => $now,
], 'id = ?', [$bookingId]);
return ['success' => true, 'passes' => $passes, 'count' => $count];
}
public static function usePass(string $passNumber, ?string $usedByName, int $employeeId): array
{
$db = App::getInstance()->db();
$pass = $db->selectOne(
"SELECT bp.*, b.booking_date, b.start_time, b.end_time, b.status as booking_status
FROM sa_booking_passes bp
INNER JOIN sa_bookings b ON b.id = bp.booking_id
WHERE bp.pass_number = ?",
[$passNumber]
);
if (!$pass) {
return ['success' => false, 'error' => 'رقم الباس غير موجود'];
}
if ($pass['status'] !== SaConstants::PASS_ISSUED) {
return ['success' => false, 'error' => 'هذا الباس ' . ($pass['status'] === 'used' ? 'مستخدم بالفعل' : 'غير صالح')];
}
if ($pass['booking_status'] === 'cancelled') {
return ['success' => false, 'error' => 'الحجز ملغي'];
}
$now = date('Y-m-d H:i:s');
$db->update('sa_booking_passes', [
'status' => SaConstants::PASS_USED,
'used_at' => $now,
'used_by_name' => $usedByName,
'gate_employee_id' => $employeeId,
'updated_at' => $now,
], 'id = ?', [(int) $pass['id']]);
$db->query(
"UPDATE sa_bookings SET passes_used = passes_used + 1, updated_at = ? WHERE id = ?",
[$now, (int) $pass['booking_id']]
);
return ['success' => true, 'pass_number' => $passNumber, 'booking_id' => (int) $pass['booking_id']];
}
public static function validatePass(string $passNumber): array
{
$db = App::getInstance()->db();
$pass = $db->selectOne(
"SELECT bp.*, b.booking_date, b.start_time, b.end_time, b.booker_name,
b.organization_name, b.status as booking_status
FROM sa_booking_passes bp
INNER JOIN sa_bookings b ON b.id = bp.booking_id
WHERE bp.pass_number = ?",
[$passNumber]
);
if (!$pass) {
return ['valid' => false, 'error' => 'رقم الباس غير موجود'];
}
if ($pass['status'] !== SaConstants::PASS_ISSUED) {
return ['valid' => false, 'error' => 'الباس ' . $pass['status'], 'pass' => $pass];
}
if ($pass['booking_status'] === 'cancelled') {
return ['valid' => false, 'error' => 'الحجز ملغي', 'pass' => $pass];
}
return ['valid' => true, 'pass' => $pass];
}
public static function getBookingPasses(int $bookingId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM sa_booking_passes WHERE booking_id = ? ORDER BY pass_number",
[$bookingId]
);
}
public static function expireBookingPasses(int $bookingId): int
{
$db = App::getInstance()->db();
$now = date('Y-m-d H:i:s');
$stmt = $db->query(
"UPDATE sa_booking_passes SET status = ?, updated_at = ? WHERE booking_id = ? AND status = ?",
[SaConstants::PASS_EXPIRED, $now, $bookingId, SaConstants::PASS_ISSUED]
);
return $stmt->rowCount();
}
}
...@@ -17,19 +17,25 @@ final class BookingService ...@@ -17,19 +17,25 @@ final class BookingService
$date = $data['booking_date'] ?? ''; $date = $data['booking_date'] ?? '';
$startTime = $data['start_time'] ?? ''; $startTime = $data['start_time'] ?? '';
$endTime = $data['end_time'] ?? ''; $endTime = $data['end_time'] ?? '';
$participantCount = (int) ($data['participant_count'] ?? 1);
$spotsReserved = (int) ($data['spots_reserved'] ?? $participantCount);
$bookerType = $data['booker_type'] ?? SaConstants::BOOKER_GUEST; $bookerType = $data['booker_type'] ?? SaConstants::BOOKER_GUEST;
$bookerId = isset($data['booker_id']) ? (int) $data['booker_id'] : null; $bookerId = isset($data['booker_id']) ? (int) $data['booker_id'] : null;
$bookerName = $data['booker_name'] ?? ''; $bookerName = $data['booker_name'] ?? '';
$organizationName = $data['organization_name'] ?? null;
$organizationContact = $data['organization_contact'] ?? null;
$participantMode = $data['participant_mode'] ?? SaConstants::PARTICIPANT_MODE_PASSES;
$isMember = ($bookerType === SaConstants::BOOKER_MEMBER); $isMember = ($bookerType === SaConstants::BOOKER_MEMBER);
$unit = $db->selectOne("SELECT expected_capacity FROM sa_facility_units WHERE id = ?", [$unitId]);
$expectedCapacity = $unit ? (int) $unit['expected_capacity'] : 1;
$participantCount = (int) ($data['participant_count'] ?? $expectedCapacity);
$spotsReserved = (int) ($data['spots_reserved'] ?? $participantCount);
$availability = SlotAvailabilityService::check($unitId, $date, $startTime, $endTime, $spotsReserved); $availability = SlotAvailabilityService::check($unitId, $date, $startTime, $endTime, $spotsReserved);
if (!$availability['available']) { if (!$availability['available']) {
return ['success' => false, 'error' => $availability['reason']]; return ['success' => false, 'error' => $availability['reason']];
} }
$pricing = PricingCalculatorService::calculate($unitId, $date, $startTime, $endTime, $participantCount, $isMember); $pricing = PricingCalculatorService::calculate($unitId, $date, $startTime, $endTime, $participantCount, $bookerType);
$totalAmount = $pricing ? $pricing['total_amount'] : 0.00; $totalAmount = $pricing ? $pricing['total_amount'] : 0.00;
$bookingNumber = NumberGeneratorService::bookingNumber(); $bookingNumber = NumberGeneratorService::bookingNumber();
...@@ -38,25 +44,30 @@ final class BookingService ...@@ -38,25 +44,30 @@ final class BookingService
$db->beginTransaction(); $db->beginTransaction();
try { try {
$bookingData = [ $bookingData = [
'booking_number' => $bookingNumber, 'booking_number' => $bookingNumber,
'facility_unit_id' => $unitId, 'facility_unit_id' => $unitId,
'booking_type' => SaConstants::BOOKING_TYPE_HOURLY, 'booking_type' => SaConstants::BOOKING_TYPE_HOURLY,
'booking_date' => $date, 'booking_date' => $date,
'start_time' => $startTime, 'start_time' => $startTime,
'end_time' => $endTime, 'end_time' => $endTime,
'booker_type' => $bookerType, 'booker_type' => $bookerType,
'booker_id' => $bookerId, 'booker_id' => $bookerId,
'booker_name' => $bookerName, 'booker_name' => $bookerName,
'participant_count' => $participantCount, 'organization_name' => $organizationName,
'spots_reserved' => $spotsReserved, 'organization_contact' => $organizationContact,
'total_amount' => $totalAmount, 'participant_count' => $participantCount,
'payment_status' => SaConstants::PAYMENT_UNPAID, 'participant_mode' => $participantMode,
'status' => SaConstants::BOOKING_CONFIRMED, 'passes_total' => ($participantMode === SaConstants::PARTICIPANT_MODE_PASSES) ? $participantCount : 0,
'notes' => $data['notes'] ?? null, 'passes_used' => 0,
'branch_id' => $data['branch_id'] ?? null, 'spots_reserved' => $spotsReserved,
'created_by' => $employeeId, 'total_amount' => $totalAmount,
'created_at' => date('Y-m-d H:i:s'), 'payment_status' => SaConstants::PAYMENT_UNPAID,
'updated_at' => date('Y-m-d H:i:s'), 'status' => SaConstants::BOOKING_CONFIRMED,
'notes' => $data['notes'] ?? null,
'branch_id' => $data['branch_id'] ?? null,
'created_by' => $employeeId,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]; ];
$db->insert('sa_bookings', $bookingData); $db->insert('sa_bookings', $bookingData);
...@@ -75,6 +86,10 @@ final class BookingService ...@@ -75,6 +86,10 @@ final class BookingService
} }
} }
if ($participantMode === SaConstants::PARTICIPANT_MODE_PASSES && $participantCount > 0) {
BookingPassService::generatePasses($bookingId, $participantCount);
}
$db->commit(); $db->commit();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$db->rollBack(); $db->rollBack();
......
...@@ -215,6 +215,188 @@ final class EnrollmentService ...@@ -215,6 +215,188 @@ final class EnrollmentService
} }
} }
public static function forceEnroll(int $groupId, int $playerId): array
{
$db = App::getInstance()->db();
$player = $db->selectOne(
"SELECT * FROM sa_players WHERE id = ? AND is_archived = 0",
[$playerId]
);
if (!$player) {
return ['success' => false, 'error' => 'اللاعب غير مسجل في النظام'];
}
$group = $db->selectOne("SELECT * FROM sa_groups WHERE id = ? AND is_archived = 0", [$groupId]);
if (!$group) {
return ['success' => false, 'error' => 'المجموعة غير موجودة'];
}
if ($group['status'] !== SaConstants::GROUP_ACTIVE) {
return ['success' => false, 'error' => 'المجموعة غير نشطة'];
}
$existing = $db->selectOne(
"SELECT id FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status IN ('active','pending_payment')",
[$groupId, $playerId]
);
if ($existing) {
return ['success' => false, 'error' => 'اللاعب مسجل بالفعل في هذه المجموعة'];
}
$fee = $player['player_type'] === SaConstants::PLAYER_MEMBER
? (string) $group['monthly_fee_member']
: (string) $group['monthly_fee_nonmember'];
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
$db->beginTransaction();
try {
$enrollmentId = $db->insert('sa_group_players', [
'group_id' => $groupId,
'player_id' => $playerId,
'enrolled_at' => date('Y-m-d'),
'status' => SaConstants::STATUS_PENDING_PAYMENT,
'force_enrolled' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employeeId,
]);
$memberId = !empty($player['member_id']) ? (int) $player['member_id'] : 0;
$description = 'تسجيل رياضي (تجاوز السعة) — ' . $group['name_ar'] . ' — ' . $player['full_name_ar'];
$requestResult = PaymentRequestService::createRequest([
'member_id' => $memberId,
'payment_type' => SaConstants::PAY_TYPE_SPORTS_REG,
'amount' => $fee,
'description_ar' => $description,
'related_entity_type' => SaConstants::ENTITY_GROUP_PLAYERS,
'related_entity_id' => $enrollmentId,
]);
if ($requestResult['success']) {
$db->update('sa_group_players', [
'payment_request_id' => (int) $requestResult['request_id'],
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$enrollmentId]);
}
$newCount = (int) $group['current_count'] + 1;
$db->update('sa_groups', [
'current_count' => $newCount,
'is_full' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$groupId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل عملية التسجيل: ' . $e->getMessage()];
}
return [
'success' => true,
'enrollment_id' => $enrollmentId,
'request_id' => $requestResult['request_id'] ?? null,
'request_number' => $requestResult['request_number'] ?? null,
'fee' => $fee,
'force_enrolled' => true,
];
}
public static function findOrCreateAvailableGroup(int $programId): array
{
$db = App::getInstance()->db();
$program = $db->selectOne(
"SELECT * FROM sa_programs WHERE id = ? AND is_archived = 0",
[$programId]
);
if (!$program) {
return ['success' => false, 'error' => 'البرنامج غير موجود'];
}
$availableGroup = $db->selectOne(
"SELECT * FROM sa_groups WHERE program_id = ? AND status = 'active' AND is_archived = 0 AND is_full = 0
ORDER BY id DESC LIMIT 1",
[$programId]
);
if ($availableGroup) {
return ['success' => true, 'group' => $availableGroup];
}
$sourceGroup = $db->selectOne(
"SELECT * FROM sa_groups WHERE program_id = ? AND is_archived = 0
ORDER BY id DESC LIMIT 1",
[$programId]
);
if (!$sourceGroup) {
return ['success' => false, 'error' => 'لا توجد مجموعات مرجعية للبرنامج'];
}
$newName = self::incrementGroupName($sourceGroup['name_ar']);
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
$db->beginTransaction();
try {
$newGroupId = $db->insert('sa_groups', [
'name_ar' => $newName,
'program_id' => $programId,
'facility_unit_id' => $sourceGroup['facility_unit_id'],
'coach_id' => $sourceGroup['coach_id'],
'monthly_fee_member' => $sourceGroup['monthly_fee_member'],
'monthly_fee_nonmember' => $sourceGroup['monthly_fee_nonmember'],
'min_age' => $sourceGroup['min_age'],
'max_age' => $sourceGroup['max_age'],
'max_capacity' => $sourceGroup['max_capacity'],
'current_count' => 0,
'is_full' => 0,
'status' => 'active',
'branch_id' => $sourceGroup['branch_id'] ?? null,
'created_by' => $employeeId,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$schedules = $db->select(
"SELECT * FROM sa_group_schedule WHERE group_id = ? AND is_active = 1",
[(int) $sourceGroup['id']]
);
foreach ($schedules as $sch) {
$db->insert('sa_group_schedule', [
'group_id' => $newGroupId,
'facility_unit_id' => $sch['facility_unit_id'],
'day_of_week' => $sch['day_of_week'],
'start_time' => $sch['start_time'],
'end_time' => $sch['end_time'],
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
$db->commit();
$newGroup = $db->selectOne("SELECT * FROM sa_groups WHERE id = ?", [$newGroupId]);
return ['success' => true, 'group' => $newGroup, 'auto_created' => true];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل إنشاء مجموعة جديدة: ' . $e->getMessage()];
}
}
private static function incrementGroupName(string $name): string
{
if (preg_match('/^(.+)\s(\d+)$/', $name, $matches)) {
return $matches[1] . ' ' . ((int) $matches[2] + 1);
}
return $name . ' 2';
}
public static function deactivateEnrollment(int $enrollmentId): bool public static function deactivateEnrollment(int $enrollmentId): bool
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
......
...@@ -9,7 +9,7 @@ use App\Modules\SportsActivity\SaConstants; ...@@ -9,7 +9,7 @@ use App\Modules\SportsActivity\SaConstants;
final class GroupTransferService final class GroupTransferService
{ {
public static function transfer(int $playerId, int $fromGroupId, int $toGroupId, string $reason = ''): array public static function transfer(int $playerId, int $fromGroupId, int $toGroupId, string $reason = '', bool $force = false): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -37,7 +37,7 @@ final class GroupTransferService ...@@ -37,7 +37,7 @@ final class GroupTransferService
return ['success' => false, 'error' => 'المجموعة المستهدفة غير موجودة أو غير نشطة']; return ['success' => false, 'error' => 'المجموعة المستهدفة غير موجودة أو غير نشطة'];
} }
if ((int) $toGroup['current_count'] >= (int) $toGroup['max_capacity']) { if (!$force && (int) $toGroup['current_count'] >= (int) $toGroup['max_capacity']) {
$db->rollBack(); $db->rollBack();
return ['success' => false, 'error' => 'المجموعة المستهدفة ممتلئة']; return ['success' => false, 'error' => 'المجموعة المستهدفة ممتلئة'];
} }
......
...@@ -27,7 +27,9 @@ final class MirrorStateService ...@@ -27,7 +27,9 @@ final class MirrorStateService
$bookings = $db->select( $bookings = $db->select(
"SELECT b.*, fu.name_ar as unit_name, fu.booking_mode, fu.max_capacity, "SELECT b.*, fu.name_ar as unit_name, fu.booking_mode, fu.max_capacity,
c.full_name_ar as coach_name, g.name_ar as group_name c.full_name_ar as coach_name, g.name_ar as group_name,
b.booker_type, b.organization_name, b.passes_total, b.passes_used,
b.participant_mode
FROM sa_bookings b FROM sa_bookings b
JOIN sa_facility_units fu ON fu.id = b.facility_unit_id JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
LEFT JOIN sa_coaches c ON c.id = b.coach_id LEFT JOIN sa_coaches c ON c.id = b.coach_id
...@@ -179,6 +181,43 @@ final class MirrorStateService ...@@ -179,6 +181,43 @@ final class MirrorStateService
]; ];
} }
public static function getMultiFacilityState(int $disciplineId, string $date): array
{
$db = App::getInstance()->db();
$discipline = $db->selectOne(
"SELECT * FROM sa_disciplines WHERE id = ? AND is_active = 1 AND is_archived = 0",
[$disciplineId]
);
if (!$discipline) {
return ['error' => 'اللعبة غير موجودة'];
}
$facilities = $db->select(
"SELECT * FROM sa_facilities WHERE discipline_id = ? AND is_active = 1 AND is_archived = 0 ORDER BY name_ar",
[$disciplineId]
);
if (empty($facilities)) {
return ['error' => 'لا توجد مرافق مرتبطة بهذه اللعبة'];
}
$facilityStates = [];
foreach ($facilities as $facility) {
$state = self::getFacilityState((int) $facility['id'], $date);
if (!isset($state['error'])) {
$facilityStates[] = $state;
}
}
return [
'discipline' => $discipline,
'facilities' => $facilityStates,
'date' => $date,
];
}
private static function buildTimeSlots(string $start, string $end, int $slotMinutes): array private static function buildTimeSlots(string $start, string $end, int $slotMinutes): array
{ {
$slots = []; $slots = [];
......
...@@ -113,7 +113,7 @@ final class PoolGridService ...@@ -113,7 +113,7 @@ final class PoolGridService
$ts = date('Y-m-d H:i:s'); $ts = date('Y-m-d H:i:s');
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0); $employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
$validActions = ['training', 'blocked', 'maintenance', 'hourly']; $validActions = ['training', 'blocked', 'maintenance', 'hourly', 'open_access'];
if (!in_array($action, $validActions, true)) { if (!in_array($action, $validActions, true)) {
return ['success' => false, 'error' => 'إجراء غير صالح']; return ['success' => false, 'error' => 'إجراء غير صالح'];
} }
...@@ -146,6 +146,8 @@ final class PoolGridService ...@@ -146,6 +146,8 @@ final class PoolGridService
$label = 'صيانة'; $label = 'صيانة';
} elseif ($action === 'hourly') { } elseif ($action === 'hourly') {
$label = $notes ?: 'حجز ساعة'; $label = $notes ?: 'حجز ساعة';
} elseif ($action === 'open_access') {
$label = 'دخول حر';
} }
// Pre-fetch all active pool zone bookings for this facility+date // Pre-fetch all active pool zone bookings for this facility+date
...@@ -220,7 +222,8 @@ final class PoolGridService ...@@ -220,7 +222,8 @@ final class PoolGridService
'end_time' => $endTimeFull, 'end_time' => $endTimeFull,
'zone_row' => $row, 'zone_row' => $row,
'zone_col' => $col, 'zone_col' => $col,
'assignment_type' => $action, 'assignment_type' => ($action === 'open_access') ? 'hourly' : $action,
'access_mode' => ($action === 'open_access') ? 'open_access' : 'reserved',
'group_id' => $groupId, 'group_id' => $groupId,
'coach_id' => $coachId, 'coach_id' => $coachId,
'label' => $label, 'label' => $label,
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
use App\Modules\Cashier\Services\PaymentRequestService;
use App\Modules\SportsActivity\SaConstants;
final class PoolTicketService
{
public static function issueTicket(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$branch = App::getInstance()->currentBranch();
$facilityId = (int) ($data['facility_id'] ?? 0);
$date = $data['booking_date'] ?? date('Y-m-d');
$startTime = $data['start_time'] ?? '';
$endTime = $data['end_time'] ?? '';
$isMember = (bool) ($data['is_member'] ?? false);
$memberId = isset($data['member_id']) ? (int) $data['member_id'] : null;
$playerId = isset($data['player_id']) ? (int) $data['player_id'] : null;
$guestName = $data['guest_name'] ?? null;
$guestNationalId = $data['guest_national_id'] ?? null;
$zone = $db->selectOne(
"SELECT * FROM sa_pool_zone_bookings
WHERE facility_id = ? AND booking_date = ? AND start_time <= ? AND end_time > ?
AND access_mode = 'open_access' AND status = 'active'
ORDER BY current_occupancy ASC LIMIT 1",
[$facilityId, $date, $startTime . ':00', $startTime . ':00']
);
if (!$zone) {
return ['success' => false, 'error' => 'لا توجد منطقة دخول حر متاحة في هذا الوقت'];
}
if (!empty($zone['access_restriction']) && $zone['access_restriction'] === 'members_only' && !$isMember) {
return ['success' => false, 'error' => 'هذه المنطقة متاحة للأعضاء فقط'];
}
if (!empty($zone['max_occupancy']) && (int) $zone['current_occupancy'] >= (int) $zone['max_occupancy']) {
return ['success' => false, 'error' => 'المنطقة وصلت للحد الأقصى من الإشغال'];
}
$price = $isMember
? (float) ($zone['ticket_price_member'] ?? 0)
: (float) ($zone['ticket_price_nonmember'] ?? 0);
$now = date('Y-m-d H:i:s');
$employeeId = $employee ? (int) $employee->id : null;
$db->beginTransaction();
try {
$ticketId = $db->insert('sa_pool_tickets', [
'facility_id' => $facilityId,
'zone_booking_id' => (int) $zone['id'],
'booking_date' => $date,
'start_time' => $zone['start_time'],
'end_time' => $zone['end_time'],
'member_id' => $memberId,
'player_id' => $playerId,
'guest_name' => $guestName,
'guest_national_id' => $guestNationalId,
'is_member' => $isMember ? 1 : 0,
'amount_paid' => $price,
'payment_status' => 'unpaid',
'status' => SaConstants::TICKET_ISSUED,
'issued_by' => $employeeId,
'branch_id' => $branch ? (int) $branch['id'] : null,
'created_at' => $now,
'updated_at' => $now,
]);
$db->query(
"UPDATE sa_pool_zone_bookings SET current_occupancy = current_occupancy + 1, updated_at = ? WHERE id = ?",
[$now, (int) $zone['id']]
);
if ($price > 0) {
$description = 'تذكرة دخول حمام السباحة — ' . ($guestName ?: 'عضو');
$payResult = PaymentRequestService::createRequest([
'member_id' => $memberId ?? 0,
'payment_type' => 'sa_pool_ticket',
'amount' => (string) $price,
'description_ar' => $description,
'related_entity_type' => 'sa_pool_tickets',
'related_entity_id' => $ticketId,
]);
if ($payResult['success']) {
$db->update('sa_pool_tickets', [
'payment_request_id' => (int) $payResult['request_id'],
'updated_at' => $now,
], 'id = ?', [$ticketId]);
}
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل إصدار التذكرة: ' . $e->getMessage()];
}
return [
'success' => true,
'ticket_id' => $ticketId,
'price' => $price,
'zone_id' => (int) $zone['id'],
];
}
public static function cancelTicket(int $ticketId, string $reason = ''): array
{
$db = App::getInstance()->db();
$ticket = $db->selectOne("SELECT * FROM sa_pool_tickets WHERE id = ?", [$ticketId]);
if (!$ticket) {
return ['success' => false, 'error' => 'التذكرة غير موجودة'];
}
if ($ticket['status'] === 'cancelled') {
return ['success' => false, 'error' => 'التذكرة ملغاة بالفعل'];
}
$now = date('Y-m-d H:i:s');
$db->update('sa_pool_tickets', [
'status' => SaConstants::TICKET_CANCELLED,
'updated_at' => $now,
], 'id = ?', [$ticketId]);
if (!empty($ticket['zone_booking_id'])) {
$db->query(
"UPDATE sa_pool_zone_bookings SET current_occupancy = GREATEST(0, current_occupancy - 1), updated_at = ? WHERE id = ?",
[$now, (int) $ticket['zone_booking_id']]
);
}
return ['success' => true];
}
public static function recordEntry(int $ticketId): array
{
$db = App::getInstance()->db();
$now = date('Y-m-d H:i:s');
$db->update('sa_pool_tickets', [
'status' => SaConstants::TICKET_ACTIVE,
'entry_time' => $now,
'updated_at' => $now,
], 'id = ?', [$ticketId]);
return ['success' => true];
}
public static function recordExit(int $ticketId): array
{
$db = App::getInstance()->db();
$now = date('Y-m-d H:i:s');
$ticket = $db->selectOne("SELECT * FROM sa_pool_tickets WHERE id = ?", [$ticketId]);
if (!$ticket) {
return ['success' => false, 'error' => 'التذكرة غير موجودة'];
}
$db->update('sa_pool_tickets', [
'status' => SaConstants::TICKET_COMPLETED,
'exit_time' => $now,
'updated_at' => $now,
], 'id = ?', [$ticketId]);
if (!empty($ticket['zone_booking_id'])) {
$db->query(
"UPDATE sa_pool_zone_bookings SET current_occupancy = GREATEST(0, current_occupancy - 1), updated_at = ? WHERE id = ?",
[$now, (int) $ticket['zone_booking_id']]
);
}
return ['success' => true];
}
public static function getActiveTickets(int $facilityId, string $date): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM sa_pool_tickets
WHERE facility_id = ? AND booking_date = ? AND status IN ('issued','active')
ORDER BY created_at DESC",
[$facilityId, $date]
);
}
}
...@@ -13,8 +13,10 @@ final class PricingCalculatorService ...@@ -13,8 +13,10 @@ final class PricingCalculatorService
string $startTime, string $startTime,
string $endTime, string $endTime,
int $participantCount, int $participantCount,
bool $isMember bool|string $isMemberOrBookerType
): ?array { ): ?array {
$bookerType = is_string($isMemberOrBookerType) ? $isMemberOrBookerType : ($isMemberOrBookerType ? 'member' : 'guest');
$isMember = ($bookerType === 'member');
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$unit = $db->selectOne( $unit = $db->selectOne(
...@@ -58,9 +60,13 @@ final class PricingCalculatorService ...@@ -58,9 +60,13 @@ final class PricingCalculatorService
return null; return null;
} }
$pricePerPerson = $isMember if ($bookerType === 'organization' && !empty($rule['price_per_person_organization'])) {
? (float) $rule['price_per_person_member'] $pricePerPerson = (float) $rule['price_per_person_organization'];
: (float) $rule['price_per_person_nonmember']; } else {
$pricePerPerson = $isMember
? (float) $rule['price_per_person_member']
: (float) $rule['price_per_person_nonmember'];
}
$durationSeconds = strtotime($endTime) - strtotime($startTime); $durationSeconds = strtotime($endTime) - strtotime($startTime);
$durationHours = $durationSeconds / 3600.0; $durationHours = $durationSeconds / 3600.0;
...@@ -75,6 +81,7 @@ final class PricingCalculatorService ...@@ -75,6 +81,7 @@ final class PricingCalculatorService
'bracket_name' => $bracket['name_ar'], 'bracket_name' => $bracket['name_ar'],
'bracket_type' => $bracket['bracket_type'], 'bracket_type' => $bracket['bracket_type'],
'is_member' => $isMember, 'is_member' => $isMember,
'booker_type' => $bookerType,
'rule_id' => (int) $rule['id'], 'rule_id' => (int) $rule['id'],
]; ];
} }
......
...@@ -243,6 +243,29 @@ final class RegistrationWizardService ...@@ -243,6 +243,29 @@ final class RegistrationWizardService
]; ];
} }
public static function selectProgram(int $registrationId, int $programId, int $months = 1, bool $hasSibling = false): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT * FROM sa_registrations WHERE id = ? AND status = 'in_progress'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل'];
}
$groupResult = EnrollmentService::findOrCreateAvailableGroup($programId);
if (!$groupResult['success']) {
return $groupResult;
}
$group = $groupResult['group'];
$groupId = (int) $group['id'];
return self::selectGroup($registrationId, $groupId, $months, $hasSibling);
}
public static function calculateFees(string $playerType): array public static function calculateFees(string $playerType): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>باسات الحجز #<?= e($booking['booking_number'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/bookings/<?= (int) $booking['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تفاصيل الحجز</a>
<button onclick="window.print()" class="btn btn-primary"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Booking Info -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:10px;font-size:13px;">
<div><strong>الحاجز:</strong> <?= e($booking['booker_name'] ?? '') ?></div>
<div><strong>المرفق:</strong> <?= e($booking['facility_name'] ?? '') ?> - <?= e($booking['unit_name'] ?? '') ?></div>
<div><strong>التاريخ:</strong> <?= e($booking['booking_date'] ?? '') ?></div>
<div><strong>الوقت:</strong> <?= e($booking['start_time'] ?? '') ?> - <?= e($booking['end_time'] ?? '') ?></div>
<div><strong>نظام المشاركين:</strong>
<?php
$modeLabels = ['full' => 'تسجيل كامل', 'partial' => 'أسماء فقط', 'passes' => 'باسات'];
echo e($modeLabels[$booking['participant_mode'] ?? 'passes'] ?? 'باسات');
?>
</div>
<div><strong>الاستخدام:</strong> <?= (int) ($booking['passes_used'] ?? 0) ?> / <?= (int) ($booking['passes_total'] ?? 0) ?></div>
</div>
</div>
<!-- Passes List -->
<div class="card">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:700;">الباسات (<?= count($passes) ?>)</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">#</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">رقم الباس</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:center;">الحالة</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">تم الاستخدام</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">بواسطة</th>
</tr>
</thead>
<tbody>
<?php foreach ($passes as $i => $pass): ?>
<?php
$statusColors = ['issued' => '#ECFDF5', 'used' => '#FEF3C7', 'expired' => '#FEE2E2', 'cancelled' => '#F3F4F6'];
$statusLabels = ['issued' => 'متاح', 'used' => 'مستخدم', 'expired' => 'منتهي', 'cancelled' => 'ملغي'];
$statusColor = $statusColors[$pass['status']] ?? '#F3F4F6';
$statusLabel = $statusLabels[$pass['status']] ?? $pass['status'];
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px;"><?= $i + 1 ?></td>
<td style="padding:10px;font-family:monospace;font-weight:600;"><?= e($pass['pass_number']) ?></td>
<td style="padding:10px;text-align:center;">
<span style="background:<?= $statusColor ?>;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;"><?= e($statusLabel) ?></span>
</td>
<td style="padding:10px;font-size:12px;color:#6B7280;"><?= e($pass['used_at'] ?? '-') ?></td>
<td style="padding:10px;font-size:12px;"><?= e($pass['used_by_name'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مرآة: <?= e($discipline['name_ar'] ?? '') ?> - جميع المرافق<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/mirror" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> كل المرافق</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<meta http-equiv="refresh" content="30">
<!-- Date Picker -->
<div class="card" style="margin-bottom:20px;padding:15px;display:flex;align-items:center;gap:15px;">
<label style="font-weight:600;font-size:14px;">التاريخ:</label>
<input type="date" id="mirror-date" value="<?= e($date) ?>" class="form-input" style="width:180px;"
onchange="window.location.href='/sa/mirror/discipline/<?= (int) $discipline['id'] ?>/' + this.value;">
<?php if ($date === date('Y-m-d')): ?>
<span style="background:#ECFDF5;color:#059669;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;">اليوم</span>
<?php endif; ?>
<span style="font-size:13px;font-weight:600;color:#2563EB;"><?= e($discipline['name_ar'] ?? '') ?></span>
<span style="font-size:11px;color:#9CA3AF;margin-right:auto;">يتم التحديث تلقائيا كل 30 ثانية</span>
</div>
<!-- Legend -->
<div class="card" style="margin-bottom:20px;padding:12px;display:flex;gap:15px;flex-wrap:wrap;font-size:12px;">
<span><span style="display:inline-block;width:14px;height:14px;background:#D1FAE5;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> متاح</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEF3C7;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> جزئي</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEE2E2;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> ممتلئ</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#DBEAFE;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> تدريب</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#D1FAE5;border:2px solid #059669;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز عضو</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEF9C3;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز زائر</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FFEDD5;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز مؤسسة</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#E5E7EB;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> مغلق</span>
</div>
<?php foreach ($facilityStates as $state): ?>
<div class="card" style="margin-bottom:20px;overflow-x:auto;">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;background:#F9FAFB;">
<span style="font-weight:700;font-size:14px;"><?= e($state['facility']['name_ar'] ?? '') ?></span>
<a href="/sa/mirror/<?= (int) $state['facility']['id'] ?>/<?= e($date) ?>" style="font-size:12px;margin-right:10px;color:#2563EB;">عرض مفصل</a>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:800px;">
<thead>
<tr>
<th style="padding:8px;border:1px solid #E5E7EB;background:#F9FAFB;position:sticky;right:0;z-index:2;min-width:100px;">الوحدة</th>
<?php foreach ($state['slots'] as $slot): ?>
<th style="padding:6px 3px;border:1px solid #E5E7EB;background:#F9FAFB;text-align:center;white-space:nowrap;min-width:60px;font-size:11px;">
<?= e($slot['start']) ?>
</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($state['grid'] as $unitId => $unitData): ?>
<tr>
<td style="padding:8px;border:1px solid #E5E7EB;font-weight:600;background:#FAFAFA;position:sticky;right:0;z-index:1;font-size:11px;">
<?= e($unitData['unit']['name_ar'] ?? '') ?>
</td>
<?php foreach ($unitData['slots'] as $cell): ?>
<?php
$bgColor = '#FFFFFF';
$cellText = '';
if ($cell['status'] === 'free') {
$bgColor = '#D1FAE5';
} elseif ($cell['status'] === 'partial') {
$bgColor = '#FEF3C7';
$cellText = $cell['occupied'] . '/' . $cell['max'];
} elseif ($cell['status'] === 'full') {
$bgColor = '#FEE2E2';
} elseif ($cell['status'] === 'training') {
$bgColor = '#DBEAFE';
if (!empty($cell['bookings'])) {
$firstBooking = reset($cell['bookings']);
$cellText = mb_substr($firstBooking['group_name'] ?? '', 0, 8);
}
} elseif ($cell['status'] === 'booked') {
if (!empty($cell['bookings'])) {
$firstBooking = reset($cell['bookings']);
$bookerType = $firstBooking['booker_type'] ?? 'guest';
if ($bookerType === 'organization') {
$bgColor = '#FFEDD5';
} elseif ($bookerType === 'member') {
$bgColor = '#D1FAE5';
} else {
$bgColor = '#FEF9C3';
}
$cellText = mb_substr($firstBooking['booker_name'] ?? '', 0, 8);
}
} elseif ($cell['status'] === 'blocked') {
$bgColor = '#E5E7EB';
}
?>
<td style="padding:4px 2px;border:1px solid #E5E7EB;background:<?= $bgColor ?>;text-align:center;vertical-align:middle;font-size:10px;font-weight:500;">
<?= e($cellText) ?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endforeach; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
});
</script>
<?php $__template->endSection(); ?>
...@@ -25,7 +25,10 @@ ...@@ -25,7 +25,10 @@
<span><span style="display:inline-block;width:14px;height:14px;background:#FEF3C7;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> جزئي</span> <span><span style="display:inline-block;width:14px;height:14px;background:#FEF3C7;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> جزئي</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEE2E2;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> ممتلئ</span> <span><span style="display:inline-block;width:14px;height:14px;background:#FEE2E2;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> ممتلئ</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#DBEAFE;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> تدريب</span> <span><span style="display:inline-block;width:14px;height:14px;background:#DBEAFE;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> تدريب</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FFEDD5;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> محجوز</span> <span><span style="display:inline-block;width:14px;height:14px;background:#D1FAE5;border:2px solid #059669;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز عضو</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEF9C3;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز زائر</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FFEDD5;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز مؤسسة</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#CFFAFE;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> دخول حر</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#E5E7EB;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> مغلق</span> <span><span style="display:inline-block;width:14px;height:14px;background:#E5E7EB;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> مغلق</span>
</div> </div>
...@@ -50,28 +53,55 @@ ...@@ -50,28 +53,55 @@
</td> </td>
<?php foreach ($unitData['slots'] as $cell): ?> <?php foreach ($unitData['slots'] as $cell): ?>
<?php <?php
$cellColors = [ $bgColor = '#FFFFFF';
'free' => '#D1FAE5',
'partial' => '#FEF3C7',
'full' => '#FEE2E2',
'training' => '#DBEAFE',
'booked' => '#FFEDD5',
'blocked' => '#E5E7EB',
];
$bgColor = $cellColors[$cell['status']] ?? '#FFFFFF';
$cellText = ''; $cellText = '';
if ($cell['status'] === 'partial') { $passIndicator = '';
if ($cell['status'] === 'free') {
$bgColor = '#D1FAE5';
} elseif ($cell['status'] === 'partial') {
$bgColor = '#FEF3C7';
$cellText = $cell['occupied'] . '/' . $cell['max'];
} elseif ($cell['status'] === 'full') {
$bgColor = '#FEE2E2';
$cellText = $cell['occupied'] . '/' . $cell['max']; $cellText = $cell['occupied'] . '/' . $cell['max'];
} elseif ($cell['status'] === 'training' && !empty($cell['bookings'])) { } elseif ($cell['status'] === 'training') {
$firstBooking = reset($cell['bookings']); $bgColor = '#DBEAFE';
$cellText = $firstBooking['group_name'] ?? ''; if (!empty($cell['bookings'])) {
} elseif ($cell['status'] === 'booked' && !empty($cell['bookings'])) { $firstBooking = reset($cell['bookings']);
$firstBooking = reset($cell['bookings']); $cellText = $firstBooking['group_name'] ?? '';
$cellText = $firstBooking['booker_name'] ?? ''; }
} elseif ($cell['status'] === 'booked') {
if (!empty($cell['bookings'])) {
$firstBooking = reset($cell['bookings']);
$bookerType = $firstBooking['booker_type'] ?? 'guest';
if ($bookerType === 'organization') {
$bgColor = '#FFEDD5';
$cellText = $firstBooking['organization_name'] ?? $firstBooking['booker_name'] ?? '';
} elseif ($bookerType === 'member') {
$bgColor = '#D1FAE5';
$cellText = $firstBooking['booker_name'] ?? '';
} else {
$bgColor = '#FEF9C3';
$cellText = $firstBooking['booker_name'] ?? '';
}
$passesTotal = (int) ($firstBooking['passes_total'] ?? 0);
$passesUsed = (int) ($firstBooking['passes_used'] ?? 0);
if ($passesTotal > 0) {
$passIndicator = $passesUsed . '/' . $passesTotal;
}
}
} elseif ($cell['status'] === 'blocked') {
$bgColor = '#E5E7EB';
} }
?> ?>
<td style="padding:6px 4px;border:1px solid #E5E7EB;background:<?= $bgColor ?>;text-align:center;vertical-align:middle;font-size:11px;font-weight:500;"> <td style="padding:6px 4px;border:1px solid #E5E7EB;background:<?= $bgColor ?>;text-align:center;vertical-align:middle;font-size:11px;font-weight:500;position:relative;">
<?= e($cellText) ?> <?= e($cellText) ?>
<?php if ($passIndicator !== ''): ?>
<span style="position:absolute;top:2px;left:2px;font-size:9px;color:#6B7280;background:rgba(255,255,255,0.8);padding:0 2px;border-radius:2px;"><?= e($passIndicator) ?></span>
<?php endif; ?>
</td> </td>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </tr>
......
...@@ -3,31 +3,49 @@ ...@@ -3,31 +3,49 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:15px;"> <?php if (!empty($disciplines)): ?>
<?php if (!empty($facilities)): ?> <div class="card" style="margin-bottom:20px;padding:15px;">
<?php foreach ($facilities as $f): ?> <div style="font-weight:700;margin-bottom:10px;font-size:14px;">تصفية بالرياضة</div>
<a href="/sa/mirror/<?= (int) $f['id'] ?>" class="card" style="padding:20px;text-decoration:none;color:inherit;transition:box-shadow 0.2s;border-right:4px solid #3B82F6;"> <div style="display:flex;gap:10px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:12px;"> <?php foreach ($disciplines as $d): ?>
<div style="width:44px;height:44px;background:#DBEAFE;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <a href="/sa/mirror/discipline/<?= (int) $d['id'] ?>" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="monitor" style="width:22px;height:22px;color:#3B82F6;"></i> <?= e($d['name_ar']) ?> <span style="background:#DBEAFE;color:#2563EB;padding:1px 6px;border-radius:8px;font-size:11px;margin-right:4px;"><?= (int) $d['facility_count'] ?></span>
</div> </a>
<div> <?php endforeach; ?>
<div style="font-weight:700;font-size:15px;"><?= e($f['name_ar'] ?? '') ?></div> </div>
<?php </div>
$typeLabels = ['pool' => 'حمام سباحة', 'court' => 'ملعب', 'pitch' => 'ملعب كبير', 'gym' => 'صالة رياضية', 'track' => 'مضمار', 'multipurpose' => 'متعدد']; <?php endif; ?>
?>
<div style="font-size:12px;color:#6B7280;"><?= e($typeLabels[$f['facility_type'] ?? ''] ?? '') ?></div> <?php if (!empty($grouped)): ?>
<?php foreach ($grouped as $disciplineName => $disciplineFacilities): ?>
<div style="margin-bottom:20px;">
<h3 style="font-size:15px;font-weight:700;margin-bottom:10px;color:#374151;"><?= e($disciplineName) ?></h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:15px;">
<?php foreach ($disciplineFacilities as $f): ?>
<a href="/sa/mirror/<?= (int) $f['id'] ?>" class="card" style="padding:20px;text-decoration:none;color:inherit;transition:box-shadow 0.2s;border-right:4px solid #3B82F6;">
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:44px;height:44px;background:#DBEAFE;border-radius:10px;display:flex;align-items:center;justify-content:center;">
<i data-lucide="monitor" style="width:22px;height:22px;color:#3B82F6;"></i>
</div>
<div>
<div style="font-weight:700;font-size:15px;"><?= e($f['name_ar'] ?? '') ?></div>
<?php
$typeLabels = ['pool' => 'حمام سباحة', 'court' => 'ملعب', 'pitch' => 'ملعب كبير', 'gym' => 'صالة رياضية', 'track' => 'مضمار', 'multipurpose' => 'متعدد'];
?>
<div style="font-size:12px;color:#6B7280;"><?= e($typeLabels[$f['facility_type'] ?? ''] ?? '') ?></div>
</div>
</div> </div>
</div> </a>
</a> <?php endforeach; ?>
<?php endforeach; ?>
<?php else: ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;grid-column:1/-1;">
<i data-lucide="monitor" style="width:36px;height:36px;color:#D1D5DB;display:block;margin:0 auto 10px;"></i>
لا توجد مرافق نشطة
</div> </div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?>
<?php else: ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<i data-lucide="monitor" style="width:36px;height:36px;color:#D1D5DB;display:block;margin:0 auto 10px;"></i>
لا توجد مرافق نشطة
</div>
<?php endif; ?>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تذاكر حمام السباحة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sa/pool-tickets" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">حمام السباحة</label>
<select name="facility_id" class="form-input" style="min-width:200px;">
<option value="">-- اختر --</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= $facilityId === (int) $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">التاريخ</label>
<input type="date" name="date" value="<?= e($date) ?>" class="form-input">
</div>
<button type="submit" class="btn btn-primary">عرض</button>
</form>
</div>
<?php if ($facilityId > 0): ?>
<!-- Issue Ticket Form -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<h3 style="font-size:14px;font-weight:700;margin-bottom:12px;">إصدار تذكرة جديدة</h3>
<form id="issueTicketForm" style="display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:10px;align-items:end;">
<input type="hidden" name="facility_id" value="<?= $facilityId ?>">
<input type="hidden" name="booking_date" value="<?= e($date) ?>">
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">وقت البداية</label>
<input type="time" name="start_time" class="form-input" required>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">وقت النهاية</label>
<input type="time" name="end_time" class="form-input" required>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">النوع</label>
<select name="is_member" class="form-input">
<option value="0">زائر</option>
<option value="1">عضو</option>
</select>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">الاسم</label>
<input type="text" name="guest_name" class="form-input" placeholder="اسم الزائر">
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">الرقم القومي</label>
<input type="text" name="guest_national_id" class="form-input" placeholder="اختياري" maxlength="14">
</div>
<div>
<button type="submit" class="btn btn-primary" style="width:100%;">إصدار تذكرة</button>
</div>
</form>
<div id="ticketResult" style="margin-top:10px;display:none;"></div>
</div>
<!-- Active Tickets -->
<div class="card">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;">التذاكر النشطة (<?= count($tickets) ?>)</span>
</div>
<?php if (empty($tickets)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;">لا توجد تذاكر نشطة</div>
<?php else: ?>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">#</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">الاسم</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:center;">نوع</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">الوقت</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:right;">المبلغ</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:center;">الحالة</th>
<th style="padding:10px;border-bottom:1px solid #E5E7EB;text-align:center;">إجراء</th>
</tr>
</thead>
<tbody>
<?php foreach ($tickets as $t): ?>
<?php
$statusLabels = ['issued' => 'صادرة', 'active' => 'داخل', 'completed' => 'خرج', 'cancelled' => 'ملغاة'];
$statusColors = ['issued' => '#DBEAFE', 'active' => '#ECFDF5', 'completed' => '#F3F4F6', 'cancelled' => '#FEE2E2'];
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px;"><?= (int) $t['id'] ?></td>
<td style="padding:10px;"><?= e($t['guest_name'] ?? 'عضو') ?></td>
<td style="padding:10px;text-align:center;">
<span style="background:<?= $t['is_member'] ? '#ECFDF5' : '#FEF3C7' ?>;padding:2px 8px;border-radius:8px;font-size:11px;"><?= $t['is_member'] ? 'عضو' : 'زائر' ?></span>
</td>
<td style="padding:10px;font-size:12px;"><?= e($t['start_time'] ?? '') ?> - <?= e($t['end_time'] ?? '') ?></td>
<td style="padding:10px;"><?= number_format((float) ($t['amount_paid'] ?? 0), 2) ?> ج.م</td>
<td style="padding:10px;text-align:center;">
<span style="background:<?= $statusColors[$t['status']] ?? '#F3F4F6' ?>;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;"><?= e($statusLabels[$t['status']] ?? $t['status']) ?></span>
</td>
<td style="padding:10px;text-align:center;">
<?php if ($t['status'] === 'issued'): ?>
<button onclick="ticketAction(<?= (int) $t['id'] ?>, 'entry')" class="btn btn-sm" style="font-size:11px;padding:3px 8px;background:#059669;color:white;border:none;border-radius:4px;cursor:pointer;">دخول</button>
<button onclick="ticketAction(<?= (int) $t['id'] ?>, 'cancel')" class="btn btn-sm" style="font-size:11px;padding:3px 8px;background:#DC2626;color:white;border:none;border-radius:4px;cursor:pointer;">إلغاء</button>
<?php elseif ($t['status'] === 'active'): ?>
<button onclick="ticketAction(<?= (int) $t['id'] ?>, 'exit')" class="btn btn-sm" style="font-size:11px;padding:3px 8px;background:#D97706;color:white;border:none;border-radius:4px;cursor:pointer;">خروج</button>
<?php else: ?>
-
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
const form = document.getElementById('issueTicketForm');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
const fd = new FormData(form);
const data = {};
fd.forEach((v,k) => data[k] = v);
fetch('/sa/pool-tickets/issue', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]')?.content || ''},
body: new URLSearchParams(data)
})
.then(r => r.json())
.then(res => {
const el = document.getElementById('ticketResult');
if (res.success) {
el.innerHTML = '<div style="background:#ECFDF5;padding:8px 12px;border-radius:6px;color:#059669;font-size:13px;">تم إصدار التذكرة بنجاح — المبلغ: ' + (res.price || 0) + ' ج.م</div>';
el.style.display = 'block';
setTimeout(() => location.reload(), 1500);
} else {
el.innerHTML = '<div style="background:#FEE2E2;padding:8px 12px;border-radius:6px;color:#DC2626;font-size:13px;">' + (res.error || 'خطأ') + '</div>';
el.style.display = 'block';
}
});
});
}
});
function ticketAction(id, action) {
fetch('/sa/pool-tickets/' + id + '/' + action, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]')?.content || ''},
})
.then(r => r.json())
.then(res => {
if (res.success) { location.reload(); }
else { alert(res.error || 'خطأ'); }
});
}
</script>
<?php $__template->endSection(); ?>
...@@ -40,6 +40,7 @@ MenuRegistry::register('sports_activity', [ ...@@ -40,6 +40,7 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 19], ['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 19],
['label_ar' => 'اللوكرات', 'label_en' => 'Lockers', 'route' => '/sa/lockers', 'permission' => 'sa.locker.view', 'order' => 20], ['label_ar' => 'اللوكرات', 'label_en' => 'Lockers', 'route' => '/sa/lockers', 'permission' => 'sa.locker.view', 'order' => 20],
['label_ar' => 'إيجارات اللوكرات', 'label_en' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 21], ['label_ar' => 'إيجارات اللوكرات', 'label_en' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 21],
['label_ar' => 'تذاكر السباحة', 'label_en' => 'Pool Tickets', 'route' => '/sa/pool-tickets', 'permission' => 'sa.pool_ticket.view', 'order' => 22],
], ],
]); ]);
...@@ -95,6 +96,10 @@ PermissionRegistry::register('sports_activity', [ ...@@ -95,6 +96,10 @@ PermissionRegistry::register('sports_activity', [
'sa.card.print' => ['ar' => 'طباعة كروت النشاط', 'en' => 'Print SA Cards'], 'sa.card.print' => ['ar' => 'طباعة كروت النشاط', 'en' => 'Print SA Cards'],
'sa.gate.view' => ['ar' => 'عرض البوابة', 'en' => 'View Gate Access'], 'sa.gate.view' => ['ar' => 'عرض البوابة', 'en' => 'View Gate Access'],
'sa.gate.scan' => ['ar' => 'مسح البوابة', 'en' => 'Scan Gate Access'], 'sa.gate.scan' => ['ar' => 'مسح البوابة', 'en' => 'Scan Gate Access'],
'sa.group.force_enroll' => ['ar' => 'تسجيل تجاوز السعة', 'en' => 'Force Enroll (Override Capacity)'],
'sa.pool_ticket.view' => ['ar' => 'عرض تذاكر حمام السباحة', 'en' => 'View Pool Tickets'],
'sa.pool_ticket.issue' => ['ar' => 'إصدار تذكرة سباحة', 'en' => 'Issue Pool Ticket'],
'sa.pool_ticket.manage' => ['ar' => 'إدارة تذاكر السباحة', 'en' => 'Manage Pool Tickets'],
]); ]);
// ─── Event Listeners ──────────────────────────────────────────────────────── // ─── Event Listeners ────────────────────────────────────────────────────────
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_bookings ADD COLUMN organization_name VARCHAR(255) NULL AFTER booker_name;
ALTER TABLE sa_bookings ADD COLUMN organization_contact VARCHAR(100) NULL AFTER organization_name
",
'down' => "
ALTER TABLE sa_bookings DROP COLUMN organization_contact;
ALTER TABLE sa_bookings DROP COLUMN organization_name
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_facility_units ADD COLUMN expected_capacity INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'Expected number of people per booking (e.g., 5-a-side pitch = 10)' AFTER max_capacity
",
'down' => "
ALTER TABLE sa_facility_units DROP COLUMN expected_capacity
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_group_players ADD COLUMN force_enrolled TINYINT(1) NOT NULL DEFAULT 0 AFTER status
",
'down' => "
ALTER TABLE sa_group_players DROP COLUMN force_enrolled
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_bookings ADD COLUMN participant_mode ENUM('full','partial','passes') NOT NULL DEFAULT 'passes' AFTER participant_count;
ALTER TABLE sa_bookings ADD COLUMN passes_total INT UNSIGNED NOT NULL DEFAULT 0 AFTER participant_mode;
ALTER TABLE sa_bookings ADD COLUMN passes_used INT UNSIGNED NOT NULL DEFAULT 0 AFTER passes_total;
CREATE TABLE sa_booking_passes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
booking_id BIGINT UNSIGNED NOT NULL,
pass_number VARCHAR(30) NOT NULL,
status ENUM('issued','used','expired','cancelled') NOT NULL DEFAULT 'issued',
used_at DATETIME NULL,
used_by_name VARCHAR(200) NULL,
gate_employee_id BIGINT UNSIGNED NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_bp_booking (booking_id),
UNIQUE KEY uq_bp_number (pass_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
",
'down' => "
DROP TABLE IF EXISTS sa_booking_passes;
ALTER TABLE sa_bookings DROP COLUMN passes_used;
ALTER TABLE sa_bookings DROP COLUMN passes_total;
ALTER TABLE sa_bookings DROP COLUMN participant_mode
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_pool_zone_bookings ADD COLUMN access_mode ENUM('reserved','open_access') NOT NULL DEFAULT 'reserved' AFTER assignment_type;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN access_restriction ENUM('all','members_only') NULL AFTER access_mode;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN ticket_price_member DECIMAL(10,2) NULL AFTER access_restriction;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN ticket_price_nonmember DECIMAL(10,2) NULL AFTER ticket_price_member;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN max_occupancy INT UNSIGNED NULL AFTER ticket_price_nonmember;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN current_occupancy INT UNSIGNED NOT NULL DEFAULT 0 AFTER max_occupancy;
CREATE TABLE sa_pool_tickets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
facility_id INT UNSIGNED NOT NULL,
zone_booking_id BIGINT UNSIGNED NULL,
booking_date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
member_id INT UNSIGNED NULL,
player_id INT UNSIGNED NULL,
guest_name VARCHAR(200) NULL,
guest_national_id VARCHAR(14) NULL,
is_member TINYINT(1) NOT NULL DEFAULT 0,
amount_paid DECIMAL(10,2) NOT NULL DEFAULT 0.00,
payment_status ENUM('unpaid','paid','refunded') NOT NULL DEFAULT 'unpaid',
payment_request_id INT UNSIGNED NULL,
entry_time DATETIME NULL,
exit_time DATETIME NULL,
status ENUM('issued','active','completed','cancelled') NOT NULL DEFAULT 'issued',
issued_by INT UNSIGNED NULL,
branch_id INT UNSIGNED NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_pt_facility_date (facility_id, booking_date),
INDEX idx_pt_zone (zone_booking_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
",
'down' => "
DROP TABLE IF EXISTS sa_pool_tickets;
ALTER TABLE sa_pool_zone_bookings DROP COLUMN current_occupancy;
ALTER TABLE sa_pool_zone_bookings DROP COLUMN max_occupancy;
ALTER TABLE sa_pool_zone_bookings DROP COLUMN ticket_price_nonmember;
ALTER TABLE sa_pool_zone_bookings DROP COLUMN ticket_price_member;
ALTER TABLE sa_pool_zone_bookings DROP COLUMN access_restriction;
ALTER TABLE sa_pool_zone_bookings DROP COLUMN access_mode
",
];
This diff is collapsed.
This diff is collapsed.
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