Commit dd3a396a authored by Fares's avatar Fares

feat(swimming): add dedicated swimming section with dashboard and registration/assignment wizards

Adds /sa/swimming/* routes with a swimming-specific dashboard, player registration wizard
(with mandatory medical certificate upload), and group assignment wizard that sends fees
to the SA treasury. Players are stored in existing sa_players table and reuse EnrollmentService.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent ecb53aca
<?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\EnrollmentService;
use App\Modules\SportsActivity\SaConstants;
class SwimmingAssignmentController extends Controller
{
public function list(Request $request): Response
{
$db = App::getInstance()->db();
$discipline = $db->selectOne(
"SELECT id FROM sa_disciplines WHERE code = 'SWIMMING' AND is_archived = 0"
);
if (!$discipline) {
return $this->redirect('/sa')->withError('لم يتم العثور على نشاط السباحة');
}
$disciplineId = (int) $discipline['id'];
$players = $db->select("
SELECT pl.id, pl.full_name_ar, pl.date_of_birth, pl.gender, pl.phone,
pl.medical_status, pl.photo_path, pl.player_type, pl.created_at,
(SELECT MAX(doc.created_at) FROM sa_player_documents doc
WHERE doc.player_id = pl.id AND doc.document_type = 'medical_cert') as medical_upload_date
FROM sa_players pl
WHERE pl.is_archived = 0
AND EXISTS (
SELECT 1 FROM sa_player_documents doc
WHERE doc.player_id = pl.id AND doc.document_type = 'medical_cert'
)
AND NOT EXISTS (
SELECT 1 FROM sa_group_players gp
INNER JOIN sa_groups g ON g.id = gp.group_id
INNER JOIN sa_programs p ON p.id = g.program_id
WHERE p.discipline_id = ? AND gp.player_id = pl.id AND gp.status IN ('active', 'pending_payment')
)
ORDER BY pl.created_at DESC
", [$disciplineId]);
return $this->view('SportsActivity.Views.swimming.assign', [
'players' => $players,
'player' => null,
'groups' => [],
'mode' => 'list',
'disciplineId' => $disciplineId,
]);
}
public function index(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$discipline = $db->selectOne(
"SELECT id FROM sa_disciplines WHERE code = 'SWIMMING' AND is_archived = 0"
);
if (!$discipline) {
return $this->redirect('/sa')->withError('لم يتم العثور على نشاط السباحة');
}
$disciplineId = (int) $discipline['id'];
$isRegistrationId = (bool) $db->selectOne(
"SELECT 1 FROM sa_registrations WHERE id = ?", [(int) $id]
);
if ($isRegistrationId) {
$registration = $db->selectOne(
"SELECT r.*, p.id as player_id, p.full_name_ar, p.date_of_birth, p.gender,
p.phone, p.photo_path, p.player_type, p.medical_status
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ?",
[(int) $id]
);
if (!$registration) {
return $this->redirect('/sa/swimming/assign')->withError('التسجيل غير موجود');
}
$player = $registration;
$playerId = (int) $registration['player_id'];
} else {
$player = $db->selectOne(
"SELECT *, id as player_id FROM sa_players WHERE id = ? AND is_archived = 0",
[(int) $id]
);
if (!$player) {
return $this->redirect('/sa/swimming/assign')->withError('اللاعب غير موجود');
}
$playerId = (int) $id;
}
$hasMedical = (bool) $db->selectOne(
"SELECT 1 FROM sa_player_documents WHERE player_id = ? AND document_type = 'medical_cert'",
[$playerId]
);
if (!$hasMedical) {
return $this->redirect('/sa/swimming/register')->withError('يجب رفع الشهادة الطبية أولاً');
}
$groups = $db->select("
SELECT g.id, g.name_ar, g.current_count, g.schedule_summary, g.min_age, g.max_age,
p.name_ar as program_name, p.max_capacity, p.monthly_fee_member, p.monthly_fee_nonmember,
p.age_from, p.age_to,
c.full_name_ar as coach_name
FROM sa_groups g
INNER JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id
WHERE p.discipline_id = ? AND g.status = 'active' AND g.is_archived = 0
ORDER BY p.name_ar ASC, g.name_ar ASC
", [$disciplineId]);
$playerAge = null;
if (!empty($player['date_of_birth'])) {
$playerAge = (int) date_diff(
date_create($player['date_of_birth']),
date_create()
)->y;
}
return $this->view('SportsActivity.Views.swimming.assign', [
'players' => [],
'player' => $player,
'playerId' => $playerId,
'playerAge' => $playerAge,
'groups' => $groups,
'mode' => 'assign',
'disciplineId' => $disciplineId,
]);
}
public function assign(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$groupId = (int) $request->post('group_id', 0);
if ($groupId <= 0) {
return $this->json(['success' => false, 'error' => 'اختر مجموعة']);
}
$isRegistrationId = (bool) $db->selectOne(
"SELECT 1 FROM sa_registrations WHERE id = ?", [(int) $id]
);
if ($isRegistrationId) {
$registration = $db->selectOne(
"SELECT player_id FROM sa_registrations WHERE id = ?", [(int) $id]
);
$playerId = (int) $registration['player_id'];
} else {
$playerId = (int) $id;
}
$result = EnrollmentService::enroll($groupId, $playerId);
if (!$result['success']) {
return $this->json($result);
}
if ($isRegistrationId) {
$db->update('sa_registrations', [
'group_id' => $groupId,
'status' => SaConstants::REG_PENDING_PAYMENT,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
}
return $this->json([
'success' => true,
'enrollment_id' => $result['enrollment_id'],
'request_id' => $result['request_id'],
'request_number' => $result['request_number'],
'fee' => $result['fee'],
'redirect' => '/sa/swimming/assign/' . $id . '?done=1',
]);
}
}
<?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;
class SwimmingDashboardController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
$monthStart = date('Y-m-01');
$monthEnd = date('Y-m-t');
$discipline = $db->selectOne(
"SELECT id FROM sa_disciplines WHERE code = 'SWIMMING' AND is_archived = 0"
);
if (!$discipline) {
return $this->redirect('/sa')->withError('لم يتم العثور على نشاط السباحة');
}
$disciplineId = (int) $discipline['id'];
$counts = $db->selectOne("
SELECT
(SELECT COUNT(DISTINCT gp.player_id) FROM sa_group_players gp
INNER JOIN sa_groups g ON g.id = gp.group_id
INNER JOIN sa_programs p ON p.id = g.program_id
WHERE p.discipline_id = ? AND gp.status = 'active') as active_players,
(SELECT COUNT(*) FROM sa_groups g
INNER JOIN sa_programs p ON p.id = g.program_id
WHERE p.discipline_id = ? AND g.status = 'active' AND g.is_archived = 0) as active_groups,
(SELECT COUNT(DISTINCT gp.player_id) FROM sa_group_players gp
INNER JOIN sa_groups g ON g.id = gp.group_id
INNER JOIN sa_programs p ON p.id = g.program_id
WHERE p.discipline_id = ? AND gp.status = 'pending_payment') as pending_payment,
(SELECT COALESCE(SUM(pr.amount), 0) FROM payment_requests pr
INNER JOIN sa_group_players gp ON gp.payment_request_id = pr.id
INNER JOIN sa_groups g ON g.id = gp.group_id
INNER JOIN sa_programs p ON p.id = g.program_id
WHERE p.discipline_id = ? AND pr.status = 'completed'
AND pr.completed_at BETWEEN ? AND ?) as revenue_this_month
", [$disciplineId, $disciplineId, $disciplineId, $disciplineId, $monthStart . ' 00:00:00', $monthEnd . ' 23:59:59']);
$groups = $db->select("
SELECT g.id, g.name_ar, g.current_count, g.status, g.schedule_summary,
p.name_ar as program_name, p.max_capacity, p.monthly_fee_member, p.monthly_fee_nonmember,
c.full_name_ar as coach_name,
(SELECT COUNT(*) FROM sa_group_players gp WHERE gp.group_id = g.id AND gp.status = 'active') as active_count,
(SELECT COUNT(*) FROM sa_group_players gp WHERE gp.group_id = g.id AND gp.status = 'pending_payment') as pending_count
FROM sa_groups g
INNER JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id
WHERE p.discipline_id = ? AND g.is_archived = 0
ORDER BY g.status ASC, g.name_ar ASC
", [$disciplineId]);
$recentRegistrations = $db->select("
SELECT r.id, r.registration_number, r.status, r.created_at,
pl.full_name_ar, pl.photo_path, pl.medical_status,
g.name_ar as group_name
FROM sa_registrations r
INNER JOIN sa_players pl ON pl.id = r.player_id
LEFT JOIN sa_groups g ON g.id = r.group_id
LEFT JOIN sa_programs p ON p.id = g.program_id
WHERE r.status != 'cancelled'
AND (p.discipline_id = ? OR EXISTS (
SELECT 1 FROM sa_group_players gp2
INNER JOIN sa_groups g2 ON g2.id = gp2.group_id
INNER JOIN sa_programs p3 ON p3.id = g2.program_id
WHERE p3.discipline_id = ? AND gp2.player_id = r.player_id
))
ORDER BY r.created_at DESC
LIMIT 10
", [$disciplineId, $disciplineId]);
$unassignedPlayers = $db->select("
SELECT pl.id, pl.full_name_ar, pl.date_of_birth, pl.medical_status, pl.created_at
FROM sa_players pl
WHERE pl.is_archived = 0
AND pl.medical_status IN ('fit', 'conditional', 'pending')
AND NOT EXISTS (
SELECT 1 FROM sa_group_players gp
INNER JOIN sa_groups g ON g.id = gp.group_id
INNER JOIN sa_programs p ON p.id = g.program_id
WHERE p.discipline_id = ? AND gp.player_id = pl.id AND gp.status IN ('active', 'pending_payment')
)
AND EXISTS (
SELECT 1 FROM sa_player_documents doc
WHERE doc.player_id = pl.id AND doc.document_type = 'medical_cert'
)
ORDER BY pl.created_at DESC
LIMIT 10
", [$disciplineId]);
$programs = $db->select("
SELECT p.id, p.name_ar, p.monthly_fee_member, p.monthly_fee_nonmember, p.max_capacity,
COUNT(DISTINCT g.id) as group_count,
COALESCE(SUM(g.current_count), 0) as total_enrolled
FROM sa_programs p
LEFT JOIN sa_groups g ON g.program_id = p.id AND g.is_archived = 0 AND g.status = 'active'
WHERE p.discipline_id = ? AND p.is_archived = 0
GROUP BY p.id, p.name_ar, p.monthly_fee_member, p.monthly_fee_nonmember, p.max_capacity
ORDER BY p.name_ar
", [$disciplineId]);
return $this->view('SportsActivity.Views.swimming.dashboard', [
'counts' => $counts,
'groups' => $groups,
'recentRegistrations' => $recentRegistrations,
'unassignedPlayers' => $unassignedPlayers,
'programs' => $programs,
'disciplineId' => $disciplineId,
]);
}
}
<?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\Members\Services\NationalIdParser;
use App\Modules\Members\Services\MembershipValidationService;
use App\Modules\SportsActivity\Services\RegistrationWizardService;
use App\Shared\Services\PhotoUploadService;
class SwimmingRegistrationController extends Controller
{
public function index(Request $request): Response
{
return $this->view('SportsActivity.Views.swimming.register', [
'registration' => null,
'step' => 'lookup',
]);
}
public function lookup(Request $request): Response
{
$nationalId = trim((string) $request->post('national_id', ''));
$membershipNumber = trim((string) $request->post('membership_number', ''));
$fullNameAr = trim((string) $request->post('full_name_ar', ''));
$fullNameEn = trim((string) $request->post('full_name_en', ''));
$phone = trim((string) $request->post('phone', ''));
$guardianName = trim((string) $request->post('guardian_name', ''));
$guardianPhone = trim((string) $request->post('guardian_phone', ''));
$guardianNationalId = trim((string) $request->post('guardian_national_id', ''));
$guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$nidParsed = null;
$playerType = 'non_member';
$memberId = 0;
$membership = null;
if ($membershipNumber !== '') {
$membership = MembershipValidationService::checkByMembershipNumber($membershipNumber);
} elseif ($nationalId !== '' && strlen($nationalId) === 14) {
$nidParsed = NationalIdParser::parse($nationalId);
$membership = MembershipValidationService::checkByNationalId($nationalId);
}
if ($membership) {
$playerType = $membership['effective_type'];
if ($membership['found'] && $membership['member']) {
$memberId = (int) $membership['member_id'];
$m = $membership['member'];
$fullNameAr = $fullNameAr ?: ($m['full_name_ar'] ?? '');
$fullNameEn = $fullNameEn ?: ($m['full_name_en'] ?? '');
$phone = $phone ?: ($m['phone_mobile'] ?? '');
$nationalId = $nationalId ?: ($m['national_id'] ?? '');
if ($nationalId !== '' && strlen($nationalId) === 14 && !$nidParsed) {
$nidParsed = NationalIdParser::parse($nationalId);
}
}
}
if ($fullNameAr === '' && $nidParsed === null) {
return $this->json(['success' => false, 'error' => 'أدخل رقم العضوية أو الرقم القومي أو الاسم']);
}
$result = RegistrationWizardService::startRegistration([
'national_id' => $nationalId,
'player_type' => $playerType,
'member_id' => $memberId,
'full_name_ar' => $fullNameAr,
'full_name_en' => $fullNameEn,
'date_of_birth' => $nidParsed['dob'] ?? null,
'gender' => $nidParsed['gender'] ?? null,
'phone' => $phone,
'guardian_name' => $guardianName,
'guardian_phone' => $guardianPhone,
'guardian_national_id' => $guardianNationalId,
'guardian_relationship' => $guardianRelationship,
]);
if (!$result['success']) {
return $this->json($result);
}
return $this->json([
'success' => true,
'registration_id' => $result['registration_id'],
'player_id' => $result['player_id'],
'nid_parsed' => $nidParsed,
'effective_type' => $playerType,
'redirect' => '/sa/swimming/register/' . $result['registration_id'],
]);
}
public function step(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT r.*, p.full_name_ar, p.full_name_en, p.national_id as player_nid,
p.date_of_birth, p.gender, p.phone, p.photo_path, p.player_type as p_type,
p.guardian_name, p.guardian_phone, p.guardian_national_id, p.guardian_relationship,
p.medical_status, p.id as player_id
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ?",
[(int) $id]
);
if (!$registration) {
return $this->redirect('/sa/swimming/register')->withError('التسجيل غير موجود');
}
$hasMedical = (bool) $db->selectOne(
"SELECT 1 FROM sa_player_documents WHERE player_id = ? AND document_type = 'medical_cert'",
[(int) $registration['player_id']]
);
$currentStep = 'personal';
if ($registration['status'] !== 'in_progress') {
$currentStep = 'complete';
} elseif ($hasMedical && $registration['photo_captured']) {
$currentStep = 'complete';
} elseif ($hasMedical) {
$currentStep = 'photo';
} else {
$currentStep = 'medical';
}
return $this->view('SportsActivity.Views.swimming.register', [
'registration' => $registration,
'step' => $currentStep,
'hasMedical' => $hasMedical,
]);
}
public function saveMedical(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT r.*, p.id as player_id FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ? AND r.status = 'in_progress'",
[(int) $id]
);
if (!$registration) {
return $this->json(['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل']);
}
$file = $_FILES['medical_file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
return $this->json(['success' => false, 'error' => 'الشهادة الطبية مطلوبة']);
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['pdf', 'png', 'jpg', 'jpeg'])) {
return $this->json(['success' => false, 'error' => 'صيغة الملف غير مدعومة (PDF, PNG, JPG فقط)']);
}
$maxSize = 10 * 1024 * 1024;
if ($file['size'] > $maxSize) {
return $this->json(['success' => false, 'error' => 'حجم الملف يتجاوز 10 ميجابايت']);
}
$examDate = trim((string) $request->post('exam_date', ''));
$expiryDate = trim((string) $request->post('expiry_date', ''));
if ($examDate === '' || $expiryDate === '') {
return $this->json(['success' => false, 'error' => 'تاريخ الفحص وتاريخ الانتهاء مطلوبان']);
}
$newName = uniqid('med_') . '.' . $ext;
$uploadDir = dirname(__DIR__, 5) . '/public/uploads/sa_documents/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
move_uploaded_file($file['tmp_name'], $uploadDir . $newName);
$filePath = 'uploads/sa_documents/' . $newName;
$playerId = (int) $registration['player_id'];
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
$docId = $db->insert('sa_player_documents', [
'player_id' => $playerId,
'document_type' => 'medical_cert',
'file_path' => $filePath,
'file_name' => $file['name'],
'exam_date' => $examDate,
'expiry_date' => $expiryDate,
'approval_status' => 'pending',
'created_by' => $employeeId,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$db->update('sa_players', [
'medical_status' => 'pending',
'medical_expiry_date' => $expiryDate,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$playerId]);
return $this->json([
'success' => true,
'document_id' => $docId,
'redirect' => '/sa/swimming/register/' . $id,
]);
}
public function savePhoto(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT r.*, p.id as player_id FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ? AND r.status = 'in_progress'",
[(int) $id]
);
if (!$registration) {
return $this->json(['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل']);
}
$base64 = trim((string) $request->post('photo_base64', ''));
if ($base64 !== '') {
$file = $this->base64ToTempFile($base64);
if (!$file) {
return $this->json(['success' => false, 'error' => 'فشل معالجة الصورة']);
}
} else {
$file = $_FILES['photo'] ?? [];
if (empty($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) {
return $this->json(['success' => false, 'error' => 'لم يتم رفع صورة']);
}
}
$playerId = (int) $registration['player_id'];
$result = PhotoUploadService::upload($file, 'sa_players', $playerId);
if (!$result) {
return $this->json(['success' => false, 'error' => 'فشل رفع الصورة']);
}
$db->update('sa_players', [
'photo_path' => $result['path'],
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$playerId]);
$db->update('sa_registrations', [
'photo_captured' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->json([
'success' => true,
'photo_path' => $result['path'],
'redirect' => '/sa/swimming/assign/' . $id,
]);
}
private function base64ToTempFile(string $base64): ?array
{
if (str_contains($base64, ',')) {
$base64 = explode(',', $base64, 2)[1];
}
$decoded = base64_decode($base64, true);
if ($decoded === false || strlen($decoded) < 100) {
return null;
}
$tmp = tempnam(sys_get_temp_dir(), 'photo_');
file_put_contents($tmp, $decoded);
return [
'tmp_name' => $tmp,
'error' => UPLOAD_ERR_OK,
'size' => strlen($decoded),
'name' => 'capture.jpg',
'type' => 'image/jpeg',
];
}
}
...@@ -292,6 +292,17 @@ return [ ...@@ -292,6 +292,17 @@ return [
['POST', '/api/sa/subscriptions/pause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@pause', ['auth', 'csrf'], 'sa.subscription.generate'], ['POST', '/api/sa/subscriptions/pause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@pause', ['auth', 'csrf'], 'sa.subscription.generate'],
['POST', '/api/sa/subscriptions/unpause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@unpause', ['auth', 'csrf'], 'sa.subscription.generate'], ['POST', '/api/sa/subscriptions/unpause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@unpause', ['auth', 'csrf'], 'sa.subscription.generate'],
// ─── Swimming Section ───────────────────────────────────────────────────────
['GET', '/sa/swimming', 'SportsActivity\Controllers\Swimming\SwimmingDashboardController@index', ['auth'], 'sa.swimming.dashboard'],
['GET', '/sa/swimming/register', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@index', ['auth'], 'sa.swimming.register'],
['POST', '/sa/swimming/register/lookup', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@lookup', ['auth', 'csrf'], 'sa.swimming.register'],
['GET', '/sa/swimming/register/{id:\d+}', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@step', ['auth'], 'sa.swimming.register'],
['POST', '/sa/swimming/register/{id:\d+}/medical', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@saveMedical', ['auth', 'csrf'], 'sa.swimming.register'],
['POST', '/sa/swimming/register/{id:\d+}/photo', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@savePhoto', ['auth', 'csrf'], 'sa.swimming.register'],
['GET', '/sa/swimming/assign', 'SportsActivity\Controllers\Swimming\SwimmingAssignmentController@list', ['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'],
// ─── 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 $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعيين لاعب في مجموعة سباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming" 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 if ($mode === 'list'): ?>
<!-- Unassigned Players List -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:700;font-size:15px;">اللاعبين المتاحين للتعيين (<?= count($players) ?>)</span>
<a href="/sa/swimming/register" class="btn btn-primary" style="font-size:12px;"><i data-lucide="user-plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>تسجيل جديد</a>
</div>
<?php if (empty($players)): ?>
<div style="padding:40px;text-align:center;">
<div style="width:64px;height:64px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
<i data-lucide="users" style="width:28px;height:28px;color:#9CA3AF;"></i>
</div>
<p style="font-size:14px;color:#6B7280;margin-bottom:12px;">لا يوجد لاعبين بانتظار التعيين في مجموعة سباحة</p>
<a href="/sa/swimming/register" class="btn btn-primary">تسجيل لاعب جديد</a>
</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px 14px;text-align:right;font-weight:600;">اللاعب</th>
<th style="padding:10px 14px;text-align:center;font-weight:600;">العمر</th>
<th style="padding:10px 14px;text-align:center;font-weight:600;">النوع</th>
<th style="padding:10px 14px;text-align:center;font-weight:600;">الحالة الطبية</th>
<th style="padding:10px 14px;text-align:center;font-weight:600;">تاريخ التسجيل</th>
<th style="padding:10px 14px;text-align:center;font-weight:600;">إجراء</th>
</tr>
</thead>
<tbody>
<?php foreach ($players as $pl):
$age = $pl['date_of_birth'] ? (int) date_diff(date_create($pl['date_of_birth']), date_create())->y : null;
$medicalLabels = ['fit' => ['لائق','#059669','#ECFDF5'], 'pending' => ['بانتظار','#D97706','#FEF3C7'], 'conditional' => ['مشروط','#2563EB','#EFF6FF']];
$ml = $medicalLabels[$pl['medical_status']] ?? [$pl['medical_status'],'#6B7280','#F3F4F6'];
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 14px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:32px;height:32px;border-radius:50%;background:#F3F4F6;overflow:hidden;flex-shrink:0;">
<?php if ($pl['photo_path']): ?>
<img src="/<?= e($pl['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;" alt="">
<?php else: ?>
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;"><i data-lucide="user" style="width:14px;height:14px;color:#9CA3AF;"></i></div>
<?php endif; ?>
</div>
<span style="font-weight:600;"><?= e($pl['full_name_ar']) ?></span>
</div>
</td>
<td style="padding:10px 14px;text-align:center;"><?= $age !== null ? $age . ' سنة' : '—' ?></td>
<td style="padding:10px 14px;text-align:center;">
<span style="font-size:11px;padding:2px 8px;border-radius:4px;background:<?= $pl['player_type'] === 'member' ? '#ECFDF5' : '#FEF3C7' ?>;color:<?= $pl['player_type'] === 'member' ? '#059669' : '#D97706' ?>;">
<?= $pl['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?>
</span>
</td>
<td style="padding:10px 14px;text-align:center;">
<span style="font-size:11px;padding:2px 8px;border-radius:4px;background:<?= $ml[2] ?>;color:<?= $ml[1] ?>;font-weight:600;"><?= $ml[0] ?></span>
</td>
<td style="padding:10px 14px;text-align:center;color:#6B7280;font-size:12px;direction:ltr;"><?= e(substr($pl['created_at'], 0, 10)) ?></td>
<td style="padding:10px 14px;text-align:center;">
<a href="/sa/swimming/assign/<?= (int) $pl['id'] ?>" class="btn btn-primary" style="font-size:11px;padding:4px 12px;">تعيين</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($mode === 'assign'): ?>
<!-- Assignment View for Specific Player -->
<?php
$done = isset($_GET['done']) && $_GET['done'] === '1';
if ($done): ?>
<!-- Success State -->
<div class="card" style="text-align:center;padding:40px;">
<div style="width:64px;height:64px;border-radius:50%;background:#ECFDF5;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
<i data-lucide="check-circle" style="width:32px;height:32px;color:#059669;"></i>
</div>
<h3 style="font-size:18px;font-weight:700;color:#1A1A2E;margin-bottom:8px;">تم تعيين اللاعب بنجاح</h3>
<p style="font-size:13px;color:#6B7280;margin-bottom:4px;">تم إرسال طلب الدفع إلى خزينة الأنشطة الرياضية</p>
<p style="font-size:12px;color:#9CA3AF;margin-bottom:20px;">سيتم تفعيل اللاعب تلقائياً بمجرد تأكيد الدفع</p>
<div style="display:flex;gap:10px;justify-content:center;">
<a href="/sa/swimming" class="btn btn-primary">لوحة التحكم</a>
<a href="/sa/swimming/register" class="btn btn-outline">تسجيل لاعب آخر</a>
<a href="/sa/swimming/assign" class="btn btn-outline">تعيين آخر</a>
</div>
</div>
<?php else: ?>
<!-- Player Info -->
<div class="card" style="margin-bottom:16px;">
<div style="padding:14px 16px;display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;border-radius:8px;overflow:hidden;background:#F3F4F6;flex-shrink:0;">
<?php if (!empty($player['photo_path'])): ?>
<img src="/<?= e($player['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;" alt="">
<?php else: ?>
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9CA3AF;"><i data-lucide="user" style="width:20px;height:20px;"></i></div>
<?php endif; ?>
</div>
<div style="flex:1;">
<div style="font-size:15px;font-weight:700;color:#1A1A2E;"><?= e($player['full_name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">
<?php if ($playerAge !== null): ?><?= $playerAge ?> سنة<?php endif; ?>
<?= $player['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?>
<?= $player['medical_status'] === 'fit' ? 'لائق طبياً' : ($player['medical_status'] === 'pending' ? 'بانتظار الموافقة' : $player['medical_status']) ?>
</div>
</div>
</div>
</div>
<!-- Groups Selection -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:14px;"><i data-lucide="users" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i>اختر المجموعة</span>
</div>
<?php if (empty($groups)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد مجموعات سباحة نشطة</div>
<?php else: ?>
<div style="padding:16px;">
<?php
$groupedByProgram = [];
foreach ($groups as $g) {
$groupedByProgram[$g['program_name']][] = $g;
}
?>
<?php foreach ($groupedByProgram as $progName => $progGroups): ?>
<div style="margin-bottom:20px;">
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid #E5E7EB;"><?= e($progName) ?></div>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:10px;">
<?php foreach ($progGroups as $g):
$capacity = (int) ($g['max_capacity'] ?? 0);
$current = (int) ($g['current_count'] ?? 0);
$isFull = $capacity > 0 && $current >= $capacity;
$pct = $capacity > 0 ? round(($current / $capacity) * 100) : 0;
$barColor = $isFull ? '#DC2626' : ($pct >= 70 ? '#F59E0B' : '#059669');
$ageOk = true;
if ($playerAge !== null) {
if (!empty($g['min_age']) && $playerAge < (int) $g['min_age']) $ageOk = false;
if (!empty($g['max_age']) && $playerAge > (int) $g['max_age']) $ageOk = false;
if (!empty($g['age_from']) && $playerAge < (int) $g['age_from']) $ageOk = false;
if (!empty($g['age_to']) && $playerAge > (int) $g['age_to']) $ageOk = false;
}
$fee = $player['player_type'] === 'member'
? (float) $g['monthly_fee_member']
: (float) $g['monthly_fee_nonmember'];
?>
<div class="group-card" data-group-id="<?= (int) $g['id'] ?>" data-fee="<?= $fee ?>"
style="border:2px solid #E5E7EB;border-radius:8px;padding:12px;cursor:<?= ($isFull || !$ageOk) ? 'not-allowed' : 'pointer' ?>;opacity:<?= ($isFull || !$ageOk) ? '0.5' : '1' ?>;transition:border-color 0.2s;"
<?php if (!$isFull && $ageOk): ?>onclick="selectGroup(<?= (int) $g['id'] ?>, <?= $fee ?>)"<?php endif; ?>>
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px;">
<span style="font-size:13px;font-weight:700;color:#1A1A2E;"><?= e($g['name_ar']) ?></span>
<?php if ($isFull): ?>
<span style="font-size:10px;background:#FEF2F2;color:#DC2626;padding:2px 6px;border-radius:4px;">ممتلئة</span>
<?php elseif (!$ageOk): ?>
<span style="font-size:10px;background:#FEF2F2;color:#DC2626;padding:2px 6px;border-radius:4px;">عمر غير مناسب</span>
<?php endif; ?>
</div>
<?php if ($g['coach_name']): ?>
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;"><i data-lucide="user" style="width:11px;height:11px;vertical-align:middle;margin-left:4px;"></i><?= e($g['coach_name']) ?></div>
<?php endif; ?>
<?php if ($g['schedule_summary']): ?>
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;"><i data-lucide="clock" style="width:11px;height:11px;vertical-align:middle;margin-left:4px;"></i><?= e($g['schedule_summary']) ?></div>
<?php endif; ?>
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
<div style="flex:1;height:5px;background:#E5E7EB;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $pct ?>%;background:<?= $barColor ?>;border-radius:3px;"></div>
</div>
<span style="font-size:10px;color:#6B7280;"><?= $current ?>/<?= $capacity ?></span>
</div>
<div style="font-size:12px;font-weight:600;color:#059669;"><?= number_format($fee) ?> ج.م / شهر</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Confirm Assignment -->
<div id="assignConfirm" style="display:none;padding:16px;border-top:1px solid #E5E7EB;background:#F9FAFB;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<div>
<span style="font-size:13px;font-weight:700;color:#1A1A2E;">المجموعة المختارة: </span>
<span id="selectedGroupName" style="font-size:13px;color:#2563EB;font-weight:600;"></span>
</div>
<div style="font-size:14px;font-weight:700;color:#059669;">
<span id="selectedFee">0</span> ج.م
</div>
</div>
<button type="button" id="confirmAssignBtn" class="btn btn-primary" style="width:100%;padding:10px;font-size:14px;" onclick="confirmAssignment()">
<i data-lucide="check" style="width:15px;height:15px;vertical-align:middle;margin-left:6px;"></i>تأكيد التعيين وإرسال للخزينة
</button>
<div id="assignError" style="display:none;margin-top:10px;padding:8px 12px;background:#FEF2F2;color:#DC2626;border-radius:6px;font-size:12px;"></div>
</div>
<?php endif; ?>
</div>
<script>
let selectedGroupId = null;
function selectGroup(groupId, fee) {
selectedGroupId = groupId;
document.querySelectorAll('.group-card').forEach(c => c.style.borderColor = '#E5E7EB');
const card = document.querySelector('[data-group-id="' + groupId + '"]');
if (card) card.style.borderColor = '#2563EB';
document.getElementById('selectedGroupName').textContent = card.querySelector('span').textContent;
document.getElementById('selectedFee').textContent = fee.toLocaleString();
document.getElementById('assignConfirm').style.display = 'block';
}
function confirmAssignment() {
if (!selectedGroupId) return;
const btn = document.getElementById('confirmAssignBtn');
const errDiv = document.getElementById('assignError');
errDiv.style.display = 'none';
btn.disabled = true;
btn.textContent = 'جاري التعيين...';
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
fetch('/sa/swimming/assign/<?= (int) ($playerId ?? 0) ?>/group', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_id: selectedGroupId, _csrf_token: csrfToken })
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect || window.location.href + '?done=1';
} else {
errDiv.textContent = data.error || 'حدث خطأ';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'تأكيد التعيين وإرسال للخزينة';
}
})
.catch(() => {
errDiv.textContent = 'فشل الاتصال بالسيرفر';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'تأكيد التعيين وإرسال للخزينة';
});
}
</script>
<?php endif; ?>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تم التسجيل بنجاح<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="max-width:600px;margin:40px auto;text-align:center;padding:40px;">
<div style="width:80px;height:80px;border-radius:50%;background:#ECFDF5;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
<i data-lucide="check-circle-2" style="width:40px;height:40px;color:#059669;"></i>
</div>
<h2 style="font-size:20px;font-weight:700;color:#1A1A2E;margin-bottom:8px;">تم تسجيل وتعيين اللاعب بنجاح</h2>
<p style="font-size:14px;color:#6B7280;margin-bottom:24px;">تم إرسال طلب الدفع إلى خزينة الأنشطة الرياضية. سيتم تفعيل اللاعب في المجموعة بمجرد تأكيد الدفع.</p>
<?php if (isset($player) && $player): ?>
<div style="background:#F9FAFB;border-radius:8px;padding:16px;margin-bottom:24px;text-align:right;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:13px;">
<div><span style="color:#6B7280;">اللاعب:</span> <strong><?= e($player['full_name_ar'] ?? '') ?></strong></div>
<div><span style="color:#6B7280;">المجموعة:</span> <strong><?= e($group['name_ar'] ?? '') ?></strong></div>
<div><span style="color:#6B7280;">البرنامج:</span> <strong><?= e($program['name_ar'] ?? '') ?></strong></div>
<div><span style="color:#6B7280;">المبلغ:</span> <strong style="color:#059669;"><?= isset($fee) ? number_format((float) $fee) : '—' ?> ج.م</strong></div>
</div>
</div>
<?php endif; ?>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
<a href="/sa/swimming" class="btn btn-primary"><i data-lucide="layout-dashboard" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>لوحة التحكم</a>
<a href="/sa/swimming/register" class="btn btn-outline"><i data-lucide="user-plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>تسجيل لاعب آخر</a>
<a href="/sa/players" class="btn btn-outline"><i data-lucide="users" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>عرض اللاعبين</a>
</div>
</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/register" class="btn btn-primary"><i data-lucide="user-plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تسجيل لاعب جديد</a>
<a href="/sa/swimming/assign" class="btn btn-outline"><i data-lucide="users" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعيين في مجموعة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:12px;margin-bottom:20px;">
<div class="card" style="padding:16px;border-right:3px solid #06B6D4;">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) ($counts['active_players'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">لاعب نشط</div>
</div>
<div class="card" style="padding:16px;border-right:3px solid #8B5CF6;">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) ($counts['active_groups'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">مجموعة نشطة</div>
</div>
<div class="card" style="padding:16px;border-right:3px solid #F59E0B;">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) ($counts['pending_payment'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">بانتظار الدفع</div>
</div>
<div class="card" style="padding:16px;border-right:3px solid #059669;">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= number_format((float) ($counts['revenue_this_month'] ?? 0)) ?> ج.م</div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">إيراد الشهر</div>
</div>
</div>
<!-- Two Column: Groups + Unassigned Players -->
<div style="display:grid;grid-template-columns:2fr 1fr;gap:15px;margin-bottom:20px;">
<!-- Groups Table -->
<div class="card">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:700;font-size:14px;">المجموعات (<?= count($groups) ?>)</span>
<a href="/sa/groups" style="font-size:12px;color:#2563EB;text-decoration:none;">عرض الكل</a>
</div>
<?php if (empty($groups)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد مجموعات سباحة</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:8px 12px;text-align:right;font-weight:600;">المجموعة</th>
<th style="padding:8px 12px;text-align:right;font-weight:600;">البرنامج</th>
<th style="padding:8px 12px;text-align:right;font-weight:600;">المدرب</th>
<th style="padding:8px 12px;text-align:center;font-weight:600;">السعة</th>
<th style="padding:8px 12px;text-align:center;font-weight:600;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group):
$capacity = (int) ($group['max_capacity'] ?? 0);
$current = (int) ($group['active_count'] ?? 0);
$pct = $capacity > 0 ? round(($current / $capacity) * 100) : 0;
$barColor = $pct >= 90 ? '#DC2626' : ($pct >= 70 ? '#F59E0B' : '#059669');
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 12px;font-weight:600;"><?= e($group['name_ar']) ?></td>
<td style="padding:8px 12px;"><?= e($group['program_name']) ?></td>
<td style="padding:8px 12px;"><?= e($group['coach_name'] ?? '—') ?></td>
<td style="padding:8px 12px;text-align:center;">
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
<div style="width:60px;height:6px;background:#E5E7EB;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $pct ?>%;background:<?= $barColor ?>;border-radius:3px;"></div>
</div>
<span style="font-size:11px;color:#6B7280;"><?= $current ?>/<?= $capacity ?></span>
</div>
</td>
<td style="padding:8px 12px;text-align:center;">
<?php if ($group['status'] === 'active'): ?>
<span style="background:#ECFDF5;color:#059669;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">نشطة</span>
<?php else: ?>
<span style="background:#FEF2F2;color:#DC2626;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;"><?= e($group['status']) ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Unassigned Players -->
<div class="card">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:700;font-size:14px;">بانتظار التعيين (<?= count($unassignedPlayers) ?>)</span>
<a href="/sa/swimming/assign" style="font-size:12px;color:#2563EB;text-decoration:none;">عرض الكل</a>
</div>
<?php if (empty($unassignedPlayers)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا يوجد لاعبين بانتظار التعيين</div>
<?php else: ?>
<div style="max-height:350px;overflow-y:auto;">
<?php foreach ($unassignedPlayers as $pl): ?>
<a href="/sa/swimming/assign/<?= (int) $pl['id'] ?>" style="display:flex;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid #F3F4F6;text-decoration:none;color:inherit;">
<div style="width:32px;height:32px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="user" style="width:16px;height:16px;color:#9CA3AF;"></i>
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><?= e($pl['full_name_ar']) ?></div>
<div style="font-size:11px;color:#6B7280;">
<?php if ($pl['date_of_birth']): ?>
<?= (int) date_diff(date_create($pl['date_of_birth']), date_create())->y ?> سنة
<?php endif; ?>
<?= e($pl['medical_status'] === 'fit' ? 'لائق' : ($pl['medical_status'] === 'pending' ? 'بانتظار' : $pl['medical_status'])) ?>
</div>
</div>
<i data-lucide="chevron-left" style="width:14px;height:14px;color:#9CA3AF;flex-shrink:0;"></i>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Programs Overview -->
<?php if (!empty($programs)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:14px;">البرامج</span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));gap:12px;padding:16px;">
<?php foreach ($programs as $prog): ?>
<div style="border:1px solid #E5E7EB;border-radius:8px;padding:12px;">
<div style="font-size:14px;font-weight:700;color:#1A1A2E;margin-bottom:8px;"><?= e($prog['name_ar']) ?></div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:#6B7280;margin-bottom:4px;">
<span>مجموعات: <?= (int) $prog['group_count'] ?></span>
<span>مسجلين: <?= (int) $prog['total_enrolled'] ?></span>
</div>
<div style="display:flex;justify-content:space-between;font-size:11px;margin-top:6px;padding-top:6px;border-top:1px solid #F3F4F6;">
<span style="color:#059669;">عضو: <?= number_format((float) $prog['monthly_fee_member']) ?> ج.م</span>
<span style="color:#D97706;">غير عضو: <?= number_format((float) $prog['monthly_fee_nonmember']) ?> ج.م</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Recent Activity -->
<?php if (!empty($recentRegistrations)): ?>
<div class="card">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:14px;">آخر التسجيلات</span>
</div>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:8px 12px;text-align:right;">اللاعب</th>
<th style="padding:8px 12px;text-align:right;">المجموعة</th>
<th style="padding:8px 12px;text-align:center;">الحالة</th>
<th style="padding:8px 12px;text-align:right;">التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentRegistrations as $reg): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 12px;font-weight:600;"><?= e($reg['full_name_ar']) ?></td>
<td style="padding:8px 12px;"><?= e($reg['group_name'] ?? '—') ?></td>
<td style="padding:8px 12px;text-align:center;">
<?php
$statusColors = [
'completed' => ['#ECFDF5','#059669','مكتمل'],
'pending_payment' => ['#FEF3C7','#D97706','بانتظار الدفع'],
'in_progress' => ['#EFF6FF','#2563EB','قيد التسجيل'],
];
$s = $statusColors[$reg['status']] ?? ['#F3F4F6','#6B7280',$reg['status']];
?>
<span style="background:<?= $s[0] ?>;color:<?= $s[1] ?>;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;"><?= $s[2] ?></span>
</td>
<td style="padding:8px 12px;color:#6B7280;direction:ltr;"><?= e(substr($reg['created_at'], 0, 10)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسجيل لاعب سباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming" 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 if (isset($registration) && $registration): ?>
<!-- Wizard Steps View -->
<div id="swimmingWizard" data-registration-id="<?= (int) $registration['id'] ?>" data-step="<?= e($step) ?>">
<!-- Progress Bar -->
<div class="card" style="margin-bottom:16px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;position:relative;">
<div style="position:absolute;top:50%;left:40px;right:40px;height:3px;background:#E5E7EB;z-index:0;transform:translateY(-50%);"></div>
<?php
$steps = ['البيانات', 'الشهادة الطبية', 'الصورة'];
$stepMap = ['personal' => 1, 'medical' => 2, 'photo' => 3, 'complete' => 4];
$currentNum = $stepMap[$step] ?? 1;
foreach ($steps as $i => $label):
$stepNum = $i + 1;
$isActive = $stepNum == $currentNum;
$isComplete = $stepNum < $currentNum;
$bgColor = $isComplete ? '#059669' : ($isActive ? '#2563EB' : '#E5E7EB');
$textColor = ($isComplete || $isActive) ? '#fff' : '#9CA3AF';
?>
<div style="text-align:center;position:relative;z-index:1;">
<div style="width:40px;height:40px;border-radius:50%;background:<?= $bgColor ?>;color:<?= $textColor ?>;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px;margin:0 auto;">
<?= $isComplete ? '✓' : $stepNum ?>
</div>
<div style="font-size:11px;margin-top:6px;color:<?= $isActive ? '#2563EB' : '#6B7280' ?>;font-weight:<?= $isActive ? '700' : '400' ?>;"><?= $label ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Player Summary -->
<div class="card" style="margin-bottom:16px;">
<div style="padding:14px 16px;display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;border-radius:8px;overflow:hidden;background:#F3F4F6;flex-shrink:0;">
<?php if ($registration['photo_path']): ?>
<img src="/<?= e($registration['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;" alt="">
<?php else: ?>
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9CA3AF;"><i data-lucide="user" style="width:20px;height:20px;"></i></div>
<?php endif; ?>
</div>
<div style="flex:1;">
<div style="font-size:15px;font-weight:700;color:#1A1A2E;"><?= e($registration['full_name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">
<?php if ($registration['player_nid']): ?><span style="direction:ltr;display:inline-block;"><?= e($registration['player_nid']) ?></span><?php endif; ?>
<?php if ($registration['date_of_birth']): ?><?= (int) date_diff(date_create($registration['date_of_birth']), date_create())->y ?> سنة<?php endif; ?>
</div>
</div>
</div>
</div>
<?php if ($step === 'medical'): ?>
<!-- Medical Certificate Upload -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:14px;color:#1A1A2E;"><i data-lucide="heart-pulse" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;color:#DC2626;"></i>الشهادة الطبية</span>
<span style="font-size:11px;color:#DC2626;margin-right:8px;">(مطلوبة - لن يمكن المتابعة بدونها)</span>
</div>
<form id="medicalForm" style="padding:16px;" enctype="multipart/form-data">
<?= csrf_field() ?>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">ملف الشهادة الطبية <span style="color:#DC2626;">*</span></label>
<input type="file" name="medical_file" id="medicalFile" accept=".pdf,.png,.jpg,.jpeg" required
style="width:100%;padding:10px;border:2px dashed #D1D5DB;border-radius:8px;font-size:13px;cursor:pointer;">
<div style="font-size:11px;color:#6B7280;margin-top:4px;">PDF أو صورة (PNG, JPG) — حجم أقصى 10 ميجابايت</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">تاريخ الفحص <span style="color:#DC2626;">*</span></label>
<input type="date" name="exam_date" required class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">تاريخ الانتهاء <span style="color:#DC2626;">*</span></label>
<input type="date" name="expiry_date" required class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;">
</div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:10px;">
<i data-lucide="upload" style="width:15px;height:15px;vertical-align:middle;margin-left:6px;"></i>رفع الشهادة الطبية
</button>
<div id="medicalError" style="display:none;margin-top:10px;padding:8px 12px;background:#FEF2F2;color:#DC2626;border-radius:6px;font-size:12px;"></div>
</form>
</div>
<script>
document.getElementById('medicalForm').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const btn = form.querySelector('button[type=submit]');
const errDiv = document.getElementById('medicalError');
errDiv.style.display = 'none';
btn.disabled = true;
btn.textContent = 'جاري الرفع...';
const fd = new FormData(form);
fetch('/sa/swimming/register/<?= (int) $registration['id'] ?>/medical', {
method: 'POST',
body: fd
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect || window.location.href;
} else {
errDiv.textContent = data.error || 'حدث خطأ';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'رفع الشهادة الطبية';
}
})
.catch(() => {
errDiv.textContent = 'فشل الاتصال بالسيرفر';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'رفع الشهادة الطبية';
});
});
</script>
<?php elseif ($step === 'photo'): ?>
<!-- Photo Capture -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:14px;"><i data-lucide="camera" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i>التقاط صورة اللاعب</span>
</div>
<div style="padding:16px;text-align:center;">
<div id="cameraContainer" style="margin-bottom:16px;">
<video id="cameraFeed" style="width:100%;max-width:320px;border-radius:8px;background:#000;" autoplay playsinline></video>
<canvas id="cameraCanvas" style="display:none;"></canvas>
</div>
<div id="photoPreview" style="display:none;margin-bottom:16px;">
<img id="capturedImage" style="width:100%;max-width:320px;border-radius:8px;" alt="">
</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button type="button" id="captureBtn" class="btn btn-primary" onclick="capturePhoto()">
<i data-lucide="camera" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>التقاط
</button>
<button type="button" id="retakeBtn" class="btn btn-outline" style="display:none;" onclick="retakePhoto()">إعادة</button>
<button type="button" id="savePhotoBtn" class="btn btn-primary" style="display:none;" onclick="savePhoto()">حفظ الصورة</button>
</div>
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #E5E7EB;">
<label style="font-size:12px;color:#6B7280;display:block;margin-bottom:6px;">أو ارفع صورة من الجهاز:</label>
<input type="file" id="photoFileInput" accept="image/*" onchange="handleFileUpload(this)" style="font-size:12px;">
</div>
<div id="photoError" style="display:none;margin-top:10px;padding:8px 12px;background:#FEF2F2;color:#DC2626;border-radius:6px;font-size:12px;"></div>
</div>
</div>
<script>
let stream = null;
const video = document.getElementById('cameraFeed');
const canvas = document.getElementById('cameraCanvas');
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: 640, height: 480 } })
.then(s => { stream = s; video.srcObject = s; })
.catch(() => {});
function capturePhoto() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
document.getElementById('capturedImage').src = canvas.toDataURL('image/jpeg', 0.85);
document.getElementById('cameraContainer').style.display = 'none';
document.getElementById('photoPreview').style.display = 'block';
document.getElementById('captureBtn').style.display = 'none';
document.getElementById('retakeBtn').style.display = '';
document.getElementById('savePhotoBtn').style.display = '';
}
function retakePhoto() {
document.getElementById('cameraContainer').style.display = '';
document.getElementById('photoPreview').style.display = 'none';
document.getElementById('captureBtn').style.display = '';
document.getElementById('retakeBtn').style.display = 'none';
document.getElementById('savePhotoBtn').style.display = 'none';
}
function handleFileUpload(input) {
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('capturedImage').src = e.target.result;
document.getElementById('cameraContainer').style.display = 'none';
document.getElementById('photoPreview').style.display = 'block';
document.getElementById('captureBtn').style.display = 'none';
document.getElementById('retakeBtn').style.display = '';
document.getElementById('savePhotoBtn').style.display = '';
};
reader.readAsDataURL(input.files[0]);
}
}
function savePhoto() {
const btn = document.getElementById('savePhotoBtn');
const errDiv = document.getElementById('photoError');
errDiv.style.display = 'none';
btn.disabled = true;
btn.textContent = 'جاري الحفظ...';
const base64 = document.getElementById('capturedImage').src;
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
fetch('/sa/swimming/register/<?= (int) $registration['id'] ?>/photo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ photo_base64: base64, _csrf_token: csrfToken })
})
.then(r => r.json())
.then(data => {
if (data.success) {
if (stream) stream.getTracks().forEach(t => t.stop());
window.location.href = data.redirect || '/sa/swimming/assign/<?= (int) $registration['id'] ?>';
} else {
errDiv.textContent = data.error || 'حدث خطأ';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'حفظ الصورة';
}
})
.catch(() => {
errDiv.textContent = 'فشل الاتصال';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'حفظ الصورة';
});
}
</script>
<?php elseif ($step === 'complete'): ?>
<!-- Registration Complete -->
<div class="card" style="text-align:center;padding:40px;">
<div style="width:64px;height:64px;border-radius:50%;background:#ECFDF5;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
<i data-lucide="check" style="width:32px;height:32px;color:#059669;"></i>
</div>
<h3 style="font-size:18px;font-weight:700;color:#1A1A2E;margin-bottom:8px;">تم تسجيل اللاعب بنجاح</h3>
<p style="font-size:13px;color:#6B7280;margin-bottom:20px;">يمكنك الآن تعيين اللاعب في مجموعة سباحة</p>
<div style="display:flex;gap:10px;justify-content:center;">
<a href="/sa/swimming/assign/<?= (int) $registration['id'] ?>" class="btn btn-primary">تعيين في مجموعة</a>
<a href="/sa/swimming/register" class="btn btn-outline">تسجيل لاعب آخر</a>
</div>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Lookup Form -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:15px;">تسجيل لاعب سباحة جديد</span>
</div>
<form id="lookupForm" style="padding:16px;">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">رقم العضوية</label>
<input type="text" name="membership_number" class="form-control" placeholder="اختياري" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" class="form-control" maxlength="14" placeholder="14 رقم" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;direction:ltr;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" class="form-control" required style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;direction:ltr;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الهاتف</label>
<input type="text" name="phone" class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;direction:ltr;">
</div>
<div></div>
</div>
<!-- Guardian Section -->
<div style="border-top:1px solid #E5E7EB;padding-top:16px;margin-top:8px;margin-bottom:16px;">
<span style="font-size:13px;font-weight:700;color:#374151;">بيانات ولي الأمر (إذا كان اللاعب أقل من 18 سنة)</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">اسم ولي الأمر</label>
<input type="text" name="guardian_name" class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">هاتف ولي الأمر</label>
<input type="text" name="guardian_phone" class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;direction:ltr;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">الرقم القومي لولي الأمر</label>
<input type="text" name="guardian_national_id" class="form-control" maxlength="14" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;direction:ltr;">
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:6px;">صلة القرابة</label>
<select name="guardian_relationship" class="form-control" style="width:100%;padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;">
<option value=""></option>
<option value="father">أب</option>
<option value="mother">أم</option>
<option value="brother">أخ</option>
<option value="sister">أخت</option>
<option value="uncle">عم / خال</option>
<option value="other">أخرى</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:10px;font-size:14px;">
<i data-lucide="search" style="width:15px;height:15px;vertical-align:middle;margin-left:6px;"></i>بحث وتسجيل
</button>
<div id="lookupError" style="display:none;margin-top:10px;padding:8px 12px;background:#FEF2F2;color:#DC2626;border-radius:6px;font-size:12px;"></div>
</form>
</div>
<script>
document.getElementById('lookupForm').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const btn = form.querySelector('button[type=submit]');
const errDiv = document.getElementById('lookupError');
errDiv.style.display = 'none';
btn.disabled = true;
btn.innerHTML = 'جاري البحث...';
const fd = new FormData(form);
fetch('/sa/swimming/register/lookup', { method: 'POST', body: fd })
.then(r => r.json())
.then(data => {
if (data.success && data.redirect) {
window.location.href = data.redirect;
} else {
errDiv.textContent = data.error || 'حدث خطأ';
errDiv.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i data-lucide="search" style="width:15px;height:15px;vertical-align:middle;margin-left:6px;"></i>بحث وتسجيل';
}
})
.catch(() => {
errDiv.textContent = 'فشل الاتصال بالسيرفر';
errDiv.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i data-lucide="search" style="width:15px;height:15px;vertical-align:middle;margin-left:6px;"></i>بحث وتسجيل';
});
});
</script>
<?php endif; ?>
<?php $__template->endSection(); ?>
...@@ -40,6 +40,10 @@ MenuRegistry::register('sports_activity', [ ...@@ -40,6 +40,10 @@ MenuRegistry::register('sports_activity', [
['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], ['label_ar' => 'تذاكر السباحة', 'label_en' => 'Pool Tickets', 'route' => '/sa/pool-tickets', 'permission' => 'sa.pool_ticket.view', 'order' => 22],
['label_ar' => '── السباحة ──', 'label_en' => '── Swimming ──', 'route' => '#', 'permission' => 'sa.swimming.dashboard','order' => 30],
['label_ar' => 'لوحة تحكم السباحة','label_en' => 'Swimming Dashboard','route' => '/sa/swimming', 'permission' => 'sa.swimming.dashboard','order' => 31],
['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],
], ],
]); ]);
...@@ -99,6 +103,9 @@ PermissionRegistry::register('sports_activity', [ ...@@ -99,6 +103,9 @@ PermissionRegistry::register('sports_activity', [
'sa.pool_ticket.view' => ['ar' => 'عرض تذاكر حمام السباحة', 'en' => 'View Pool Tickets'], 'sa.pool_ticket.view' => ['ar' => 'عرض تذاكر حمام السباحة', 'en' => 'View Pool Tickets'],
'sa.pool_ticket.issue' => ['ar' => 'إصدار تذكرة سباحة', 'en' => 'Issue Pool Ticket'], 'sa.pool_ticket.issue' => ['ar' => 'إصدار تذكرة سباحة', 'en' => 'Issue Pool Ticket'],
'sa.pool_ticket.manage' => ['ar' => 'إدارة تذاكر السباحة', 'en' => 'Manage Pool Tickets'], 'sa.pool_ticket.manage' => ['ar' => 'إدارة تذاكر السباحة', 'en' => 'Manage Pool Tickets'],
'sa.swimming.dashboard' => ['ar' => 'لوحة تحكم السباحة', 'en' => 'Swimming Dashboard'],
'sa.swimming.register' => ['ar' => 'تسجيل لاعب سباحة', 'en' => 'Register Swimming Player'],
'sa.swimming.assign' => ['ar' => 'تعيين لاعب في مجموعة سباحة','en' => 'Assign Swimming Player to Group'],
]); ]);
// ─── Event Listeners ──────────────────────────────────────────────────────── // ─── Event Listeners ────────────────────────────────────────────────────────
......
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