Commit c0df407f authored by Mahmoud Aglan's avatar Mahmoud Aglan

Created Wizard

parent be45caf1
<?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\GateAccessService;
class GateController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$todayStats = $db->selectOne(
"SELECT
COUNT(*) as total_scans,
SUM(CASE WHEN granted = 1 AND access_type = 'entry' THEN 1 ELSE 0 END) as entries,
SUM(CASE WHEN granted = 0 THEN 1 ELSE 0 END) as denied,
COUNT(DISTINCT player_id) as unique_players
FROM sa_gate_access_log
WHERE DATE(recorded_at) = CURDATE()"
);
$recentEntries = $db->select(
"SELECT g.*, p.full_name_ar, p.photo_path, c.card_number
FROM sa_gate_access_log g
INNER JOIN sa_players p ON p.id = g.player_id
LEFT JOIN sa_player_cards c ON c.id = g.card_id
WHERE DATE(g.recorded_at) = CURDATE()
ORDER BY g.recorded_at DESC
LIMIT 20"
);
return $this->view('SportsActivity.Views.gate.index', [
'todayStats' => $todayStats,
'recentEntries' => $recentEntries,
]);
}
public function scan(Request $request): Response
{
$scannedData = trim((string) $request->post('scanned_data', ''));
$accessPoint = trim((string) $request->post('access_point', ''));
if ($scannedData === '') {
return $this->json(['success' => false, 'error' => 'لا توجد بيانات مسح']);
}
$result = GateAccessService::checkAccess($scannedData);
if ($result['granted']) {
$card = $result['card'];
GateAccessService::recordEntry(
(int) $card['player_id'],
(int) $card['id'],
$accessPoint
);
return $this->json([
'success' => true,
'granted' => true,
'player_name' => $result['player_name'],
'player_type' => $result['player_type'],
'card_number' => $card['card_number'],
'card_type' => $result['card_type'],
'valid_until' => $result['valid_until'],
'photo_path' => $card['photo_path'] ?? null,
]);
}
$card = $result['card'] ?? null;
if ($card) {
GateAccessService::recordDenial(
(int) $card['player_id'],
(int) $card['id'],
$result['reason'],
$scannedData
);
}
return $this->json([
'success' => true,
'granted' => false,
'reason' => $result['reason'],
'player_name' => $card['full_name_ar'] ?? null,
'card_number' => $card['card_number'] ?? null,
]);
}
public function log(Request $request): Response
{
$db = App::getInstance()->db();
$date = trim((string) $request->get('date', date('Y-m-d')));
$grantedFilter = trim((string) $request->get('granted', ''));
$search = trim((string) $request->get('q', ''));
$page = max(1, (int) $request->get('page', 1));
$perPage = 50;
$where = 'DATE(g.recorded_at) = ?';
$params = [$date];
if ($grantedFilter !== '') {
$where .= ' AND g.granted = ?';
$params[] = (int) $grantedFilter;
}
if ($search !== '') {
$where .= ' AND (p.full_name_ar LIKE ? OR c.card_number LIKE ?)';
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
}
$total = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_gate_access_log g
INNER JOIN sa_players p ON p.id = g.player_id
LEFT JOIN sa_player_cards c ON c.id = g.card_id
WHERE {$where}",
$params
)['cnt'];
$pagination = \App\Core\Pagination::paginate($total, $perPage, $page);
$offset = ($pagination['current_page'] - 1) * $perPage;
$entries = $db->select(
"SELECT g.*, p.full_name_ar, p.player_type, p.photo_path, c.card_number, c.card_type,
e.full_name_ar as recorded_by_name
FROM sa_gate_access_log g
INNER JOIN sa_players p ON p.id = g.player_id
LEFT JOIN sa_player_cards c ON c.id = g.card_id
LEFT JOIN employees e ON e.id = g.recorded_by
WHERE {$where}
ORDER BY g.recorded_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return $this->view('SportsActivity.Views.gate.log', [
'entries' => $entries,
'pagination' => $pagination,
'filters' => ['date' => $date, 'granted' => $grantedFilter, 'q' => $search],
]);
}
public function report(Request $request): Response
{
$date = trim((string) $request->get('date', date('Y-m-d')));
$report = GateAccessService::getDailyAccessReport($date);
return $this->view('SportsActivity.Views.gate.log', [
'entries' => $report['entries'],
'pagination' => null,
'filters' => ['date' => $date, 'granted' => '', 'q' => ''],
'summary' => $report['summary'],
'isReport' => true,
]);
}
}
This diff is collapsed.
...@@ -187,4 +187,32 @@ return [ ...@@ -187,4 +187,32 @@ return [
['POST', '/api/sa/pool-grid/{id:\d+}/templates/{tid:\d+}/delete', 'SportsActivity\Controllers\Api\PoolGridTemplateApiController@delete', ['auth', 'csrf'], 'sa.pool-grid.manage'], ['POST', '/api/sa/pool-grid/{id:\d+}/templates/{tid:\d+}/delete', 'SportsActivity\Controllers\Api\PoolGridTemplateApiController@delete', ['auth', 'csrf'], 'sa.pool-grid.manage'],
['POST', '/api/sa/pool-grid/{id:\d+}/templates/{tid:\d+}/expand', 'SportsActivity\Controllers\Api\PoolGridTemplateApiController@expand', ['auth', 'csrf'], 'sa.pool-grid.manage'], ['POST', '/api/sa/pool-grid/{id:\d+}/templates/{tid:\d+}/expand', 'SportsActivity\Controllers\Api\PoolGridTemplateApiController@expand', ['auth', 'csrf'], 'sa.pool-grid.manage'],
['POST', '/api/sa/pool-grid/{id:\d+}/templates/{tid:\d+}/runs/{rid:\d+}/rollback', 'SportsActivity\Controllers\Api\PoolGridTemplateApiController@rollback', ['auth', 'csrf'], 'sa.pool-grid.manage'], ['POST', '/api/sa/pool-grid/{id:\d+}/templates/{tid:\d+}/runs/{rid:\d+}/rollback', 'SportsActivity\Controllers\Api\PoolGridTemplateApiController@rollback', ['auth', 'csrf'], 'sa.pool-grid.manage'],
// ─── Registration Wizard ────────────────────────────────────────────────────
['GET', '/sa/registration/report', 'SportsActivity\Controllers\RegistrationWizardController@report', ['auth'], 'sa.registration.view'],
['GET', '/sa/registration', 'SportsActivity\Controllers\RegistrationWizardController@index', ['auth'], 'sa.registration.manage'],
['POST', '/sa/registration/lookup', 'SportsActivity\Controllers\RegistrationWizardController@lookupPlayer', ['auth', 'csrf'], 'sa.registration.manage'],
['GET', '/sa/registration/{id:\d+}', 'SportsActivity\Controllers\RegistrationWizardController@wizardStep', ['auth'], 'sa.registration.manage'],
['POST', '/sa/registration/{id:\d+}/photo', 'SportsActivity\Controllers\RegistrationWizardController@uploadPhoto', ['auth', 'csrf'], 'sa.registration.manage'],
['POST', '/sa/registration/{id:\d+}/activity', 'SportsActivity\Controllers\RegistrationWizardController@selectActivity', ['auth', 'csrf'], 'sa.registration.manage'],
['POST', '/sa/registration/{id:\d+}/pay', 'SportsActivity\Controllers\RegistrationWizardController@submitPayment', ['auth', 'csrf'], 'sa.registration.manage'],
['GET', '/sa/registration/{id:\d+}/print-form', 'SportsActivity\Controllers\RegistrationWizardController@printForm', ['auth'], 'sa.registration.manage'],
['POST', '/sa/registration/{id:\d+}/generate-card', 'SportsActivity\Controllers\RegistrationWizardController@generateCard', ['auth', 'csrf'], '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'],
// ─── SA Player Cards ────────────────────────────────────────────────────────
['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/{id:\d+}', 'SportsActivity\Controllers\SaCardController@show', ['auth'], 'sa.card.view'],
['POST', '/sa/cards/{id:\d+}/suspend', 'SportsActivity\Controllers\SaCardController@suspend', ['auth', 'csrf'], 'sa.card.manage'],
['POST', '/sa/cards/{id:\d+}/revoke', 'SportsActivity\Controllers\SaCardController@revoke', ['auth', 'csrf'], 'sa.card.manage'],
['POST', '/sa/cards/{id:\d+}/reactivate', 'SportsActivity\Controllers\SaCardController@reactivate', ['auth', 'csrf'], 'sa.card.manage'],
['GET', '/sa/cards/{id:\d+}/print', 'SportsActivity\Controllers\SaCardController@print', ['auth'], 'sa.card.print'],
// ─── Gate Access ────────────────────────────────────────────────────────────
['GET', '/sa/gate', 'SportsActivity\Controllers\GateController@index', ['auth'], 'sa.gate.view'],
['POST', '/sa/gate/scan', 'SportsActivity\Controllers\GateController@scan', ['auth', 'csrf'], 'sa.gate.scan'],
['GET', '/sa/gate/log', 'SportsActivity\Controllers\GateController@log', ['auth'], 'sa.gate.view'],
['GET', '/sa/gate/report', 'SportsActivity\Controllers\GateController@report', ['auth'], 'sa.gate.view'],
]; ];
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
final class GateAccessService
{
public static function checkAccess(string $scannedData): array
{
$db = App::getInstance()->db();
$cardNumber = self::extractCardNumber($scannedData);
if ($cardNumber === null) {
return ['granted' => false, 'reason' => 'بيانات المسح غير صالحة'];
}
$card = $db->selectOne(
"SELECT c.*, p.full_name_ar, p.photo_path, p.player_type
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE c.card_number = ? AND c.is_archived = 0",
[$cardNumber]
);
if (!$card) {
return ['granted' => false, 'reason' => 'الكارت غير موجود في النظام'];
}
if ($card['status'] === 'suspended') {
return [
'granted' => false,
'reason' => 'الكارت موقوف',
'card' => $card,
];
}
if ($card['status'] === 'revoked') {
return [
'granted' => false,
'reason' => 'الكارت ملغى',
'card' => $card,
];
}
if ($card['status'] === 'expired') {
return [
'granted' => false,
'reason' => 'الكارت منتهي الصلاحية',
'card' => $card,
];
}
if ($card['valid_until'] !== null && $card['valid_until'] < date('Y-m-d')) {
$db->update('sa_player_cards', [
'status' => 'expired',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $card['id']]);
return [
'granted' => false,
'reason' => 'الكارت منتهي الصلاحية',
'card' => $card,
];
}
if (!in_array($card['status'], ['active', 'temporary'], true)) {
return [
'granted' => false,
'reason' => 'حالة الكارت غير صالحة للدخول (' . $card['status'] . ')',
'card' => $card,
];
}
return [
'granted' => true,
'card' => $card,
'player_name' => $card['full_name_ar'],
'player_type' => $card['player_type'],
'card_type' => $card['card_type'],
'valid_until' => $card['valid_until'],
];
}
public static function recordEntry(int $playerId, ?int $cardId, string $accessPoint = ''): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
return $db->insert('sa_gate_access_log', [
'player_id' => $playerId,
'card_id' => $cardId,
'access_type' => 'entry',
'access_point' => $accessPoint ?: null,
'granted' => 1,
'recorded_by' => $employee ? (int) $employee->id : null,
'recorded_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordDenial(int $playerId, ?int $cardId, string $reason, string $scannedData = ''): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
return $db->insert('sa_gate_access_log', [
'player_id' => $playerId,
'card_id' => $cardId,
'access_type' => 'entry',
'access_point' => null,
'granted' => 0,
'denial_reason' => $reason,
'scanned_data' => $scannedData ?: null,
'recorded_by' => $employee ? (int) $employee->id : null,
'recorded_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordExit(int $playerId, string $accessPoint = ''): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$db->insert('sa_gate_access_log', [
'player_id' => $playerId,
'card_id' => null,
'access_type' => 'exit',
'access_point' => $accessPoint ?: null,
'granted' => 1,
'recorded_by' => $employee ? (int) $employee->id : null,
'recorded_at' => date('Y-m-d H:i:s'),
]);
}
public static function getPlayerAccessHistory(int $playerId, ?string $from = null, ?string $to = null): array
{
$db = App::getInstance()->db();
$where = 'g.player_id = ?';
$params = [$playerId];
if ($from) {
$where .= ' AND g.recorded_at >= ?';
$params[] = $from . ' 00:00:00';
}
if ($to) {
$where .= ' AND g.recorded_at <= ?';
$params[] = $to . ' 23:59:59';
}
return $db->select(
"SELECT g.*, e.full_name_ar as recorded_by_name
FROM sa_gate_access_log g
LEFT JOIN employees e ON e.id = g.recorded_by
WHERE {$where}
ORDER BY g.recorded_at DESC
LIMIT 200",
$params
);
}
public static function getDailyAccessReport(?string $date = null): array
{
$db = App::getInstance()->db();
$date = $date ?: date('Y-m-d');
$summary = $db->selectOne(
"SELECT
COUNT(*) as total_scans,
SUM(CASE WHEN granted = 1 AND access_type = 'entry' THEN 1 ELSE 0 END) as entries_granted,
SUM(CASE WHEN granted = 0 THEN 1 ELSE 0 END) as entries_denied,
SUM(CASE WHEN access_type = 'exit' THEN 1 ELSE 0 END) as exits,
COUNT(DISTINCT player_id) as unique_players
FROM sa_gate_access_log
WHERE DATE(recorded_at) = ?",
[$date]
);
$entries = $db->select(
"SELECT g.*, p.full_name_ar, p.player_type, c.card_number, c.card_type
FROM sa_gate_access_log g
INNER JOIN sa_players p ON p.id = g.player_id
LEFT JOIN sa_player_cards c ON c.id = g.card_id
WHERE DATE(g.recorded_at) = ?
ORDER BY g.recorded_at DESC
LIMIT 500",
[$date]
);
return [
'date' => $date,
'summary' => $summary,
'entries' => $entries,
];
}
private static function extractCardNumber(?string $scannedData): ?string
{
if ($scannedData === null || $scannedData === '') {
return null;
}
if (str_starts_with($scannedData, 'THECLUB:SA:')) {
$parts = explode(':', $scannedData);
return $parts[2] ?? null;
}
if (preg_match('/^SAC-\d{4}-\d{6}$/', $scannedData)) {
return $scannedData;
}
return null;
}
}
This diff is collapsed.
<?php
use App\Modules\Settings\Services\BrandingService;
$clubNameAr = BrandingService::clubNameAr();
$clubNameEn = BrandingService::clubNameEn();
$logoUrl = BrandingService::logo();
$photoPath = $card['player_photo'] ?? $card['photo_path'] ?? '';
$cardStatus = $card['status'];
$validUntil = $card['valid_until'] ?? null;
?>
<?php $__template->layout('Layout.print'); ?>
<?php $__template->section('title'); ?>كارت النشاط الرياضي — <?= e($card['card_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<style>
@media print { body { margin: 0; } .no-print { display: none !important; } }
.sa-card-container { width: 340px; margin: 20px auto; font-family: 'Cairo', Arial, sans-serif; }
.sa-card-front {
width: 340px; height: 215px; background: linear-gradient(135deg, #1E3A5F 0%, #2563EB 100%);
border-radius: 12px; color: #fff; position: relative; overflow: hidden; margin-bottom: 15px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2); display: flex; direction: rtl;
}
.sa-card-front .photo-side {
width: 110px; height: 215px; flex-shrink: 0; position: relative; overflow: hidden;
border-radius: 12px 0 0 12px;
}
.sa-card-front .photo-side img { width: 100%; height: 100%; object-fit: cover; }
.sa-card-front .photo-side .no-photo {
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.5); font-size: 11px;
}
.sa-card-front .info-side { flex: 1; padding: 14px 16px; display: flex; flex-direction: column; justify-content: space-between; }
.sa-card-front .club-header { display: flex; align-items: center; gap: 8px; }
.sa-card-front .club-header img { max-height: 22px; }
.sa-card-front .club-header .club-name { font-size: 11px; font-weight: 600; opacity: 0.9; }
.sa-card-front .badge { position: absolute; top: 10px; left: 10px; background: #F59E0B; color: #1A1A2E; font-size: 9px; font-weight: 700; padding: 2px 8px; border-radius: 10px; }
.sa-card-front .player-info { flex: 1; display: flex; flex-direction: column; justify-content: center; }
.sa-card-front .player-name { font-size: 14px; font-weight: 700; margin-bottom: 2px; }
.sa-card-front .player-name-en { font-size: 10px; opacity: 0.7; margin-bottom: 6px; }
.sa-card-front .card-num { font-size: 16px; font-weight: 700; letter-spacing: 1.5px; direction: ltr; text-align: right; }
.sa-card-front .activity-name { font-size: 11px; opacity: 0.8; margin-top: 4px; }
.sa-card-front .validity { font-size: 10px; opacity: 0.7; }
.sa-card-front .qr-area {
position: absolute; bottom: 8px; left: 8px; width: 50px; height: 50px;
background: #fff; border-radius: 5px; padding: 3px;
}
.sa-card-front .qr-area svg { width: 44px; height: 44px; }
.sa-card-back {
width: 340px; height: 215px; background: #fff; border-radius: 12px;
border: 2px solid #2563EB; padding: 16px; position: relative;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.sa-card-back .back-header { text-align: center; border-bottom: 2px solid #2563EB; padding-bottom: 8px; margin-bottom: 10px; }
.sa-card-back .back-header .title { font-size: 12px; font-weight: 700; color: #2563EB; }
.sa-card-back .rules { font-size: 9px; color: #6B7280; line-height: 1.6; }
.sa-card-back .rules li { margin-bottom: 3px; }
.sa-card-back .emergency { position: absolute; bottom: 10px; left: 16px; right: 16px; text-align: center; font-size: 9px; color: #9CA3AF; border-top: 1px solid #E5E7EB; padding-top: 6px; }
</style>
<div class="no-print" style="text-align:center;padding:10px;">
<button onclick="window.print()" style="padding:8px 20px;background:#2563EB;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">طباعة</button>
<a href="/sa/cards" style="margin-right:10px;color:#6B7280;font-size:14px;">رجوع</a>
</div>
<div class="sa-card-container">
<!-- Front -->
<div class="sa-card-front">
<div class="badge">نشاط رياضي</div>
<div class="photo-side">
<?php if ($photoPath): ?>
<img src="/<?= e($photoPath) ?>" alt="صورة">
<?php else: ?>
<div class="no-photo">لا صورة</div>
<?php endif; ?>
</div>
<div class="info-side">
<div class="club-header">
<?php if ($logoUrl): ?><img src="<?= e($logoUrl) ?>" alt="Logo"><?php endif; ?>
<span class="club-name"><?= e($clubNameAr) ?></span>
</div>
<div class="player-info">
<div class="player-name"><?= e($card['full_name_ar']) ?></div>
<?php if (!empty($card['full_name_en'])): ?>
<div class="player-name-en"><?= e($card['full_name_en']) ?></div>
<?php endif; ?>
<div class="card-num"><?= e($card['card_number']) ?></div>
<?php if ($group): ?>
<div class="activity-name"><?= e($group['discipline_name'] ?? '') ?><?= e($group['name_ar'] ?? '') ?></div>
<?php endif; ?>
</div>
<div class="validity">
صالح من: <?= e(substr($card['valid_from'], 0, 10)) ?>
<?php if ($validUntil): ?> — حتى: <?= e($validUntil) ?><?php endif; ?>
</div>
</div>
<div class="qr-area"><?= $qrSvg ?></div>
</div>
<!-- Back -->
<div class="sa-card-back">
<div class="back-header">
<div class="title"><?= e($clubNameAr) ?> — قسم الأنشطة الرياضية</div>
</div>
<ul class="rules">
<li>هذا الكارت شخصي ولا يجوز استخدامه من الغير.</li>
<li>يجب إبراز الكارت عند الدخول لمنطقة النشاط.</li>
<li>في حالة فقدان الكارت يتم الإبلاغ فوراً واستخراج بدل فاقد.</li>
<li>الإدارة غير مسؤولة عن المتعلقات الشخصية.</li>
<li>يلتزم اللاعب بالزي الرياضي المناسب.</li>
</ul>
<div class="emergency"><?= e($clubNameEn) ?> — Sports Activity Card</div>
</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/gate/log" class="btn btn-outline"><i data-lucide="list" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> السجل</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Stats -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">إجمالي المسح</div>
<div style="font-size:22px;font-weight:800;color:#1A1A2E;margin-top:4px;"><?= (int) ($todayStats['total_scans'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">دخول مسموح</div>
<div style="font-size:22px;font-weight:800;color:#059669;margin-top:4px;"><?= (int) ($todayStats['entries'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">مرفوض</div>
<div style="font-size:22px;font-weight:800;color:#DC2626;margin-top:4px;"><?= (int) ($todayStats['denied'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">لاعبين مختلفين</div>
<div style="font-size:22px;font-weight:800;color:#2563EB;margin-top:4px;"><?= (int) ($todayStats['unique_players'] ?? 0) ?></div>
</div>
</div>
<!-- Scan Interface -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="scan" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> مسح الكارت</h3>
</div>
<div style="padding:30px;text-align:center;">
<div style="max-width:400px;margin:0 auto;">
<input type="text" id="scanInput" class="form-input" placeholder="امسح QR أو أدخل رقم الكارت..." dir="ltr" style="font-size:18px;text-align:center;padding:14px;" autofocus>
<button type="button" id="btnScan" class="btn btn-primary" style="margin-top:12px;width:100%;padding:12px;font-size:16px;"><i data-lucide="shield-check" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تحقق</button>
</div>
<!-- Result Display -->
<div id="scanResult" style="display:none;margin-top:20px;padding:20px;border-radius:12px;max-width:400px;margin-left:auto;margin-right:auto;">
</div>
</div>
</div>
<!-- Recent Entries -->
<?php if (!empty($recentEntries)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;font-weight:600;">آخر العمليات اليوم</h3>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">الوقت</th>
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">اللاعب</th>
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">الكارت</th>
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">النتيجة</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentEntries as $entry): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:8px 15px;direction:ltr;text-align:right;"><?= e(substr($entry['recorded_at'], 11, 5)) ?></td>
<td style="padding:8px 15px;"><?= e($entry['full_name_ar']) ?></td>
<td style="padding:8px 15px;direction:ltr;text-align:right;font-size:12px;"><?= e($entry['card_number'] ?? '—') ?></td>
<td style="padding:8px 15px;">
<?php if ((int) $entry['granted']): ?>
<span style="color:#059669;font-weight:600;">✓ دخول</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;"><?= e($entry['denial_reason'] ?? 'مرفوض') ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var scanInput = document.getElementById('scanInput');
var btnScan = document.getElementById('btnScan');
var scanResult = document.getElementById('scanResult');
var csrfToken = '';
var csrfInput = document.querySelector('input[name="_csrf_token"]');
if (csrfInput) csrfToken = csrfInput.value;
function doScan() {
var data = scanInput.value.trim();
if (!data) return;
btnScan.disabled = true;
fetch('/sa/gate/scan', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({scanned_data: data, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(res) {
btnScan.disabled = false;
scanResult.style.display = 'block';
if (res.granted) {
scanResult.style.background = '#ECFDF5';
scanResult.style.border = '2px solid #059669';
scanResult.innerHTML = '<div style="font-size:40px;margin-bottom:8px;">✓</div>' +
'<div style="font-size:18px;font-weight:700;color:#059669;">مسموح بالدخول</div>' +
'<div style="margin-top:8px;font-size:14px;color:#1A1A2E;">' + (res.player_name || '') + '</div>' +
'<div style="font-size:12px;color:#6B7280;">' + (res.card_number || '') + '</div>';
} else {
scanResult.style.background = '#FEF2F2';
scanResult.style.border = '2px solid #DC2626';
scanResult.innerHTML = '<div style="font-size:40px;margin-bottom:8px;">✗</div>' +
'<div style="font-size:18px;font-weight:700;color:#DC2626;">ممنوع الدخول</div>' +
'<div style="margin-top:8px;font-size:14px;color:#DC2626;">' + (res.reason || '') + '</div>' +
(res.player_name ? '<div style="font-size:12px;color:#6B7280;margin-top:4px;">' + res.player_name + '</div>' : '');
}
scanInput.value = '';
scanInput.focus();
setTimeout(function() { scanResult.style.display = 'none'; }, 5000);
}).catch(function() {
btnScan.disabled = false;
});
}
btnScan.addEventListener('click', doScan);
scanInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); doScan(); }
});
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>سجل البوابة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/gate" 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($summary)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:12px;margin-bottom:20px;">
<div class="card" style="padding:14px;text-align:center;"><div style="font-size:11px;color:#6B7280;">إجمالي</div><div style="font-size:20px;font-weight:800;"><?= (int) ($summary['total_scans'] ?? 0) ?></div></div>
<div class="card" style="padding:14px;text-align:center;"><div style="font-size:11px;color:#6B7280;">دخول</div><div style="font-size:20px;font-weight:800;color:#059669;"><?= (int) ($summary['entries_granted'] ?? 0) ?></div></div>
<div class="card" style="padding:14px;text-align:center;"><div style="font-size:11px;color:#6B7280;">مرفوض</div><div style="font-size:20px;font-weight:800;color:#DC2626;"><?= (int) ($summary['entries_denied'] ?? 0) ?></div></div>
<div class="card" style="padding:14px;text-align:center;"><div style="font-size:11px;color:#6B7280;">لاعبين</div><div style="font-size:20px;font-weight:800;color:#2563EB;"><?= (int) ($summary['unique_players'] ?? 0) ?></div></div>
</div>
<?php endif; ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;align-items:end;gap:15px;flex-wrap:wrap;">
<div class="form-group" style="margin:0;min-width:150px;">
<label class="form-label" style="font-size:12px;">التاريخ</label>
<input type="date" name="date" value="<?= e($filters['date'] ?? date('Y-m-d')) ?>" class="form-input" style="direction:ltr;">
</div>
<div class="form-group" style="margin:0;min-width:130px;">
<label class="form-label" style="font-size:12px;">النتيجة</label>
<select name="granted" class="form-select">
<option value="">— الكل —</option>
<option value="1" <?= ($filters['granted'] ?? '') === '1' ? 'selected' : '' ?>>مسموح</option>
<option value="0" <?= ($filters['granted'] ?? '') === '0' ? 'selected' : '' ?>>مرفوض</option>
</select>
</div>
<div class="form-group" style="margin:0;min-width:180px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" class="form-input" placeholder="اسم أو رقم كارت...">
</div>
<button type="submit" class="btn btn-primary" style="padding:9px 20px;"><i data-lucide="search" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> بحث</button>
</form>
</div>
</div>
<!-- Log Table -->
<div class="card">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">الوقت</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">اللاعب</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">النوع</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">الكارت</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">العملية</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">النتيجة</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">السبب</th>
</tr>
</thead>
<tbody>
<?php if (empty($entries)): ?>
<tr><td colspan="7" style="padding:30px;text-align:center;color:#9CA3AF;">لا توجد سجلات</td></tr>
<?php else: ?>
<?php foreach ($entries as $entry): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= e(substr($entry['recorded_at'], 11, 5)) ?></td>
<td style="padding:10px 15px;"><?= e($entry['full_name_ar']) ?></td>
<td style="padding:10px 15px;font-size:11px;"><?= ($entry['player_type'] ?? '') === 'member' ? 'عضو' : 'غير عضو' ?></td>
<td style="padding:10px 15px;direction:ltr;text-align:right;font-size:12px;"><?= e($entry['card_number'] ?? '—') ?></td>
<td style="padding:10px 15px;"><?= $entry['access_type'] === 'entry' ? 'دخول' : 'خروج' ?></td>
<td style="padding:10px 15px;">
<?php if ((int) $entry['granted']): ?>
<span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#ECFDF5;color:#059669;">مسموح</span>
<?php else: ?>
<span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#FEF2F2;color:#DC2626;">مرفوض</span>
<?php endif; ?>
</td>
<td style="padding:10px 15px;font-size:12px;color:#6B7280;"><?= e($entry['denial_reason'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && $pagination && $pagination['total_pages'] > 1): ?>
<div style="margin-top:15px;display:flex;justify-content:center;gap:5px;">
<?php for ($p = 1; $p <= $pagination['total_pages']; $p++): ?>
<a href="?<?= http_build_query(array_merge($filters, ['page' => $p])) ?>" class="btn <?= $p === $pagination['current_page'] ? 'btn-primary' : 'btn-outline' ?>" style="padding:6px 12px;font-size:12px;"><?= $p ?></a>
<?php endfor; ?>
</div>
<?php endif; ?>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Settings\Services\BrandingService;
$clubNameAr = BrandingService::clubNameAr();
$clubNameEn = BrandingService::clubNameEn();
$clubSubtitle = BrandingService::subtitle();
$logoUrl = BrandingService::logo();
?>
<?php $__template->layout('Layout.print'); ?>
<?php $__template->section('title'); ?>استمارة تسجيل النشاط الرياضي — <?= e($registration['registration_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<style>
@media print { body { margin: 0; } .no-print { display: none !important; } }
.reg-form { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 15mm 20mm; font-family: 'Cairo', Arial, sans-serif; direction: rtl; font-size: 13px; color: #1A1A2E; }
.reg-form .header { text-align: center; margin-bottom: 20px; border-bottom: 3px solid #1A1A2E; padding-bottom: 15px; }
.reg-form .header img { max-height: 50px; margin-bottom: 8px; }
.reg-form .header h1 { font-size: 20px; margin: 5px 0 3px; }
.reg-form .header h2 { font-size: 14px; color: #6B7280; margin: 0; font-weight: 400; }
.reg-form .form-title { text-align: center; font-size: 18px; font-weight: 700; margin: 15px 0; padding: 8px; background: #F3F4F6; border-radius: 6px; }
.reg-form .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 20px; margin-bottom: 20px; }
.reg-form .info-row { display: flex; gap: 8px; padding: 6px 0; border-bottom: 1px dotted #D1D5DB; }
.reg-form .info-row .label { font-weight: 600; min-width: 120px; color: #374151; }
.reg-form .info-row .value { flex: 1; }
.reg-form .photo-section { float: left; margin: 0 0 15px 15px; }
.reg-form .photo-section img { width: 100px; height: 120px; object-fit: cover; border: 2px solid #1A1A2E; border-radius: 6px; }
.reg-form .photo-section .no-photo { width: 100px; height: 120px; border: 2px solid #D1D5DB; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #9CA3AF; font-size: 11px; }
.reg-form .fee-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.reg-form .fee-table th, .reg-form .fee-table td { border: 1px solid #D1D5DB; padding: 8px 12px; text-align: right; }
.reg-form .fee-table th { background: #F3F4F6; font-weight: 600; }
.reg-form .fee-table .total-row { background: #EFF6FF; font-weight: 700; }
.reg-form .signatures { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-top: 40px; text-align: center; }
.reg-form .signatures .sig-box { border-top: 1px solid #1A1A2E; padding-top: 8px; margin-top: 50px; }
.reg-form .qr-section { position: absolute; bottom: 20mm; left: 20mm; }
.reg-form .reg-number { position: absolute; top: 15mm; left: 20mm; font-size: 12px; direction: ltr; color: #6B7280; }
</style>
<div class="no-print" style="text-align:center;padding:10px;">
<button onclick="window.print()" style="padding:8px 20px;background:#2563EB;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">طباعة</button>
<a href="/sa/registration/<?= (int) $registration['id'] ?>" style="margin-right:10px;color:#6B7280;font-size:14px;">رجوع</a>
</div>
<div class="reg-form" style="position:relative;">
<div class="reg-number"><?= e($registration['registration_number']) ?></div>
<!-- Header -->
<div class="header">
<?php if ($logoUrl): ?><img src="<?= e($logoUrl) ?>" alt="Logo"><?php endif; ?>
<h1><?= e($clubNameAr) ?></h1>
<h2><?= e($clubSubtitle) ?></h2>
</div>
<div class="form-title">استمارة تسجيل النشاط الرياضي</div>
<!-- Photo -->
<div class="photo-section">
<?php if ($registration['photo_path']): ?>
<img src="/<?= e($registration['photo_path']) ?>" alt="صورة اللاعب">
<?php else: ?>
<div class="no-photo">لا توجد صورة</div>
<?php endif; ?>
</div>
<!-- Player Data -->
<div class="info-grid">
<div class="info-row"><span class="label">الاسم:</span><span class="value"><?= e($registration['full_name_ar']) ?></span></div>
<div class="info-row"><span class="label">النوع:</span><span class="value"><?= $registration['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?></span></div>
<div class="info-row"><span class="label">الرقم القومي:</span><span class="value" style="direction:ltr;text-align:right;"><?= e($registration['player_nid'] ?? '—') ?></span></div>
<div class="info-row"><span class="label">تاريخ الميلاد:</span><span class="value"><?= e($registration['date_of_birth'] ?? '—') ?></span></div>
<div class="info-row"><span class="label">النوع:</span><span class="value"><?= ($registration['gender'] ?? '') === 'male' ? 'ذكر' : (($registration['gender'] ?? '') === 'female' ? 'أنثى' : '—') ?></span></div>
<div class="info-row"><span class="label">الهاتف:</span><span class="value" style="direction:ltr;text-align:right;"><?= e($registration['phone'] ?? '—') ?></span></div>
<div class="info-row"><span class="label">ولي الأمر:</span><span class="value"><?= e($registration['guardian_name'] ?? '—') ?></span></div>
<div class="info-row"><span class="label">هاتف ولي الأمر:</span><span class="value" style="direction:ltr;text-align:right;"><?= e($registration['guardian_phone'] ?? '—') ?></span></div>
<div class="info-row"><span class="label">الرقم التسلسلي:</span><span class="value" style="direction:ltr;text-align:right;"><?= e($registration['registration_serial'] ?? '—') ?></span></div>
</div>
<div style="clear:both;"></div>
<!-- Activity Info -->
<?php if ($group): ?>
<div style="margin:15px 0;padding:12px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:6px;">
<strong>النشاط:</strong> <?= e($group['discipline_name'] ?? '') ?><?= e($group['name_ar']) ?>
<?php if (!empty($group['program_name'])): ?><br><strong>البرنامج:</strong> <?= e($group['program_name']) ?><?php endif; ?>
</div>
<?php endif; ?>
<!-- Fee Table -->
<table class="fee-table">
<thead>
<tr><th>البند</th><th style="width:120px;">المبلغ</th></tr>
</thead>
<tbody>
<tr><td>رسوم التسجيل (<?= $registration['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?>)</td><td><?= money((float) $registration['registration_fee']) ?></td></tr>
<tr><td>رسوم الكارت</td><td><?= money((float) $registration['card_fee']) ?></td></tr>
<tr><td>رسوم الاستمارة</td><td><?= money((float) $registration['form_fee']) ?></td></tr>
<?php if ((float) ($registration['subscription_amount'] ?? 0) > 0): ?>
<tr><td>اشتراك النشاط (شهري)</td><td><?= money((float) $registration['subscription_amount']) ?></td></tr>
<?php endif; ?>
<tr class="total-row"><td>الإجمالي</td><td><?= money((float) $registration['total_fees']) ?></td></tr>
</tbody>
</table>
<!-- Signatures -->
<div class="signatures">
<div><div class="sig-box">توقيع اللاعب / ولي الأمر</div></div>
<div><div class="sig-box">مسؤول التسجيل</div></div>
<div><div class="sig-box">مدير النشاط الرياضي</div></div>
</div>
<!-- QR -->
<div class="qr-section"><?= $qrSvg ?></div>
<!-- Date -->
<div style="position:absolute;bottom:20mm;right:20mm;font-size:12px;color:#6B7280;">
التاريخ: <?= date('Y-m-d') ?>
</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/registration" 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'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;align-items:end;gap:15px;flex-wrap:wrap;">
<div class="form-group" style="margin:0;min-width:150px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="from" value="<?= e($dateFrom) ?>" class="form-input" style="direction:ltr;">
</div>
<div class="form-group" style="margin:0;min-width:150px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="to" value="<?= e($dateTo) ?>" class="form-input" style="direction:ltr;">
</div>
<button type="submit" class="btn btn-primary" style="padding:9px 20px;"><i data-lucide="filter" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> عرض</button>
</form>
</div>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));gap:15px;margin-bottom:20px;">
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">إجمالي التسجيلات</div>
<div style="font-size:22px;font-weight:800;color:#1A1A2E;margin-top:4px;"><?= (int) ($summary['total_registrations'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">مكتمل</div>
<div style="font-size:22px;font-weight:800;color:#059669;margin-top:4px;"><?= (int) ($summary['completed'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">بانتظار الدفع</div>
<div style="font-size:22px;font-weight:800;color:#D97706;margin-top:4px;"><?= (int) ($summary['pending'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">ملغى</div>
<div style="font-size:22px;font-weight:800;color:#DC2626;margin-top:4px;"><?= (int) ($summary['cancelled'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">إجمالي الإيرادات</div>
<div style="font-size:22px;font-weight:800;color:#2563EB;margin-top:4px;"><?= money((float) ($summary['total_revenue'] ?? 0)) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">أعضاء</div>
<div style="font-size:22px;font-weight:800;color:#7C3AED;margin-top:4px;"><?= (int) ($summary['member_count'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">غير أعضاء</div>
<div style="font-size:22px;font-weight:800;color:#6B7280;margin-top:4px;"><?= (int) ($summary['nonmember_count'] ?? 0) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">كروت صادرة</div>
<div style="font-size:22px;font-weight:800;color:#059669;margin-top:4px;"><?= (int) ($summary['cards_issued'] ?? 0) ?></div>
</div>
</div>
<!-- Daily Breakdown -->
<?php if (!empty($dailyBreakdown)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;">التفصيل اليومي</h3>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">التاريخ</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">عدد التسجيلات</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">الإيرادات</th>
</tr>
</thead>
<tbody>
<?php foreach ($dailyBreakdown as $day): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= e($day['reg_date']) ?></td>
<td style="padding:10px 15px;"><?= (int) $day['total'] ?></td>
<td style="padding:10px 15px;font-weight:600;color:#059669;"><?= money((float) $day['revenue']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
This diff is collapsed.
...@@ -17,23 +17,26 @@ MenuRegistry::register('sports_activity', [ ...@@ -17,23 +17,26 @@ MenuRegistry::register('sports_activity', [
'order' => 395, 'order' => 395,
'children' => [ 'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/sa', 'permission' => 'sa.dashboard', 'order' => 1], ['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/sa', 'permission' => 'sa.dashboard', 'order' => 1],
['label_ar' => 'المراية', 'label_en' => 'Mirror', 'route' => '/sa/mirror', 'permission' => 'sa.mirror.view', 'order' => 2], ['label_ar' => 'مكتب التسجيل', 'label_en' => 'Registration', 'route' => '/sa/registration', 'permission' => 'sa.registration.manage','order' => 2],
['label_ar' => 'شبكة حمام السباحة', 'label_en' => 'Pool Grid', 'route' => '/sa/pool-grid', 'permission' => 'sa.pool-grid.manage', 'order' => 3], ['label_ar' => 'البوابة', 'label_en' => 'Gate', 'route' => '/sa/gate', 'permission' => 'sa.gate.view', 'order' => 3],
['label_ar' => 'الألعاب', 'label_en' => 'Disciplines', 'route' => '/sa/disciplines', 'permission' => 'sa.discipline.view', 'order' => 3], ['label_ar' => 'المراية', 'label_en' => 'Mirror', 'route' => '/sa/mirror', 'permission' => 'sa.mirror.view', 'order' => 4],
['label_ar' => 'المرافق', 'label_en' => 'Facilities', 'route' => '/sa/facilities', 'permission' => 'sa.facility.view', 'order' => 4], ['label_ar' => 'شبكة حمام السباحة', 'label_en' => 'Pool Grid', 'route' => '/sa/pool-grid', 'permission' => 'sa.pool-grid.manage', 'order' => 5],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/sa/coaches', 'permission' => 'sa.coach.view', 'order' => 5], ['label_ar' => 'الألعاب', 'label_en' => 'Disciplines', 'route' => '/sa/disciplines', 'permission' => 'sa.discipline.view', 'order' => 6],
['label_ar' => 'اللاعبين', 'label_en' => 'Players', 'route' => '/sa/players', 'permission' => 'sa.player.view', 'order' => 6], ['label_ar' => 'المرافق', 'label_en' => 'Facilities', 'route' => '/sa/facilities', 'permission' => 'sa.facility.view', 'order' => 7],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/sa/academies', 'permission' => 'sa.academy.view', 'order' => 7], ['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/sa/coaches', 'permission' => 'sa.coach.view', 'order' => 8],
['label_ar' => 'البرامج', 'label_en' => 'Programs', 'route' => '/sa/programs', 'permission' => 'sa.program.view', 'order' => 8], ['label_ar' => 'اللاعبين', 'label_en' => 'Players', 'route' => '/sa/players', 'permission' => 'sa.player.view', 'order' => 9],
['label_ar' => 'المجموعات', 'label_en' => 'Groups', 'route' => '/sa/groups', 'permission' => 'sa.group.view', 'order' => 9], ['label_ar' => 'الكروت', 'label_en' => 'Cards', 'route' => '/sa/cards', 'permission' => 'sa.card.view', 'order' => 10],
['label_ar' => 'الجدول', 'label_en' => 'Schedule', 'route' => '/sa/schedule', 'permission' => 'sa.schedule.view', 'order' => 10], ['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/sa/academies', 'permission' => 'sa.academy.view', 'order' => 11],
['label_ar' => 'الحجوزات', 'label_en' => 'Bookings', 'route' => '/sa/bookings', 'permission' => 'sa.booking.view', 'order' => 11], ['label_ar' => 'البرامج', 'label_en' => 'Programs', 'route' => '/sa/programs', 'permission' => 'sa.program.view', 'order' => 12],
['label_ar' => 'التسعير', 'label_en' => 'Pricing', 'route' => '/sa/pricing', 'permission' => 'sa.pricing.view', 'order' => 12], ['label_ar' => 'المجموعات', 'label_en' => 'Groups', 'route' => '/sa/groups', 'permission' => 'sa.group.view', 'order' => 13],
['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.view', 'order' => 13], ['label_ar' => 'الجدول', 'label_en' => 'Schedule', 'route' => '/sa/schedule', 'permission' => 'sa.schedule.view', 'order' => 14],
['label_ar' => 'الحضور', 'label_en' => 'Attendance', 'route' => '/sa/attendance', 'permission' => 'sa.attendance.view', 'order' => 14], ['label_ar' => 'الحجوزات', 'label_en' => 'Bookings', 'route' => '/sa/bookings', 'permission' => 'sa.booking.view', 'order' => 15],
['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 15], ['label_ar' => 'التسعير', 'label_en' => 'Pricing', 'route' => '/sa/pricing', 'permission' => 'sa.pricing.view', 'order' => 16],
['label_ar' => 'اللوكرات', 'label_en' => 'Lockers', 'route' => '/sa/lockers', 'permission' => 'sa.locker.view', 'order' => 16], ['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.view', 'order' => 17],
['label_ar' => 'إيجارات اللوكرات', 'label_en' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 17], ['label_ar' => 'الحضور', 'label_en' => 'Attendance', 'route' => '/sa/attendance', 'permission' => 'sa.attendance.view', 'order' => 18],
['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' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 21],
], ],
]); ]);
...@@ -81,6 +84,13 @@ PermissionRegistry::register('sports_activity', [ ...@@ -81,6 +84,13 @@ PermissionRegistry::register('sports_activity', [
'sa.locker_rental.create' => ['ar' => 'إنشاء إيجار لوكر', 'en' => 'Create Locker Rental'], 'sa.locker_rental.create' => ['ar' => 'إنشاء إيجار لوكر', 'en' => 'Create Locker Rental'],
'sa.locker_rental.manage' => ['ar' => 'إدارة إيجارات اللوكرات', 'en' => 'Manage Locker Rentals'], 'sa.locker_rental.manage' => ['ar' => 'إدارة إيجارات اللوكرات', 'en' => 'Manage Locker Rentals'],
'sa.locker_rental.evict' => ['ar' => 'محضر فض لوكر', 'en' => 'Evict Locker Tenant'], 'sa.locker_rental.evict' => ['ar' => 'محضر فض لوكر', 'en' => 'Evict Locker Tenant'],
'sa.registration.view' => ['ar' => 'عرض التسجيلات', 'en' => 'View Registrations'],
'sa.registration.manage' => ['ar' => 'إدارة التسجيل', 'en' => 'Manage Registrations'],
'sa.card.view' => ['ar' => 'عرض كروت النشاط', 'en' => 'View SA Cards'],
'sa.card.manage' => ['ar' => 'إدارة كروت النشاط', 'en' => 'Manage SA Cards'],
'sa.card.print' => ['ar' => 'طباعة كروت النشاط', 'en' => 'Print SA Cards'],
'sa.gate.view' => ['ar' => 'عرض البوابة', 'en' => 'View Gate Access'],
'sa.gate.scan' => ['ar' => 'مسح البوابة', 'en' => 'Scan Gate Access'],
]); ]);
// ─── Event Listeners ──────────────────────────────────────────────────────── // ─── Event Listeners ────────────────────────────────────────────────────────
...@@ -88,6 +98,15 @@ PermissionRegistry::register('sports_activity', [ ...@@ -88,6 +98,15 @@ PermissionRegistry::register('sports_activity', [
EventBus::listen('payment_request.completed', function (array $data): void { EventBus::listen('payment_request.completed', function (array $data): void {
$entityType = $data['related_entity_type'] ?? ''; $entityType = $data['related_entity_type'] ?? '';
try { try {
if ($entityType === 'sa_registrations') {
$registrationId = (int) ($data['related_entity_id'] ?? 0);
$paymentId = (int) ($data['payment_id'] ?? 0);
if ($registrationId > 0) {
\App\Modules\SportsActivity\Services\RegistrationWizardService::completeRegistration($registrationId, $paymentId);
}
return;
}
$db = App::getInstance()->db(); $db = App::getInstance()->db();
if ($entityType === 'sa_subscriptions') { if ($entityType === 'sa_subscriptions') {
$db->update('sa_subscriptions', [ $db->update('sa_subscriptions', [
......
This diff is collapsed.
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `sa_registrations` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`registration_number` VARCHAR(30) NOT NULL,
`player_id` BIGINT UNSIGNED NOT NULL,
`player_type` VARCHAR(20) NOT NULL COMMENT 'member, non_member',
`member_id` BIGINT UNSIGNED NULL,
`national_id` VARCHAR(20) NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'in_progress' COMMENT 'in_progress, pending_payment, completed, cancelled',
`registration_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`card_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`form_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`total_fees` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`group_id` BIGINT UNSIGNED NULL,
`subscription_amount` DECIMAL(10,2) NULL,
`photo_captured` TINYINT(1) NOT NULL DEFAULT 0,
`form_printed` TINYINT(1) NOT NULL DEFAULT 0,
`card_generated` TINYINT(1) NOT NULL DEFAULT 0,
`payment_request_id` BIGINT UNSIGNED NULL,
`payment_status` VARCHAR(20) NOT NULL DEFAULT 'unpaid' COMMENT 'unpaid, pending, paid',
`completed_at` TIMESTAMP NULL,
`cancelled_at` TIMESTAMP NULL,
`cancel_reason` VARCHAR(500) NULL,
`branch_id` BIGINT UNSIGNED NULL,
`created_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uq_sa_reg_number` (`registration_number`),
INDEX `idx_sa_reg_player` (`player_id`),
INDEX `idx_sa_reg_status` (`status`),
INDEX `idx_sa_reg_date` (`created_at`),
INDEX `idx_sa_reg_payment` (`payment_status`),
CONSTRAINT `fk_sa_reg_player` FOREIGN KEY (`player_id`) REFERENCES `sa_players`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `sa_player_cards` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`card_number` VARCHAR(30) NOT NULL,
`player_id` BIGINT UNSIGNED NOT NULL,
`registration_id` BIGINT UNSIGNED NULL,
`card_type` VARCHAR(20) NOT NULL DEFAULT 'standard' COMMENT 'standard, temporary',
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, temporary, expired, suspended, revoked',
`qr_code_data` VARCHAR(200) NULL,
`valid_from` DATE NOT NULL,
`valid_until` DATE NULL COMMENT 'NULL = no expiry; temporary = +7 days',
`photo_path` VARCHAR(500) NULL,
`issued_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`issued_by` BIGINT UNSIGNED NULL,
`suspended_at` TIMESTAMP NULL,
`suspended_by` BIGINT UNSIGNED NULL,
`suspend_reason` VARCHAR(500) NULL,
`revoked_at` TIMESTAMP NULL,
`revoked_by` BIGINT UNSIGNED NULL,
`revoke_reason` VARCHAR(500) NULL,
`print_count` INT NOT NULL DEFAULT 0,
`last_printed_at` TIMESTAMP NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uq_sa_card_number` (`card_number`),
INDEX `idx_sa_card_player` (`player_id`),
INDEX `idx_sa_card_status` (`status`),
INDEX `idx_sa_card_valid` (`valid_until`),
INDEX `idx_sa_card_registration` (`registration_id`),
CONSTRAINT `fk_sa_card_player` FOREIGN KEY (`player_id`) REFERENCES `sa_players`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `sa_gate_access_log` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`card_id` BIGINT UNSIGNED NULL,
`access_type` VARCHAR(10) NOT NULL COMMENT 'entry, exit',
`access_point` VARCHAR(100) NULL,
`granted` TINYINT(1) NOT NULL DEFAULT 1,
`denial_reason` VARCHAR(200) NULL,
`scanned_data` VARCHAR(200) NULL,
`recorded_by` BIGINT UNSIGNED NULL,
`recorded_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_sa_gate_player` (`player_id`),
INDEX `idx_sa_gate_card` (`card_id`),
INDEX `idx_sa_gate_date` (`recorded_at`),
INDEX `idx_sa_gate_granted` (`granted`),
CONSTRAINT `fk_sa_gate_player` FOREIGN KEY (`player_id`) REFERENCES `sa_players`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "
DROP TABLE IF EXISTS `sa_gate_access_log`;
DROP TABLE IF EXISTS `sa_player_cards`;
DROP TABLE IF EXISTS `sa_registrations`
",
];
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$configs = [
['sa.registration_fee_member', '100.00', 'رسوم تسجيل النشاط الرياضي - عضو'],
['sa.registration_fee_nonmember', '50.00', 'رسوم تسجيل النشاط الرياضي - غير عضو'],
['sa.card_fee', '25.00', 'رسوم كارت النشاط الرياضي'],
['sa.form_fee', '10.00', 'رسوم استمارة النشاط الرياضي'],
['sa.temp_card_days', '7', 'مدة الكارت المؤقت بالأيام'],
];
foreach ($configs as [$key, $value, $desc]) {
$existing = $db->selectOne(
"SELECT id FROM system_config WHERE config_key = ?",
[$key]
);
if (!$existing) {
$db->insert('system_config', [
'config_key' => $key,
'config_value' => $value,
'description_ar' => $desc,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment