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,
]);
}
}
<?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\Core\Pagination;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\SportsActivity\Services\RegistrationWizardService;
use App\Modules\Carnets\Services\QRCodeGenerator;
use App\Modules\Settings\Services\BrandingService;
class RegistrationWizardController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$recentRegistrations = $db->select(
"SELECT r.*, p.full_name_ar, p.photo_path, g.name_ar as group_name
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
LEFT JOIN sa_groups g ON g.id = r.group_id
WHERE r.status != 'cancelled'
ORDER BY r.created_at DESC
LIMIT 10"
);
return $this->view('SportsActivity.Views.registration.wizard', [
'recentRegistrations' => $recentRegistrations,
]);
}
public function lookupPlayer(Request $request): Response
{
$nationalId = trim((string) $request->post('national_id', ''));
$playerType = trim((string) $request->post('player_type', 'non_member'));
$memberId = (int) $request->post('member_id', 0);
$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;
if ($nationalId !== '' && strlen($nationalId) === 14) {
$nidParsed = NationalIdParser::parse($nationalId);
}
if ($playerType === 'member' && $memberId > 0) {
$db = App::getInstance()->db();
$member = $db->selectOne(
"SELECT full_name_ar, full_name_en, national_id, date_of_birth, gender, phone
FROM members WHERE membership_number = ? AND is_archived = 0",
[$memberId]
);
if (!$member) {
$member = $db->selectOne(
"SELECT full_name_ar, full_name_en, national_id, date_of_birth, gender, phone
FROM members WHERE id = ? AND is_archived = 0",
[$memberId]
);
}
if ($member) {
$fullNameAr = $fullNameAr ?: ($member['full_name_ar'] ?? '');
$fullNameEn = $fullNameEn ?: ($member['full_name_en'] ?? '');
$nationalId = $nationalId ?: ($member['national_id'] ?? '');
$phone = $phone ?: ($member['phone'] ?? '');
if ($nationalId !== '' && strlen($nationalId) === 14 && !$nidParsed) {
$nidParsed = NationalIdParser::parse($nationalId);
}
} elseif ($fullNameAr === '') {
return $this->json(['success' => false, 'error' => 'العضو غير موجود']);
}
}
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(array_merge($result, [
'nid_parsed' => $nidParsed,
'redirect' => '/sa/registration/' . $result['registration_id'],
]));
}
public function wizardStep(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
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/registration')->withError('التسجيل غير موجود');
}
$groups = $db->select(
"SELECT g.*, p.name_ar as program_name, d.name_ar as discipline_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
WHERE g.status = 'active' AND g.is_archived = 0 AND g.is_full = 0
ORDER BY d.name_ar ASC, g.name_ar ASC"
);
$selectedGroup = null;
if ($registration['group_id']) {
$selectedGroup = $db->selectOne(
"SELECT g.*, p.name_ar as program_name, d.name_ar as discipline_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
WHERE g.id = ?",
[(int) $registration['group_id']]
);
}
return $this->view('SportsActivity.Views.registration.wizard', [
'registration' => $registration,
'groups' => $groups,
'selectedGroup' => $selectedGroup,
'step' => $this->determineStep($registration),
]);
}
public function uploadPhoto(Request $request, string $id): Response
{
$registrationId = (int) $id;
$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' => 'لم يتم رفع صورة']);
}
}
$result = RegistrationWizardService::capturePhoto($registrationId, $file);
return $this->json($result);
}
public function selectActivity(Request $request, string $id): Response
{
$registrationId = (int) $id;
$groupId = (int) $request->post('group_id', 0);
if ($groupId <= 0) {
return $this->json(['success' => false, 'error' => 'اختر مجموعة']);
}
$result = RegistrationWizardService::selectGroup($registrationId, $groupId);
return $this->json($result);
}
public function submitPayment(Request $request, string $id): Response
{
$registrationId = (int) $id;
$result = RegistrationWizardService::submitToPaymentQueue($registrationId);
return $this->json($result);
}
public function printForm(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.guardian_name, p.guardian_phone, p.guardian_national_id, p.guardian_relationship,
p.registration_serial
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/registration')->withError('التسجيل غير موجود');
}
$group = null;
if ($registration['group_id']) {
$group = $db->selectOne(
"SELECT g.*, p.name_ar as program_name, d.name_ar as discipline_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
WHERE g.id = ?",
[(int) $registration['group_id']]
);
}
$qrSvg = QRCodeGenerator::renderSvg(
QRCodeGenerator::encode('SAR:' . $registration['registration_number']),
120
);
$db->update('sa_registrations', [
'form_printed' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->view('SportsActivity.Views.registration.print_form', [
'registration' => $registration,
'group' => $group,
'qrSvg' => $qrSvg,
]);
}
public function generateCard(Request $request, string $id): Response
{
$result = RegistrationWizardService::generateCard((int) $id);
return $this->json($result);
}
public function printCard(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$registration = $db->selectOne("SELECT * FROM sa_registrations WHERE id = ?", [(int) $id]);
if (!$registration) {
return $this->redirect('/sa/registration')->withError('التسجيل غير موجود');
}
$card = $db->selectOne(
"SELECT c.*, p.full_name_ar, p.full_name_en, p.national_id, p.date_of_birth,
p.player_type, p.phone, p.photo_path as player_photo
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE c.registration_id = ? AND c.is_archived = 0
ORDER BY c.id DESC LIMIT 1",
[(int) $id]
);
if (!$card) {
return $this->redirect('/sa/registration/' . $id)->withError('لم يتم إنشاء الكارت بعد');
}
$group = null;
if ($registration['group_id']) {
$group = $db->selectOne(
"SELECT g.name_ar, d.name_ar as discipline_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
WHERE g.id = ?",
[(int) $registration['group_id']]
);
}
$qrSvg = QRCodeGenerator::renderSvg($card['qr_code_data'] ?? '', 150);
$db->update('sa_player_cards', [
'print_count' => (int) $card['print_count'] + 1,
'last_printed_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $card['id']]);
return $this->view('SportsActivity.Views.cards.print', [
'card' => $card,
'group' => $group,
'qrSvg' => $qrSvg,
]);
}
public function cancel(Request $request, string $id): Response
{
$reason = trim((string) $request->post('reason', ''));
$result = RegistrationWizardService::cancelRegistration((int) $id, $reason);
if (!$result['success']) {
return $this->redirect('/sa/registration/' . $id)->withError($result['error']);
}
return $this->redirect('/sa/registration')->withSuccess('تم إلغاء التسجيل');
}
public function report(Request $request): Response
{
$db = App::getInstance()->db();
$dateFrom = trim((string) $request->get('from', date('Y-m-01')));
$dateTo = trim((string) $request->get('to', date('Y-m-d')));
$summary = $db->selectOne(
"SELECT
COUNT(*) as total_registrations,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
SUM(CASE WHEN status = 'pending_payment' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'completed' THEN total_fees ELSE 0 END) as total_revenue,
SUM(CASE WHEN player_type = 'member' AND status = 'completed' THEN 1 ELSE 0 END) as member_count,
SUM(CASE WHEN player_type = 'non_member' AND status = 'completed' THEN 1 ELSE 0 END) as nonmember_count,
SUM(CASE WHEN card_generated = 1 THEN 1 ELSE 0 END) as cards_issued,
SUM(CASE WHEN form_printed = 1 THEN 1 ELSE 0 END) as forms_printed
FROM sa_registrations
WHERE DATE(created_at) BETWEEN ? AND ?",
[$dateFrom, $dateTo]
);
$dailyBreakdown = $db->select(
"SELECT DATE(created_at) as reg_date,
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN total_fees ELSE 0 END) as revenue
FROM sa_registrations
WHERE DATE(created_at) BETWEEN ? AND ?
GROUP BY DATE(created_at)
ORDER BY reg_date DESC",
[$dateFrom, $dateTo]
);
return $this->view('SportsActivity.Views.registration.report', [
'summary' => $summary,
'dailyBreakdown' => $dailyBreakdown,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
]);
}
private function determineStep(array $registration): int
{
if ($registration['status'] === 'completed') {
return 5;
}
if ($registration['payment_status'] === 'paid') {
return 5;
}
if ($registration['status'] === 'pending_payment') {
return 4;
}
if (!empty($registration['group_id'])) {
return 3;
}
if ((int) $registration['photo_captured'] === 1) {
return 3;
}
return 2;
}
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;
}
$tmpFile = tempnam(sys_get_temp_dir(), 'sa_photo_');
if ($tmpFile === false) {
return null;
}
file_put_contents($tmpFile, $decoded);
return [
'tmp_name' => $tmpFile,
'error' => UPLOAD_ERR_OK,
'size' => strlen($decoded),
'name' => 'webcam_capture.jpg',
'type' => 'image/jpeg',
];
}
}
<?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\Core\Pagination;
use App\Modules\Carnets\Services\QRCodeGenerator;
class SaCardController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$search = trim((string) $request->get('q', ''));
$statusFilter = trim((string) $request->get('status', ''));
$typeFilter = trim((string) $request->get('card_type', ''));
$page = max(1, (int) $request->get('page', 1));
$perPage = 25;
$where = 'c.is_archived = 0';
$params = [];
if ($search !== '') {
$where .= ' AND (p.full_name_ar LIKE ? OR c.card_number LIKE ? OR p.national_id LIKE ?)';
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
}
if ($statusFilter !== '') {
$where .= ' AND c.status = ?';
$params[] = $statusFilter;
}
if ($typeFilter !== '') {
$where .= ' AND c.card_type = ?';
$params[] = $typeFilter;
}
$total = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE {$where}",
$params
)['cnt'];
$pagination = Pagination::paginate($total, $perPage, $page);
$offset = ($pagination['current_page'] - 1) * $perPage;
$cards = $db->select(
"SELECT c.*, p.full_name_ar, p.player_type, p.photo_path as player_photo
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE {$where}
ORDER BY c.created_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return $this->view('SportsActivity.Views.cards.index', [
'cards' => $cards,
'pagination' => $pagination,
'filters' => ['q' => $search, 'status' => $statusFilter, 'card_type' => $typeFilter],
]);
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$card = $db->selectOne(
"SELECT c.*, p.full_name_ar, p.full_name_en, p.national_id, p.player_type,
p.date_of_birth, p.phone, p.photo_path as player_photo,
r.registration_number, r.group_id,
g.name_ar as group_name, d.name_ar as discipline_name
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
LEFT JOIN sa_registrations r ON r.id = c.registration_id
LEFT JOIN sa_groups g ON g.id = r.group_id
LEFT JOIN sa_programs pr ON pr.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = pr.discipline_id
WHERE c.id = ? AND c.is_archived = 0",
[(int) $id]
);
if (!$card) {
return $this->redirect('/sa/cards')->withError('الكارت غير موجود');
}
$accessHistory = $db->select(
"SELECT * FROM sa_gate_access_log WHERE card_id = ? ORDER BY recorded_at DESC LIMIT 20",
[(int) $id]
);
return $this->view('SportsActivity.Views.cards.index', [
'card' => $card,
'accessHistory' => $accessHistory,
'showDetail' => true,
]);
}
public function suspend(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$card = $db->selectOne(
"SELECT * FROM sa_player_cards WHERE id = ? AND status = 'active' AND is_archived = 0",
[(int) $id]
);
if (!$card) {
return $this->redirect('/sa/cards')->withError('الكارت غير موجود أو لا يمكن إيقافه');
}
$reason = trim((string) $request->post('reason', ''));
$db->update('sa_player_cards', [
'status' => 'suspended',
'suspended_at' => date('Y-m-d H:i:s'),
'suspended_by' => $employee ? (int) $employee->id : null,
'suspend_reason' => $reason ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
$db->update('sa_players', [
'card_status' => 'suspended',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $card['player_id']]);
return $this->redirect('/sa/cards/' . $id)->withSuccess('تم إيقاف الكارت');
}
public function revoke(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$card = $db->selectOne(
"SELECT * FROM sa_player_cards WHERE id = ? AND status IN ('active','suspended','temporary') AND is_archived = 0",
[(int) $id]
);
if (!$card) {
return $this->redirect('/sa/cards')->withError('الكارت غير موجود أو لا يمكن إلغاؤه');
}
$reason = trim((string) $request->post('reason', ''));
$db->update('sa_player_cards', [
'status' => 'revoked',
'revoked_at' => date('Y-m-d H:i:s'),
'revoked_by' => $employee ? (int) $employee->id : null,
'revoke_reason' => $reason ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
$db->update('sa_players', [
'card_status' => 'revoked',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $card['player_id']]);
return $this->redirect('/sa/cards/' . $id)->withSuccess('تم إلغاء الكارت');
}
public function reactivate(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$card = $db->selectOne(
"SELECT * FROM sa_player_cards WHERE id = ? AND status = 'suspended' AND is_archived = 0",
[(int) $id]
);
if (!$card) {
return $this->redirect('/sa/cards')->withError('الكارت غير موجود أو لا يمكن تفعيله');
}
$db->update('sa_player_cards', [
'status' => 'active',
'suspended_at' => null,
'suspended_by' => null,
'suspend_reason' => null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
$db->update('sa_players', [
'card_status' => 'active',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $card['player_id']]);
return $this->redirect('/sa/cards/' . $id)->withSuccess('تم إعادة تفعيل الكارت');
}
public function print(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$card = $db->selectOne(
"SELECT c.*, p.full_name_ar, p.full_name_en, p.national_id,
p.date_of_birth, p.player_type, p.phone, p.photo_path as player_photo
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE c.id = ? AND c.is_archived = 0",
[(int) $id]
);
if (!$card) {
return $this->redirect('/sa/cards')->withError('الكارت غير موجود');
}
$group = null;
if ($card['registration_id']) {
$reg = $db->selectOne("SELECT group_id FROM sa_registrations WHERE id = ?", [(int) $card['registration_id']]);
if ($reg && $reg['group_id']) {
$group = $db->selectOne(
"SELECT g.name_ar, d.name_ar as discipline_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
WHERE g.id = ?",
[(int) $reg['group_id']]
);
}
}
$qrSvg = QRCodeGenerator::renderSvg($card['qr_code_data'] ?? '', 150);
$db->update('sa_player_cards', [
'print_count' => (int) $card['print_count'] + 1,
'last_printed_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->view('SportsActivity.Views.cards.print', [
'card' => $card,
'group' => $group,
'qrSvg' => $qrSvg,
]);
}
public function report(Request $request): Response
{
$db = App::getInstance()->db();
$summary = $db->selectOne(
"SELECT
COUNT(*) as total_cards,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_cards,
SUM(CASE WHEN status = 'temporary' THEN 1 ELSE 0 END) as temp_cards,
SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as expired_cards,
SUM(CASE WHEN status = 'suspended' THEN 1 ELSE 0 END) as suspended_cards,
SUM(CASE WHEN status = 'revoked' THEN 1 ELSE 0 END) as revoked_cards
FROM sa_player_cards WHERE is_archived = 0"
);
$recentCards = $db->select(
"SELECT c.*, p.full_name_ar, p.player_type
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE c.is_archived = 0
ORDER BY c.created_at DESC
LIMIT 50"
);
return $this->view('SportsActivity.Views.cards.index', [
'cards' => $recentCards,
'pagination' => null,
'filters' => ['q' => '', 'status' => '', 'card_type' => ''],
'summary' => $summary,
'isReport' => true,
]);
}
}
......@@ -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+}/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'],
// ─── 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;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Cashier\Services\PaymentRequestService;
use App\Modules\Carnets\Services\QRCodeGenerator;
use App\Modules\Members\Services\NationalIdParser;
use App\Shared\Services\PhotoUploadService;
final class RegistrationWizardService
{
public static function startRegistration(array $playerData): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$branch = App::getInstance()->currentBranch();
$nationalId = trim($playerData['national_id'] ?? '');
$playerType = $playerData['player_type'] ?? 'non_member';
$memberId = (int) ($playerData['member_id'] ?? 0);
$fullNameAr = trim($playerData['full_name_ar'] ?? '');
$fullNameEn = trim($playerData['full_name_en'] ?? '');
$dateOfBirth = $playerData['date_of_birth'] ?? null;
$gender = $playerData['gender'] ?? null;
$phone = trim($playerData['phone'] ?? '');
$guardianName = trim($playerData['guardian_name'] ?? '');
$guardianPhone = trim($playerData['guardian_phone'] ?? '');
$guardianNationalId = trim($playerData['guardian_national_id'] ?? '');
$guardianRelationship = trim($playerData['guardian_relationship'] ?? '');
if ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $dateOfBirth ?: $parsed['dob'];
$gender = $gender ?: $parsed['gender'];
}
}
$existingPlayer = null;
if ($nationalId !== '') {
$existingPlayer = $db->selectOne(
"SELECT * FROM sa_players WHERE national_id = ? AND is_archived = 0",
[$nationalId]
);
}
if (!$existingPlayer && $playerType === 'member' && $memberId > 0) {
$existingPlayer = $db->selectOne(
"SELECT * FROM sa_players WHERE member_id = ? AND is_archived = 0",
[$memberId]
);
}
if ($existingPlayer) {
$playerId = (int) $existingPlayer['id'];
$updateData = ['updated_at' => date('Y-m-d H:i:s')];
if ($fullNameAr !== '' && $existingPlayer['full_name_ar'] !== $fullNameAr) {
$updateData['full_name_ar'] = $fullNameAr;
}
if ($phone !== '' && ($existingPlayer['phone'] ?? '') !== $phone) {
$updateData['phone'] = $phone;
}
if ($guardianName !== '' && ($existingPlayer['guardian_name'] ?? '') !== $guardianName) {
$updateData['guardian_name'] = $guardianName;
$updateData['guardian_phone'] = $guardianPhone ?: $existingPlayer['guardian_phone'];
}
if (count($updateData) > 1) {
$db->update('sa_players', $updateData, 'id = ?', [$playerId]);
}
} else {
if ($fullNameAr === '') {
return ['success' => false, 'error' => 'الاسم بالعربي مطلوب'];
}
$maxSerial = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(registration_serial, 5) AS UNSIGNED)) as max_num FROM sa_players WHERE registration_serial LIKE 'SAP-%'"
);
$nextNum = ((int) ($maxSerial['max_num'] ?? 0)) + 1;
$serialNumber = 'SAP-' . str_pad((string) $nextNum, 5, '0', STR_PAD_LEFT);
$playerId = $db->insert('sa_players', [
'registration_serial' => $serialNumber,
'full_name_ar' => $fullNameAr,
'full_name_en' => $fullNameEn ?: null,
'player_type' => $playerType,
'member_id' => $memberId > 0 ? $memberId : null,
'national_id' => $nationalId ?: null,
'date_of_birth' => $dateOfBirth ?: null,
'gender' => $gender ?: null,
'phone' => $phone ?: null,
'guardian_name' => $guardianName ?: null,
'guardian_phone' => $guardianPhone ?: null,
'guardian_national_id' => $guardianNationalId ?: null,
'guardian_relationship' => $guardianRelationship ?: null,
'medical_status' => 'pending',
'card_status' => 'inactive',
'branch_id' => $branch ? (int) $branch['id'] : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
}
$fees = self::calculateFees($playerType);
$registrationNumber = self::generateRegistrationNumber();
$registrationId = $db->insert('sa_registrations', [
'registration_number' => $registrationNumber,
'player_id' => $playerId,
'player_type' => $playerType,
'member_id' => $memberId > 0 ? $memberId : null,
'national_id' => $nationalId ?: null,
'status' => 'in_progress',
'registration_fee' => $fees['registration_fee'],
'card_fee' => $fees['card_fee'],
'form_fee' => $fees['form_fee'],
'total_fees' => $fees['total_fees'],
'branch_id' => $branch ? (int) $branch['id'] : null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
return [
'success' => true,
'registration_id' => $registrationId,
'registration_number' => $registrationNumber,
'player_id' => $playerId,
'fees' => $fees,
];
}
public static function capturePhoto(int $registrationId, array $file): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT * FROM sa_registrations WHERE id = ? AND status = 'in_progress'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل'];
}
$playerId = (int) $registration['player_id'];
$result = PhotoUploadService::upload($file, 'sa_players', $playerId);
if (!$result) {
return ['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 = ?', [$registrationId]);
return [
'success' => true,
'photo_path' => $result['path'],
'thumbnail' => $result['thumbnail_path'],
];
}
public static function selectGroup(int $registrationId, int $groupId): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT * FROM sa_registrations WHERE id = ? AND status = 'in_progress'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل'];
}
$group = $db->selectOne(
"SELECT * FROM sa_groups WHERE id = ? AND status = 'active' AND is_archived = 0",
[$groupId]
);
if (!$group) {
return ['success' => false, 'error' => 'المجموعة غير موجودة أو غير نشطة'];
}
if ((int) $group['current_count'] >= (int) $group['max_capacity']) {
return ['success' => false, 'error' => 'المجموعة ممتلئة'];
}
$playerType = $registration['player_type'];
$subscriptionAmount = $playerType === 'member'
? (float) $group['monthly_fee_member']
: (float) $group['monthly_fee_nonmember'];
$totalFees = (float) $registration['registration_fee']
+ (float) $registration['card_fee']
+ (float) $registration['form_fee']
+ $subscriptionAmount;
$db->update('sa_registrations', [
'group_id' => $groupId,
'subscription_amount' => $subscriptionAmount,
'total_fees' => $totalFees,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
return [
'success' => true,
'group_name' => $group['name_ar'],
'subscription_amount' => $subscriptionAmount,
'total_fees' => $totalFees,
];
}
public static function calculateFees(string $playerType): array
{
$db = App::getInstance()->db();
$regFeeKey = $playerType === 'member' ? 'sa.registration_fee_member' : 'sa.registration_fee_nonmember';
$regFeeRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", [$regFeeKey]);
$cardFeeRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['sa.card_fee']);
$formFeeRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['sa.form_fee']);
$registrationFee = (float) ($regFeeRow['config_value'] ?? ($playerType === 'member' ? '100.00' : '50.00'));
$cardFee = (float) ($cardFeeRow['config_value'] ?? '25.00');
$formFee = (float) ($formFeeRow['config_value'] ?? '10.00');
return [
'registration_fee' => $registrationFee,
'card_fee' => $cardFee,
'form_fee' => $formFee,
'total_fees' => $registrationFee + $cardFee + $formFee,
];
}
public static function submitToPaymentQueue(int $registrationId): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT r.*, p.full_name_ar, p.member_id
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ? AND r.status = 'in_progress'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل'];
}
if ((int) $registration['photo_captured'] === 0) {
return ['success' => false, 'error' => 'يجب التقاط الصورة أولاً'];
}
if (empty($registration['group_id'])) {
return ['success' => false, 'error' => 'يجب اختيار النشاط أولاً'];
}
$totalFees = (float) $registration['total_fees'];
if ($totalFees <= 0) {
return ['success' => false, 'error' => 'إجمالي الرسوم غير صالح'];
}
$memberId = (int) ($registration['member_id'] ?? 0);
$description = 'تسجيل نشاط رياضي — ' . ($registration['full_name_ar'] ?? '');
$result = PaymentRequestService::createRequest([
'member_id' => $memberId,
'payment_type' => 'sports_registration',
'amount' => (string) $totalFees,
'description_ar' => $description,
'related_entity_type' => 'sa_registrations',
'related_entity_id' => $registrationId,
]);
if (!$result['success']) {
return $result;
}
$db->update('sa_registrations', [
'status' => 'pending_payment',
'payment_request_id' => (int) $result['request_id'],
'payment_status' => 'pending',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
return [
'success' => true,
'request_id' => $result['request_id'],
'request_number' => $result['request_number'],
'amount' => $totalFees,
];
}
public static function generateCard(int $registrationId): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$branch = App::getInstance()->currentBranch();
$registration = $db->selectOne(
"SELECT r.*, p.photo_path, p.full_name_ar
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ? AND r.payment_status = 'paid'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو لم يتم الدفع'];
}
$existing = $db->selectOne(
"SELECT id FROM sa_player_cards WHERE registration_id = ? AND is_archived = 0",
[$registrationId]
);
if ($existing) {
return ['success' => true, 'card_id' => (int) $existing['id'], 'already_exists' => true];
}
$cardNumber = self::generateCardNumber();
$qrData = QRCodeGenerator::encode('SA:' . $cardNumber);
$cardId = $db->insert('sa_player_cards', [
'card_number' => $cardNumber,
'player_id' => (int) $registration['player_id'],
'registration_id' => $registrationId,
'card_type' => 'standard',
'status' => 'active',
'qr_code_data' => $qrData,
'valid_from' => date('Y-m-d'),
'valid_until' => null,
'photo_path' => $registration['photo_path'],
'issued_by' => $employee ? (int) $employee->id : null,
'branch_id' => $branch ? (int) $branch['id'] : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$db->update('sa_registrations', [
'card_generated' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
$db->update('sa_players', [
'card_status' => 'active',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $registration['player_id']]);
EventBus::dispatch('sa.card.issued', [
'card_id' => $cardId,
'card_number' => $cardNumber,
'player_id' => (int) $registration['player_id'],
'registration_id' => $registrationId,
]);
return [
'success' => true,
'card_id' => $cardId,
'card_number' => $cardNumber,
];
}
public static function completeRegistration(int $registrationId, ?int $paymentId = null): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT * FROM sa_registrations WHERE id = ?",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود'];
}
$db->update('sa_registrations', [
'status' => 'completed',
'payment_status' => 'paid',
'completed_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
$db->update('sa_players', [
'registration_fee_paid' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $registration['player_id']]);
$groupId = (int) ($registration['group_id'] ?? 0);
if ($groupId > 0) {
$existingEnrollment = $db->selectOne(
"SELECT id FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status IN ('active','pending_payment')",
[$groupId, (int) $registration['player_id']]
);
if (!$existingEnrollment) {
$employeeId = (int) ($registration['created_by'] ?? 0);
$enrollmentId = $db->insert('sa_group_players', [
'group_id' => $groupId,
'player_id' => (int) $registration['player_id'],
'enrolled_at' => date('Y-m-d'),
'status' => 'active',
'activated_by_payment_id' => $paymentId,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employeeId,
]);
$group = $db->selectOne("SELECT current_count, max_capacity FROM sa_groups WHERE id = ?", [$groupId]);
$newCount = (int) $group['current_count'] + 1;
$db->update('sa_groups', [
'current_count' => $newCount,
'is_full' => $newCount >= (int) $group['max_capacity'] ? 1 : 0,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$groupId]);
}
}
EventBus::dispatch('sa.registration.completed', [
'registration_id' => $registrationId,
'player_id' => (int) $registration['player_id'],
'group_id' => $groupId,
'total_fees' => (float) $registration['total_fees'],
]);
return ['success' => true];
}
public static function cancelRegistration(int $registrationId, string $reason = ''): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT * FROM sa_registrations WHERE id = ? AND status IN ('in_progress', 'pending_payment')",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو لا يمكن إلغاؤه'];
}
$db->update('sa_registrations', [
'status' => 'cancelled',
'cancelled_at' => date('Y-m-d H:i:s'),
'cancel_reason' => $reason ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
if (!empty($registration['payment_request_id']) && $registration['payment_status'] === 'pending') {
PaymentRequestService::cancelRequest((int) $registration['payment_request_id'], $reason ?: 'إلغاء التسجيل');
}
return ['success' => true];
}
public static function generateRegistrationNumber(): string
{
$db = App::getInstance()->db();
$year = date('Y');
$prefix = 'SAR-' . $year . '-';
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(registration_number, " . (strlen($prefix) + 1) . ") AS UNSIGNED)) as max_num
FROM sa_registrations WHERE registration_number LIKE ?",
[$prefix . '%']
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return $prefix . str_pad((string) $next, 6, '0', STR_PAD_LEFT);
}
private static function generateCardNumber(): string
{
$db = App::getInstance()->db();
$year = date('Y');
$prefix = 'SAC-' . $year . '-';
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(card_number, " . (strlen($prefix) + 1) . ") AS UNSIGNED)) as max_num
FROM sa_player_cards WHERE card_number LIKE ?",
[$prefix . '%']
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return $prefix . str_pad((string) $next, 6, '0', STR_PAD_LEFT);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= isset($isReport) ? 'تقرير الكروت' : 'كروت النشاط الرياضي' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/registration" class="btn btn-outline"><i data-lucide="user-plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تسجيل جديد</a>
<?php if (!isset($isReport)): ?>
<a href="/sa/cards/report" class="btn btn-outline"><i data-lucide="bar-chart-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> التقرير</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (isset($showDetail) && isset($card)): ?>
<!-- Card Detail View -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:20px;display:flex;gap:20px;align-items:flex-start;">
<div style="width:80px;height:80px;border-radius:10px;overflow:hidden;background:#F3F4F6;flex-shrink:0;">
<?php if ($card['player_photo']): ?>
<img src="/<?= e($card['player_photo']) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9CA3AF;font-size:11px;">لا صورة</div>
<?php endif; ?>
</div>
<div style="flex:1;">
<h3 style="margin:0 0 5px;font-size:18px;"><?= e($card['full_name_ar']) ?></h3>
<div style="font-size:13px;color:#6B7280;">
كارت: <strong style="direction:ltr;"><?= e($card['card_number']) ?></strong>
<?= $card['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?>
<?php if ($card['discipline_name']): ?><?= e($card['discipline_name']) ?><?php endif; ?>
</div>
<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;">
<?php
$statusColors = ['active' => '#059669', 'temporary' => '#F59E0B', 'expired' => '#6B7280', 'suspended' => '#DC2626', 'revoked' => '#7C3AED'];
$statusLabels = ['active' => 'نشط', 'temporary' => 'مؤقت', 'expired' => 'منتهي', 'suspended' => 'موقوف', 'revoked' => 'ملغى'];
$sc = $statusColors[$card['status']] ?? '#6B7280';
?>
<span style="padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600;background:<?= $sc ?>15;color:<?= $sc ?>;"><?= $statusLabels[$card['status']] ?? $card['status'] ?></span>
<span style="font-size:12px;color:#6B7280;">صالح من: <?= e(substr($card['valid_from'], 0, 10)) ?></span>
<?php if ($card['valid_until']): ?><span style="font-size:12px;color:#6B7280;">حتى: <?= e($card['valid_until']) ?></span><?php endif; ?>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<a href="/sa/cards/<?= (int) $card['id'] ?>/print" target="_blank" class="btn btn-outline" style="font-size:12px;padding:6px 12px;"><i data-lucide="printer" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> طباعة</a>
<?php if ($card['status'] === 'active'): ?>
<form method="POST" action="/sa/cards/<?= (int) $card['id'] ?>/suspend" style="display:inline;"><?= csrf_field() ?><button class="btn btn-outline" style="font-size:12px;padding:6px 12px;color:#DC2626;border-color:#DC2626;" onclick="return confirm('إيقاف الكارت؟')">إيقاف</button></form>
<?php endif; ?>
<?php if ($card['status'] === 'suspended'): ?>
<form method="POST" action="/sa/cards/<?= (int) $card['id'] ?>/reactivate" style="display:inline;"><?= csrf_field() ?><button class="btn btn-primary" style="font-size:12px;padding:6px 12px;">تفعيل</button></form>
<?php endif; ?>
<?php if (in_array($card['status'], ['active', 'suspended', 'temporary'])): ?>
<form method="POST" action="/sa/cards/<?= (int) $card['id'] ?>/revoke" style="display:inline;"><?= csrf_field() ?><button class="btn btn-outline" style="font-size:12px;padding:6px 12px;color:#7C3AED;border-color:#7C3AED;" onclick="return confirm('إلغاء الكارت نهائياً؟')">إلغاء</button></form>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($accessHistory)): ?>
<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;">التاريخ</th><th style="padding:8px 15px;text-align:right;">النوع</th><th style="padding:8px 15px;text-align:right;">النتيجة</th></tr></thead>
<tbody>
<?php foreach ($accessHistory as $entry): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:8px 15px;direction:ltr;text-align:right;"><?= e(substr($entry['recorded_at'], 0, 16)) ?></td>
<td style="padding:8px 15px;"><?= $entry['access_type'] === 'entry' ? 'دخول' : 'خروج' ?></td>
<td style="padding:8px 15px;"><?= (int) $entry['granted'] ? '<span style="color:#059669;">✓ مسموح</span>' : '<span style="color:#DC2626;">✗ مرفوض</span>' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php else: ?>
<!-- Cards List -->
<?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_cards'] ?? 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['active_cards'] ?? 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:#F59E0B;"><?= (int) ($summary['temp_cards'] ?? 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:#6B7280;"><?= (int) ($summary['expired_cards'] ?? 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['suspended_cards'] ?? 0) ?></div></div>
</div>
<?php endif; ?>
<!-- Filters -->
<?php if (!isset($isReport)): ?>
<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:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" class="form-input" placeholder="اسم أو رقم كارت...">
</div>
<div class="form-group" style="margin:0;min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">— الكل —</option>
<option value="active" <?= ($filters['status'] ?? '') === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="temporary" <?= ($filters['status'] ?? '') === 'temporary' ? 'selected' : '' ?>>مؤقت</option>
<option value="expired" <?= ($filters['status'] ?? '') === 'expired' ? 'selected' : '' ?>>منتهي</option>
<option value="suspended" <?= ($filters['status'] ?? '') === 'suspended' ? 'selected' : '' ?>>موقوف</option>
<option value="revoked" <?= ($filters['status'] ?? '') === 'revoked' ? 'selected' : '' ?>>ملغى</option>
</select>
</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>
<?php endif; ?>
<!-- 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>
</tr>
</thead>
<tbody>
<?php if (empty($cards)): ?>
<tr><td colspan="6" style="padding:30px;text-align:center;color:#9CA3AF;">لا توجد كروت</td></tr>
<?php else: ?>
<?php foreach ($cards as $c): ?>
<?php
$sc = $statusColors[$c['status']] ?? '#6B7280';
$sl = $statusLabels[$c['status']] ?? $c['status'];
?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:10px 15px;direction:ltr;text-align:right;font-weight:600;"><?= e($c['card_number']) ?></td>
<td style="padding:10px 15px;"><?= e($c['full_name_ar']) ?></td>
<td style="padding:10px 15px;"><?= $c['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?></td>
<td style="padding:10px 15px;"><span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= $sc ?>15;color:<?= $sc ?>;"><?= $sl ?></span></td>
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= e($c['valid_until'] ?? '—') ?></td>
<td style="padding:10px 15px;">
<a href="/sa/cards/<?= (int) $c['id'] ?>" style="color:#2563EB;font-size:12px;">عرض</a>
<a href="/sa/cards/<?= (int) $c['id'] ?>/print" target="_blank" style="color:#6B7280;font-size:12px;margin-right:8px;">طباعة</a>
</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; ?>
<?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();
$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(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مكتب التسجيل الرياضي<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/registration/report" class="btn btn-outline"><i data-lucide="bar-chart-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> التقارير</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (isset($registration)): ?>
<!-- Wizard Steps View -->
<div id="wizardApp" data-registration-id="<?= (int) $registration['id'] ?>" data-step="<?= $step ?? 2 ?>">
<!-- Progress Bar -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<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 = ['الرقم القومي', 'الصورة', 'النشاط', 'الدفع', 'الاستلام'];
foreach ($steps as $i => $label):
$stepNum = $i + 1;
$isActive = $stepNum == ($step ?? 2);
$isComplete = $stepNum < ($step ?? 2);
$bgColor = $isComplete ? '#059669' : ($isActive ? '#2563EB' : '#E5E7EB');
$textColor = ($isComplete || $isActive) ? '#fff' : '#9CA3AF';
?>
<div style="text-align:center;position:relative;z-index:1;">
<div style="width:36px;height:36px;border-radius:50%;background:<?= $bgColor ?>;color:<?= $textColor ?>;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;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 Info Summary -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;display:flex;align-items:center;gap:15px;border-bottom:1px solid #E5E7EB;">
<div style="width:60px;height:60px;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:28px;height:28px;"></i></div>
<?php endif; ?>
</div>
<div style="flex:1;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($registration['full_name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">
<?= $registration['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?>
<?php if ($registration['player_nid']): ?><?= e($registration['player_nid']) ?><?php endif; ?>
</div>
</div>
<div style="text-align:left;">
<span style="font-size:11px;color:#6B7280;">رقم التسجيل</span>
<div style="font-size:13px;font-weight:600;direction:ltr;"><?= e($registration['registration_number']) ?></div>
</div>
</div>
</div>
<!-- Step 2: Photo Capture -->
<div id="step-photo" class="wizard-step" style="<?= ($step ?? 2) == 2 ? '' : 'display:none;' ?>">
<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="camera" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> التقاط الصورة</h3>
</div>
<div style="padding:20px;text-align:center;">
<div id="photoPreview" style="width:200px;height:200px;border-radius:12px;background:#F3F4F6;margin:0 auto 15px;overflow:hidden;display:flex;align-items:center;justify-content:center;">
<?php if ($registration['photo_path']): ?>
<img src="/<?= e($registration['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i data-lucide="camera" style="width:48px;height:48px;color:#D1D5DB;"></i>
<?php endif; ?>
</div>
<video id="webcamVideo" style="display:none;width:200px;height:200px;border-radius:12px;object-fit:cover;margin:0 auto 15px;"></video>
<canvas id="webcamCanvas" style="display:none;"></canvas>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
<button type="button" id="btnStartWebcam" class="btn btn-outline"><i data-lucide="video" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> كاميرا</button>
<button type="button" id="btnCaptureWebcam" class="btn btn-primary" style="display:none;"><i data-lucide="camera" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> التقاط</button>
<label class="btn btn-outline" style="cursor:pointer;"><i data-lucide="upload" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> رفع ملف<input type="file" id="photoFileInput" accept="image/*" style="display:none;"></label>
</div>
<div id="photoStatus" style="margin-top:10px;font-size:12px;display:none;"></div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="button" id="btnPhotoNext" class="btn btn-primary" <?= (int) $registration['photo_captured'] ? '' : 'disabled' ?>><i data-lucide="arrow-left" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> التالي — اختيار النشاط</button>
</div>
</div>
<!-- Step 3: Activity/Group Selection -->
<div id="step-activity" class="wizard-step" style="<?= ($step ?? 2) == 3 ? '' : 'display:none;' ?>">
<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="trophy" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> اختيار النشاط والمجموعة</h3>
</div>
<div style="padding:20px;">
<div style="margin-bottom:15px;">
<input type="text" id="groupSearch" class="form-input" placeholder="ابحث عن نشاط أو مجموعة..." style="max-width:400px;">
</div>
<div id="groupsList" style="display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:12px;">
<?php foreach ($groups as $g): ?>
<div class="group-option" data-group-id="<?= (int) $g['id'] ?>" data-name="<?= e($g['name_ar'] . ' ' . ($g['discipline_name'] ?? '')) ?>"
style="border:2px solid <?= (isset($selectedGroup) && (int)$selectedGroup['id'] === (int)$g['id']) ? '#2563EB' : '#E5E7EB' ?>;border-radius:10px;padding:14px;cursor:pointer;transition:border-color 0.2s;">
<div style="font-weight:600;font-size:14px;color:#1A1A2E;"><?= e($g['name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:3px;"><?= e($g['discipline_name'] ?? '') ?><?= e($g['program_name'] ?? '') ?></div>
<div style="display:flex;justify-content:space-between;margin-top:8px;font-size:12px;">
<span style="color:#6B7280;">السعة: <?= (int) $g['current_count'] ?>/<?= (int) $g['max_capacity'] ?></span>
<span style="color:#059669;font-weight:600;">
<?= $registration['player_type'] === 'member' ? money((float) $g['monthly_fee_member']) : money((float) $g['monthly_fee_nonmember']) ?> / شهر
</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Fee Breakdown -->
<div class="card" style="margin-bottom:20px;" id="feeBreakdown" style="<?= $selectedGroup ? '' : 'display:none;' ?>">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;">تفاصيل الرسوم</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;">رسوم التسجيل</td><td style="text-align:left;font-weight:600;" id="feeReg"><?= money((float) $registration['registration_fee']) ?></td></tr>
<tr><td style="padding:6px 0;">رسوم الكارت</td><td style="text-align:left;font-weight:600;" id="feeCard"><?= money((float) $registration['card_fee']) ?></td></tr>
<tr><td style="padding:6px 0;">رسوم الاستمارة</td><td style="text-align:left;font-weight:600;" id="feeForm"><?= money((float) $registration['form_fee']) ?></td></tr>
<tr><td style="padding:6px 0;">اشتراك النشاط</td><td style="text-align:left;font-weight:600;" id="feeSub"><?= money((float) ($registration['subscription_amount'] ?? 0)) ?></td></tr>
<tr style="border-top:2px solid #E5E7EB;"><td style="padding:10px 0;font-weight:700;font-size:15px;">الإجمالي</td><td style="text-align:left;font-weight:800;font-size:15px;color:#2563EB;" id="feeTotal"><?= money((float) $registration['total_fees']) ?></td></tr>
</table>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="button" id="btnActivityNext" class="btn btn-primary" <?= $selectedGroup ? '' : 'disabled' ?>><i data-lucide="banknote" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إرسال للخزينة</button>
<button type="button" class="btn btn-outline" onclick="showStep('photo')"><i data-lucide="arrow-right" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> رجوع</button>
</div>
</div>
<!-- Step 4: Pending Payment -->
<div id="step-payment" class="wizard-step" style="<?= ($step ?? 2) == 4 ? '' : 'display:none;' ?>">
<div class="card" style="margin-bottom:20px;">
<div style="padding:40px;text-align:center;">
<div style="width:80px;height:80px;border-radius:50%;background:#FEF3C7;margin:0 auto 15px;display:flex;align-items:center;justify-content:center;">
<i data-lucide="clock" style="width:40px;height:40px;color:#D97706;"></i>
</div>
<h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#1A1A2E;">بانتظار الدفع في الخزينة</h3>
<p style="color:#6B7280;font-size:14px;margin:0;">تم إرسال طلب الدفع — ينتظر التحصيل من الخزينة</p>
<div style="margin-top:15px;font-size:20px;font-weight:800;color:#2563EB;"><?= money((float) $registration['total_fees']) ?></div>
</div>
</div>
<div style="display:flex;gap:10px;">
<a href="/sa/registration/<?= (int) $registration['id'] ?>/print-form" target="_blank" class="btn btn-outline"><i data-lucide="printer" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> طباعة الاستمارة</a>
<form method="POST" action="/sa/registration/<?= (int) $registration['id'] ?>/cancel" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل تريد إلغاء التسجيل؟')">إلغاء التسجيل</button>
</form>
</div>
</div>
<!-- Step 5: Complete -->
<div id="step-complete" class="wizard-step" style="<?= ($step ?? 2) == 5 ? '' : 'display:none;' ?>">
<div class="card" style="margin-bottom:20px;">
<div style="padding:40px;text-align:center;">
<div style="width:80px;height:80px;border-radius:50%;background:#ECFDF5;margin:0 auto 15px;display:flex;align-items:center;justify-content:center;">
<i data-lucide="check-circle" style="width:40px;height:40px;color:#059669;"></i>
</div>
<h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#059669;">تم التسجيل بنجاح!</h3>
<p style="color:#6B7280;font-size:14px;margin:0;">تم الدفع وتفعيل التسجيل</p>
</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<a href="/sa/registration/<?= (int) $registration['id'] ?>/print-form" target="_blank" class="btn btn-outline"><i data-lucide="file-text" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> طباعة الاستمارة</a>
<button type="button" id="btnGenerateCard" class="btn btn-primary" <?= (int) $registration['card_generated'] ? 'disabled' : '' ?>><i data-lucide="credit-card" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إنشاء الكارت</button>
<?php if ((int) $registration['card_generated']): ?>
<a href="/sa/registration/<?= (int) $registration['id'] ?>/print-card" target="_blank" class="btn btn-outline"><i data-lucide="printer" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> طباعة الكارت</a>
<?php endif; ?>
<a href="/sa/registration" class="btn btn-outline"><i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تسجيل جديد</a>
</div>
</div>
</div>
<?php else: ?>
<!-- Start New Registration -->
<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="user-plus" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تسجيل جديد</h3>
</div>
<div style="padding:20px;">
<form id="startForm">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:15px;">
<div>
<label class="form-label">نوع اللاعب <span style="color:#DC2626;">*</span></label>
<select name="player_type" id="regPlayerType" class="form-select" required>
<option value="non_member">غير عضو</option>
<option value="member">عضو</option>
</select>
</div>
<div id="memberIdWrap" style="display:none;">
<label class="form-label">رقم العضوية <span style="color:#DC2626;">*</span></label>
<input type="number" name="member_id" class="form-input" placeholder="رقم العضوية">
</div>
<div>
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" class="form-input" maxlength="14" dir="ltr" placeholder="14 رقم" id="regNid">
<small id="regNidInfo" style="display:none;margin-top:4px;font-size:11px;color:#059669;"></small>
</div>
<div>
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" class="form-input" id="regNameAr">
</div>
<div>
<label class="form-label">الهاتف</label>
<input type="text" name="phone" class="form-input" dir="ltr">
</div>
<div>
<label class="form-label">اسم ولي الأمر</label>
<input type="text" name="guardian_name" class="form-input">
</div>
<div>
<label class="form-label">هاتف ولي الأمر</label>
<input type="text" name="guardian_phone" class="form-input" dir="ltr">
</div>
</div>
<div style="margin-top:20px;">
<button type="submit" class="btn btn-primary" id="btnStartReg"><i data-lucide="play" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> بدء التسجيل</button>
</div>
<div id="startError" style="display:none;margin-top:10px;padding:10px;background:#FEF2F2;border-radius:6px;color:#DC2626;font-size:13px;"></div>
</form>
</div>
</div>
<!-- Recent Registrations -->
<?php if (!empty($recentRegistrations)): ?>
<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>
<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 ($recentRegistrations as $reg): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:10px 15px;"><a href="/sa/registration/<?= (int) $reg['id'] ?>" style="color:#2563EB;font-weight:600;"><?= e($reg['registration_number']) ?></a></td>
<td style="padding:10px 15px;"><?= e($reg['full_name_ar']) ?></td>
<td style="padding:10px 15px;"><?= e($reg['group_name'] ?? '—') ?></td>
<td style="padding:10px 15px;">
<?php
$statusMap = ['in_progress' => ['جاري', '#F59E0B'], 'pending_payment' => ['بانتظار الدفع', '#D97706'], 'completed' => ['مكتمل', '#059669'], 'cancelled' => ['ملغى', '#DC2626']];
$s = $statusMap[$reg['status']] ?? [$reg['status'], '#6B7280'];
?>
<span style="padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= $s[1] ?>15;color:<?= $s[1] ?>;"><?= $s[0] ?></span>
</td>
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= substr($reg['created_at'], 0, 16) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var regId = document.getElementById('wizardApp') ? document.getElementById('wizardApp').dataset.registrationId : null;
var csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
var csrfInput = document.querySelector('input[name="_csrf_token"]');
if (csrfInput) csrfToken = csrfInput.value;
// Start form
var startForm = document.getElementById('startForm');
if (startForm) {
var playerTypeSelect = document.getElementById('regPlayerType');
var memberIdWrap = document.getElementById('memberIdWrap');
playerTypeSelect.addEventListener('change', function() {
memberIdWrap.style.display = this.value === 'member' ? '' : 'none';
});
var regNid = document.getElementById('regNid');
var regNidInfo = document.getElementById('regNidInfo');
if (regNid) {
regNid.addEventListener('input', function() {
this.value = this.value.replace(/\D/g, '');
if (this.value.length === 14) {
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
body: JSON.stringify({national_id: this.value})
}).then(function(r){return r.json();}).then(function(data) {
if (data.parsed && data.parsed.is_valid) {
regNidInfo.style.display = 'block';
regNidInfo.textContent = '✓ ' + data.parsed.governorate_name_ar + ' — ' + data.parsed.age_years + ' سنة — ' + (data.parsed.gender === 'male' ? 'ذكر' : 'أنثى');
} else {
regNidInfo.style.display = 'block';
regNidInfo.style.color = '#DC2626';
regNidInfo.textContent = 'رقم قومي غير صالح';
}
});
} else {
regNidInfo.style.display = 'none';
regNidInfo.style.color = '#059669';
}
});
}
startForm.addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(startForm);
var body = {};
formData.forEach(function(v, k) { body[k] = v; });
body._csrf_token = csrfToken;
document.getElementById('btnStartReg').disabled = true;
fetch('/sa/registration/lookup', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify(body)
}).then(function(r){return r.json();}).then(function(data) {
if (data.success && data.redirect) {
window.location.href = data.redirect;
} else {
document.getElementById('startError').style.display = 'block';
document.getElementById('startError').textContent = data.error || 'حدث خطأ';
document.getElementById('btnStartReg').disabled = false;
}
}).catch(function() {
document.getElementById('btnStartReg').disabled = false;
});
});
}
// Wizard logic
if (!regId) return;
function showStep(name) {
document.querySelectorAll('.wizard-step').forEach(function(el) { el.style.display = 'none'; });
var target = document.getElementById('step-' + name);
if (target) target.style.display = '';
}
window.showStep = showStep;
// Photo: webcam
var video = document.getElementById('webcamVideo');
var canvas = document.getElementById('webcamCanvas');
var btnStart = document.getElementById('btnStartWebcam');
var btnCapture = document.getElementById('btnCaptureWebcam');
var photoPreview = document.getElementById('photoPreview');
var photoStatus = document.getElementById('photoStatus');
var btnPhotoNext = document.getElementById('btnPhotoNext');
var stream = null;
if (btnStart) {
btnStart.addEventListener('click', function() {
navigator.mediaDevices.getUserMedia({video: {facingMode: 'user', width: 400, height: 400}}).then(function(s) {
stream = s;
video.srcObject = s;
video.play();
video.style.display = 'block';
photoPreview.style.display = 'none';
btnCapture.style.display = '';
btnStart.style.display = 'none';
}).catch(function() {
alert('لا يمكن الوصول للكاميرا');
});
});
btnCapture.addEventListener('click', function() {
canvas.width = 400; canvas.height = 400;
canvas.getContext('2d').drawImage(video, 0, 0, 400, 400);
var dataUrl = canvas.toDataURL('image/jpeg', 0.85);
if (stream) { stream.getTracks().forEach(function(t){t.stop();}); stream = null; }
video.style.display = 'none';
btnCapture.style.display = 'none';
btnStart.style.display = '';
photoPreview.innerHTML = '<img src="' + dataUrl + '" style="width:100%;height:100%;object-fit:cover;">';
photoPreview.style.display = '';
uploadPhoto(dataUrl);
});
}
// Photo: file upload
var fileInput = document.getElementById('photoFileInput');
if (fileInput) {
fileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
photoPreview.innerHTML = '<img src="' + e.target.result + '" style="width:100%;height:100%;object-fit:cover;">';
uploadPhoto(e.target.result);
};
reader.readAsDataURL(this.files[0]);
}
});
}
function uploadPhoto(base64) {
photoStatus.style.display = 'block';
photoStatus.style.color = '#6B7280';
photoStatus.textContent = 'جاري رفع الصورة...';
fetch('/sa/registration/' + regId + '/photo', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({photo_base64: base64, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
photoStatus.style.color = '#059669';
photoStatus.textContent = '✓ تم رفع الصورة بنجاح';
btnPhotoNext.disabled = false;
} else {
photoStatus.style.color = '#DC2626';
photoStatus.textContent = data.error || 'فشل الرفع';
}
});
}
if (btnPhotoNext) {
btnPhotoNext.addEventListener('click', function() { showStep('activity'); });
}
// Activity selection
document.querySelectorAll('.group-option').forEach(function(el) {
el.addEventListener('click', function() {
document.querySelectorAll('.group-option').forEach(function(g) { g.style.borderColor = '#E5E7EB'; });
this.style.borderColor = '#2563EB';
var gid = this.dataset.groupId;
fetch('/sa/registration/' + regId + '/activity', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({group_id: gid, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
document.getElementById('feeSub').textContent = data.subscription_amount.toLocaleString() + ' ج.م';
document.getElementById('feeTotal').textContent = data.total_fees.toLocaleString() + ' ج.م';
document.getElementById('feeBreakdown').style.display = '';
document.getElementById('btnActivityNext').disabled = false;
}
});
});
});
// Group search
var groupSearch = document.getElementById('groupSearch');
if (groupSearch) {
groupSearch.addEventListener('input', function() {
var q = this.value.toLowerCase();
document.querySelectorAll('.group-option').forEach(function(el) {
el.style.display = el.dataset.name.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
});
});
}
// Submit to payment
var btnActivityNext = document.getElementById('btnActivityNext');
if (btnActivityNext) {
btnActivityNext.addEventListener('click', function() {
this.disabled = true;
fetch('/sa/registration/' + regId + '/pay', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({_csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
showStep('payment');
} else {
alert(data.error || 'حدث خطأ');
btnActivityNext.disabled = false;
}
});
});
}
// Generate card
var btnGenCard = document.getElementById('btnGenerateCard');
if (btnGenCard) {
btnGenCard.addEventListener('click', function() {
this.disabled = true;
fetch('/sa/registration/' + regId + '/generate-card', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({_csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
window.location.reload();
} else {
alert(data.error || 'حدث خطأ');
btnGenCard.disabled = false;
}
});
});
}
});
</script>
<?php $__template->endSection(); ?>
......@@ -17,23 +17,26 @@ MenuRegistry::register('sports_activity', [
'order' => 395,
'children' => [
['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' => 'Pool Grid', 'route' => '/sa/pool-grid', 'permission' => 'sa.pool-grid.manage', 'order' => 3],
['label_ar' => 'الألعاب', 'label_en' => 'Disciplines', 'route' => '/sa/disciplines', 'permission' => 'sa.discipline.view', 'order' => 3],
['label_ar' => 'المرافق', 'label_en' => 'Facilities', 'route' => '/sa/facilities', 'permission' => 'sa.facility.view', 'order' => 4],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/sa/coaches', 'permission' => 'sa.coach.view', 'order' => 5],
['label_ar' => 'اللاعبين', 'label_en' => 'Players', 'route' => '/sa/players', 'permission' => 'sa.player.view', 'order' => 6],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/sa/academies', 'permission' => 'sa.academy.view', 'order' => 7],
['label_ar' => 'البرامج', 'label_en' => 'Programs', 'route' => '/sa/programs', 'permission' => 'sa.program.view', 'order' => 8],
['label_ar' => 'المجموعات', 'label_en' => 'Groups', 'route' => '/sa/groups', 'permission' => 'sa.group.view', 'order' => 9],
['label_ar' => 'الجدول', 'label_en' => 'Schedule', 'route' => '/sa/schedule', 'permission' => 'sa.schedule.view', 'order' => 10],
['label_ar' => 'الحجوزات', 'label_en' => 'Bookings', 'route' => '/sa/bookings', 'permission' => 'sa.booking.view', 'order' => 11],
['label_ar' => 'التسعير', 'label_en' => 'Pricing', 'route' => '/sa/pricing', 'permission' => 'sa.pricing.view', 'order' => 12],
['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.view', 'order' => 13],
['label_ar' => 'الحضور', 'label_en' => 'Attendance', 'route' => '/sa/attendance', 'permission' => 'sa.attendance.view', 'order' => 14],
['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 15],
['label_ar' => 'اللوكرات', 'label_en' => 'Lockers', 'route' => '/sa/lockers', 'permission' => 'sa.locker.view', 'order' => 16],
['label_ar' => 'إيجارات اللوكرات', 'label_en' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 17],
['label_ar' => 'مكتب التسجيل', 'label_en' => 'Registration', 'route' => '/sa/registration', 'permission' => 'sa.registration.manage','order' => 2],
['label_ar' => 'البوابة', 'label_en' => 'Gate', 'route' => '/sa/gate', 'permission' => 'sa.gate.view', 'order' => 3],
['label_ar' => 'المراية', 'label_en' => 'Mirror', 'route' => '/sa/mirror', 'permission' => 'sa.mirror.view', 'order' => 4],
['label_ar' => 'شبكة حمام السباحة', 'label_en' => 'Pool Grid', 'route' => '/sa/pool-grid', 'permission' => 'sa.pool-grid.manage', 'order' => 5],
['label_ar' => 'الألعاب', 'label_en' => 'Disciplines', 'route' => '/sa/disciplines', 'permission' => 'sa.discipline.view', 'order' => 6],
['label_ar' => 'المرافق', 'label_en' => 'Facilities', 'route' => '/sa/facilities', 'permission' => 'sa.facility.view', 'order' => 7],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/sa/coaches', 'permission' => 'sa.coach.view', 'order' => 8],
['label_ar' => 'اللاعبين', 'label_en' => 'Players', 'route' => '/sa/players', 'permission' => 'sa.player.view', 'order' => 9],
['label_ar' => 'الكروت', 'label_en' => 'Cards', 'route' => '/sa/cards', 'permission' => 'sa.card.view', 'order' => 10],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/sa/academies', 'permission' => 'sa.academy.view', 'order' => 11],
['label_ar' => 'البرامج', 'label_en' => 'Programs', 'route' => '/sa/programs', 'permission' => 'sa.program.view', 'order' => 12],
['label_ar' => 'المجموعات', 'label_en' => 'Groups', 'route' => '/sa/groups', 'permission' => 'sa.group.view', 'order' => 13],
['label_ar' => 'الجدول', 'label_en' => 'Schedule', 'route' => '/sa/schedule', 'permission' => 'sa.schedule.view', 'order' => 14],
['label_ar' => 'الحجوزات', 'label_en' => 'Bookings', 'route' => '/sa/bookings', 'permission' => 'sa.booking.view', 'order' => 15],
['label_ar' => 'التسعير', 'label_en' => 'Pricing', 'route' => '/sa/pricing', 'permission' => 'sa.pricing.view', 'order' => 16],
['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.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', [
'sa.locker_rental.create' => ['ar' => 'إنشاء إيجار لوكر', 'en' => 'Create Locker Rental'],
'sa.locker_rental.manage' => ['ar' => 'إدارة إيجارات اللوكرات', 'en' => 'Manage Locker Rentals'],
'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 ────────────────────────────────────────────────────────
......@@ -88,6 +98,15 @@ PermissionRegistry::register('sports_activity', [
EventBus::listen('payment_request.completed', function (array $data): void {
$entityType = $data['related_entity_type'] ?? '';
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();
if ($entityType === 'sa_subscriptions') {
$db->update('sa_subscriptions', [
......
......@@ -218,6 +218,12 @@ final class TutorialRegistry
'icon' => 'layout-grid',
'color' => '#16A34A',
],
'sa-registration' => [
'title' => 'تسجيل النشاط الرياضي',
'subtitle' => 'معالج التسجيل والكروت والبوابة والتقارير',
'icon' => 'clipboard-check',
'color' => '#0891B2',
],
];
}
......@@ -259,6 +265,7 @@ final class TutorialRegistry
'pricing-management' => self::pricingManagementTutorials(),
'audit' => self::auditTutorials(),
'playgrounds' => self::playgroundsTutorials(),
'sa-registration' => self::saRegistrationTutorials(),
default => [],
};
}
......@@ -1246,6 +1253,39 @@ final class TutorialRegistry
['title' => 'تسجيل الحضور', 'body' => 'اختر الحصة. حدد اللاعبين الحاضرين بعلامة ✓. الغائبين يبقون بدون علامة.'],
['title' => 'الحفظ', 'body' => 'اضغط <span class="field">حفظ الحضور</span>. النسبة تُحسب وتظهر في تقارير المدرب.<span class="info">يمكن تعديل الحضور خلال 24 ساعة.</span>'],
],
// ── SA REGISTRATION ──
'sa-registration.registration-wizard' => [
['title' => 'فتح مكتب التسجيل', 'body' => 'من القائمة: <span class="field">النشاط الرياضي</span> > <span class="field">مكتب التسجيل</span>.'],
['title' => 'إدخال الرقم القومي', 'body' => 'أدخل الرقم القومي (14 رقم) واضغط <span class="field">استعلام</span>. النظام يستخرج تلقائياً: تاريخ الميلاد، العمر، النوع، المحافظة.<span class="info">إذا كان اللاعب مسجلاً مسبقاً — يتم استرجاع بياناته مباشرة.</span>'],
['title' => 'التقاط الصورة', 'body' => 'التقط صورة عبر <span class="field">كاميرا الويب</span> أو ارفع ملف صورة. الصورة إلزامية لإتمام التسجيل.<span class="warn">الصورة تظهر على الاستمارة والكارت — تأكد من جودتها.</span>'],
['title' => 'اختيار النشاط والمجموعة', 'body' => 'اختر المجموعة المناسبة (حسب السن والنشاط). يظهر تلقائياً:<ul><li>رسوم التسجيل (100 ج عضو / 50 ج غير عضو)</li><li>رسوم الكارت (25 ج)</li><li>رسوم الاستمارة (10 ج)</li><li>اشتراك المجموعة الشهري</li></ul>'],
['title' => 'إرسال للخزينة', 'body' => 'اضغط <span class="field">إرسال للخزينة</span>. يتم إنشاء طلب دفع شامل يظهر في طابور التحصيل.<span class="success">بعد السداد يتم تفعيل التسجيل وإصدار الكارت تلقائياً.</span>'],
['title' => 'الطباعة وإصدار الكارت', 'body' => 'بعد السداد يمكنك: <span class="field">طباعة الاستمارة</span> (A4) و<span class="field">طباعة الكارت</span> (PVC). الكارت يحتوي على QR للدخول من البوابة.'],
],
'sa-registration.card-management' => [
['title' => 'فتح إدارة الكروت', 'body' => 'من القائمة: <span class="field">النشاط الرياضي</span> > <span class="field">الكروت</span>.'],
['title' => 'البحث والفلترة', 'body' => 'ابحث بالاسم أو رقم الكارت. فلتر بالحالة: نشط، مؤقت، منتهي، موقوف، ملغى.'],
['title' => 'عرض تفاصيل كارت', 'body' => 'اضغط <span class="field">عرض</span> لمشاهدة: بيانات اللاعب، حالة الكارت، تاريخ الصلاحية، وسجل الدخول.'],
['title' => 'إيقاف أو إلغاء', 'body' => '<span class="field">إيقاف</span>: تعليق مؤقت (يمكن إعادة التفعيل). <span class="field">إلغاء</span>: إلغاء نهائي لا رجعة فيه.<span class="warn">الكارت الموقوف أو الملغي يُرفض تلقائياً عند البوابة.</span>'],
['title' => 'إعادة الطباعة', 'body' => 'اضغط <span class="field">طباعة</span> لإصدار نسخة جديدة من الكارت. النظام يسجل عدد مرات الطباعة.'],
],
'sa-registration.gate-scanning' => [
['title' => 'فتح البوابة', 'body' => 'من القائمة: <span class="field">النشاط الرياضي</span> > <span class="field">البوابة</span>.'],
['title' => 'مسح الكارت', 'body' => 'امسح QR Code بالقارئ أو أدخل رقم الكارت يدوياً. النظام يتحقق فوراً من:<ul><li>صلاحية الكارت (تاريخ الانتهاء)</li><li>حالة الكارت (نشط/موقوف/ملغى)</li><li>حالة اللاعب</li></ul>'],
['title' => 'نتيجة المسح', 'body' => '<span style="color:#059669;font-weight:bold;">✓ مسموح</span>: يظهر اسم اللاعب ورقم الكارت بخلفية خضراء.<br><span style="color:#DC2626;font-weight:bold;">✗ ممنوع</span>: يظهر سبب الرفض بخلفية حمراء.'],
['title' => 'السجل', 'body' => 'اضغط <span class="field">السجل</span> لعرض جميع عمليات المسح مع الفلاتر (التاريخ، النتيجة، البحث).<span class="info">يتم تسجيل كل عملية مسح سواء مسموح أو مرفوض.</span>'],
],
'sa-registration.registration-reports' => [
['title' => 'فتح تقارير التسجيل', 'body' => 'من <span class="field">النشاط الرياضي</span> > <span class="field">مكتب التسجيل</span> > <span class="field">التقارير</span>.'],
['title' => 'اختيار الفترة', 'body' => 'حدد <span class="field">تاريخ البداية</span> و<span class="field">تاريخ النهاية</span>. يظهر ملخص:<ul><li>إجمالي التسجيلات (مكتمل/ملغي/قيد الانتظار)</li><li>إجمالي الإيرادات</li><li>الأعضاء مقابل غير الأعضاء</li><li>الكروت المصدرة</li></ul>'],
['title' => 'التفصيل اليومي', 'body' => 'جدول يومي يعرض عدد التسجيلات والإيرادات لكل يوم في الفترة المحددة.'],
['title' => 'تقارير الكروت', 'body' => 'من <span class="field">الكروت</span> > <span class="field">التقرير</span>: إحصائيات الكروت (نشط/مؤقت/منتهي/موقوف) مع توزيع حسب النوع.'],
],
'sa-registration.temp-card-workflow' => [
['title' => 'الكارت المؤقت', 'body' => 'عند إصدار كارت بنوع <span class="field">مؤقت</span>: الصلاحية 7 أيام فقط من تاريخ الإصدار.'],
['title' => 'الدخول خلال المدة', 'body' => 'اللاعب يستخدم الكارت المؤقت للدخول من البوابة بشكل طبيعي خلال فترة الصلاحية.'],
['title' => 'انتهاء الصلاحية', 'body' => 'بعد 7 أيام يتحول الكارت تلقائياً إلى حالة <span style="color:#6B7280;font-weight:bold;">منتهي</span> ويُرفض عند البوابة.<span class="warn">يجب إصدار كارت دائم قبل انتهاء المؤقت.</span>'],
],
];
}
......@@ -1287,6 +1327,7 @@ final class TutorialRegistry
'pricing-management' => self::pricingManagementCategories(),
'audit' => self::auditCategories(),
'playgrounds' => self::playgroundsCategories(),
'sa-registration' => self::saRegistrationCategories(),
default => [],
};
}
......@@ -3277,4 +3318,64 @@ final class TutorialRegistry
'operations' => ['label' => 'التشغيل', 'icon' => 'calendar', 'color' => '#3B82F6'],
];
}
// ─────────────────────────────────────────────────────────────────────
// SA REGISTRATION
// ─────────────────────────────────────────────────────────────────────
private static function saRegistrationTutorials(): array
{
return [
'registration-wizard' => [
'title' => 'معالج التسجيل',
'subtitle' => 'تسجيل لاعب جديد من البداية حتى إصدار الكارت',
'icon' => 'clipboard-check',
'color' => '#0891B2',
'category' => 'registration',
'order' => 1,
],
'card-management' => [
'title' => 'إدارة الكروت',
'subtitle' => 'عرض وإيقاف وإلغاء وإعادة طباعة كروت اللاعبين',
'icon' => 'id-card',
'color' => '#2563EB',
'category' => 'cards',
'order' => 2,
],
'gate-scanning' => [
'title' => 'مسح البوابة',
'subtitle' => 'التحقق من الكروت عند الدخول والخروج',
'icon' => 'scan',
'color' => '#059669',
'category' => 'gate',
'order' => 3,
],
'registration-reports' => [
'title' => 'تقارير التسجيل',
'subtitle' => 'إحصائيات التسجيلات والإيرادات والكروت',
'icon' => 'bar-chart-3',
'color' => '#7C3AED',
'category' => 'reports',
'order' => 4,
],
'temp-card-workflow' => [
'title' => 'سير عمل الكارت المؤقت',
'subtitle' => 'إصدار كارت مؤقت (7 أيام) وإدارة انتهاء الصلاحية',
'icon' => 'clock',
'color' => '#F59E0B',
'category' => 'cards',
'order' => 5,
],
];
}
private static function saRegistrationCategories(): array
{
return [
'registration' => ['label' => 'التسجيل', 'icon' => 'clipboard-check', 'color' => '#0891B2'],
'cards' => ['label' => 'الكروت', 'icon' => 'id-card', 'color' => '#2563EB'],
'gate' => ['label' => 'البوابة', 'icon' => 'scan', 'color' => '#059669'],
'reports' => ['label' => 'التقارير', 'icon' => 'bar-chart-3', 'color' => '#7C3AED'],
];
}
}
<?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