Commit 85a4f4fb authored by Mahmoud Aglan's avatar Mahmoud Aglan

Sports module overhaul: fix bugs, NID parsing, photo upload, cascading forms, auto-codes

- Fix route URL mismatches in player show.php (card/activate, card/suspend, card/revoke, enroll/drop)
- Fix Sports module permissions (was using temp.view/temp.add, now sports.view/sports.add/sports.convert)
- Add CSRF middleware to Sports POST route
- Fix AcademyEnrollment::getForPlayer() to JOIN academy/level names
- Add NID auto-parsing API and JS to player create/edit (deduces DOB, age, gender, governorate)
- Replace raw ID inputs in enrollment form with cascading dropdowns (academy→level→schedule)
- Add profile photo upload to player create/edit/show with live preview
- Add full player history timeline (enrollments, payments, medical, attendance)
- Add sport→facility cascade filter to reservation create form
- Make all code fields optional with auto-generation (Disciplines, Academies, Facilities, Levels)
- Overhaul Sports create form with discipline dropdown and competitive level select
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e42b17a1
......@@ -62,14 +62,19 @@ class AcademyController extends Controller
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Auto-generate code if empty
if ($code === '') {
$base = $nameEn ?: $nameAr;
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '_', $base));
$code = substr($code, 0, 25) . '_' . time() % 10000;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود الأكاديمية مطلوب';
}
if ($disciplineId <= 0) {
$errors[] = 'النشاط الرياضي مطلوب';
} else {
......@@ -83,13 +88,11 @@ class AcademyController extends Controller
}
// Check unique code
if ($code !== '') {
$existing = Academy::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود الأكاديمية مستخدم بالفعل';
}
$existing = Academy::query()
->where('code', '=', $code)
->first();
if ($existing) {
$code = $code . '_' . bin2hex(random_bytes(2));
}
if (!empty($errors)) {
......@@ -178,14 +181,19 @@ class AcademyController extends Controller
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Auto-generate code if empty
if ($code === '') {
$base = $nameEn ?: $nameAr;
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '_', $base));
$code = substr($code, 0, 25) . '_' . time() % 10000;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود الأكاديمية مطلوب';
}
if ($disciplineId <= 0) {
$errors[] = 'النشاط الرياضي مطلوب';
} else {
......@@ -199,14 +207,12 @@ class AcademyController extends Controller
}
// Check unique code (exclude current)
if ($code !== '') {
$existing = Academy::query()
->where('code', '=', $code)
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود الأكاديمية مستخدم بالفعل';
}
$existing = Academy::query()
->where('code', '=', $code)
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود الأكاديمية مستخدم بالفعل — جرب كود مختلف';
}
if (!empty($errors)) {
......@@ -268,24 +274,25 @@ class AcademyController extends Controller
$ageMax = (int) $request->post('age_max', 99);
$maxCapacity = (int) $request->post('max_capacity', 0);
// Auto-generate code if empty
if ($code === '') {
$code = 'LVL_' . strtoupper(substr(preg_replace('/[^A-Z0-9]/i', '', $nameEn ?: $nameAr), 0, 10)) . '_' . $levelOrder;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'اسم المستوى بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود المستوى مطلوب';
}
// Check unique code within this academy
if ($code !== '') {
$existing = AcademyLevel::query()
->where('academy_id', '=', (int) $id)
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود المستوى مستخدم بالفعل في هذه الأكاديمية';
}
// Ensure unique code within this academy
$existing = AcademyLevel::query()
->where('academy_id', '=', (int) $id)
->where('code', '=', $code)
->first();
if ($existing) {
$code = $code . '_' . bin2hex(random_bytes(2));
}
if (!empty($errors)) {
......
......@@ -29,9 +29,9 @@
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود الأكاديمية <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" id="codeInput" required placeholder="مثال: ACAD_FOOTBALL" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">يتم توليده تلقائيا من الاسم الإنجليزي</small>
<label class="form-label">كود الأكاديمية</label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" id="codeInput" placeholder="يتم توليده تلقائياً" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">اختياري — يتم توليده تلقائياً إن ترك فارغاً</small>
</div>
<div class="form-group">
<label class="form-label">النشاط الرياضي <span style="color:#DC2626;">*</span></label>
......
......@@ -36,8 +36,8 @@ $capacity = $config['capacity'] ?? '';
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود الأكاديمية <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $academy->code) ?>" class="form-input" id="codeInput" required style="direction:ltr;text-align:left;text-transform:uppercase;">
<label class="form-label">كود الأكاديمية</label>
<input type="text" name="code" value="<?= e(old('code') ?: $academy->code) ?>" class="form-input" id="codeInput" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">النشاط الرياضي <span style="color:#DC2626;">*</span></label>
......
......@@ -55,26 +55,29 @@ class DisciplineController extends Controller
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Auto-generate code if empty
if ($code === '') {
$base = $nameEn ?: $nameAr;
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '_', $base));
$code = substr($code, 0, 25) . '_' . time() % 10000;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود النشاط مطلوب';
}
if (!array_key_exists($category, SportDiscipline::getCategories())) {
$errors[] = 'فئة النشاط غير صالحة';
}
// Check unique code
if ($code !== '') {
$existing = SportDiscipline::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود النشاط مستخدم بالفعل';
}
// Ensure unique code
$existing = SportDiscipline::query()
->where('code', '=', $code)
->first();
if ($existing) {
$code = $code . '_' . bin2hex(random_bytes(2));
}
if (!empty($errors)) {
......@@ -154,14 +157,19 @@ class DisciplineController extends Controller
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Auto-generate code if empty
if ($code === '') {
$base = $nameEn ?: $nameAr;
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '_', $base));
$code = substr($code, 0, 25) . '_' . time() % 10000;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود النشاط مطلوب';
}
if (!array_key_exists($category, SportDiscipline::getCategories())) {
$errors[] = 'فئة النشاط غير صالحة';
}
......@@ -173,7 +181,7 @@ class DisciplineController extends Controller
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود النشاط مستخدم بالفعل';
$errors[] = 'كود النشاط مستخدم بالفعل — جرب كود مختلف';
}
}
......
......@@ -29,9 +29,9 @@
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود النشاط <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" id="codeInput" required placeholder="مثال: FOOTBALL" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">يتم توليده تلقائيا من الاسم الإنجليزي</small>
<label class="form-label">كود النشاط</label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" id="codeInput" placeholder="يتم توليده تلقائياً" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">اختياري — يتم توليده تلقائياً إن ترك فارغاً</small>
</div>
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
......
......@@ -36,8 +36,8 @@ $maxPlayers = $config['max_players_per_session'] ?? '';
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود النشاط <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $discipline->code) ?>" class="form-input" id="codeInput" required style="direction:ltr;text-align:left;text-transform:uppercase;">
<label class="form-label">كود النشاط</label>
<input type="text" name="code" value="<?= e(old('code') ?: $discipline->code) ?>" class="form-input" id="codeInput" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
......
......@@ -69,14 +69,18 @@ class FacilityController extends Controller
$disciplineId = $request->post('linked_discipline_id', '');
$disciplineId = $disciplineId !== '' ? (int) $disciplineId : null;
// Auto-generate code if empty
if ($code === '') {
$typePrefix = strtoupper(substr($facilityType ?: 'FAC', 0, 5));
$code = $typePrefix . '_' . strtoupper(substr(preg_replace('/[^A-Z0-9]/i', '', $nameEn ?: $nameAr), 0, 10)) . '_' . time() % 10000;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'اسم المرفق بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود المرفق مطلوب';
}
if (!array_key_exists($facilityType, Facility::getTypes())) {
$errors[] = 'نوع المرفق غير صالح';
}
......@@ -84,14 +88,12 @@ class FacilityController extends Controller
$errors[] = 'الموقع غير صالح';
}
// Check unique code
if ($code !== '') {
$existing = Facility::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود المرفق مستخدم بالفعل';
}
// Ensure unique code
$existing = Facility::query()
->where('code', '=', $code)
->first();
if ($existing) {
$code = $code . '_' . bin2hex(random_bytes(2));
}
if (!empty($errors)) {
......@@ -197,14 +199,18 @@ class FacilityController extends Controller
$disciplineId = $request->post('linked_discipline_id', '');
$disciplineId = $disciplineId !== '' ? (int) $disciplineId : null;
// Auto-generate code if empty
if ($code === '') {
$typePrefix = strtoupper(substr($facilityType ?: 'FAC', 0, 5));
$code = $typePrefix . '_' . strtoupper(substr(preg_replace('/[^A-Z0-9]/i', '', $nameEn ?: $nameAr), 0, 10)) . '_' . time() % 10000;
}
$code = strtoupper($code);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'اسم المرفق بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود المرفق مطلوب';
}
if (!array_key_exists($facilityType, Facility::getTypes())) {
$errors[] = 'نوع المرفق غير صالح';
}
......@@ -219,7 +225,7 @@ class FacilityController extends Controller
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود المرفق مستخدم بالفعل';
$errors[] = 'كود المرفق مستخدم بالفعل — جرب كود مختلف';
}
}
......
......@@ -19,8 +19,9 @@
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود المرفق <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" required placeholder="مثال: PITCH-01" style="direction:ltr;text-align:left;text-transform:uppercase;">
<label class="form-label">كود المرفق</label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" placeholder="يتم توليده تلقائياً" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">اختياري — يتم توليده تلقائياً</small>
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
......
......@@ -20,8 +20,8 @@
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود المرفق <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $facility->code) ?>" class="form-input" required style="direction:ltr;text-align:left;text-transform:uppercase;">
<label class="form-label">كود المرفق</label>
<input type="text" name="code" value="<?= e(old('code') ?: $facility->code) ?>" class="form-input" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
......
......@@ -66,14 +66,20 @@ class AcademyEnrollment extends Model
}
/**
* Get all enrollments for a specific player.
* Get all enrollments for a specific player with academy and level names.
*/
public static function getForPlayer(int $playerId): array
{
return static::query()
->where('player_id', '=', $playerId)
->orderBy('created_at', 'DESC')
->get();
$db = \App\Core\App::getInstance()->db();
return $db->select(
"SELECT ae.*, a.name_ar AS academy_name, al.name_ar AS level_name
FROM academy_enrollments ae
LEFT JOIN academies a ON a.id = ae.academy_id
LEFT JOIN academy_levels al ON al.id = ae.level_id
WHERE ae.player_id = ?
ORDER BY ae.created_at DESC",
[$playerId]
);
}
/**
......
......@@ -15,6 +15,10 @@ return [
['POST', '/players/{id:\d+}/enroll', 'PlayerAffairs\Controllers\PlayerController@enroll', ['auth', 'csrf'], 'academy.enroll'],
['POST', '/players/{id:\d+}/enroll/drop', 'PlayerAffairs\Controllers\PlayerController@dropEnrollment', ['auth', 'csrf'], 'academy.enroll'],
// API
['POST', '/api/players/parse-nid', 'PlayerAffairs\Controllers\PlayerController@parseNid', ['auth'], 'player.register'],
['GET', '/api/players/academies', 'PlayerAffairs\Controllers\PlayerController@apiAcademies', ['auth'], 'player.view'],
// Attendance
['GET', '/attendance', 'PlayerAffairs\Controllers\AttendanceController@index', ['auth'], 'player.view'],
['POST', '/attendance/record', 'PlayerAffairs\Controllers\AttendanceController@record', ['auth', 'csrf'], 'player.edit'],
......
......@@ -7,7 +7,7 @@
<?php $__template->section('content'); ?>
<form method="POST" action="/players" id="playerForm">
<form method="POST" action="/players" id="playerForm" enctype="multipart/form-data">
<?= csrf_field() ?>
<!-- Section 1: Basic Data -->
......@@ -31,6 +31,14 @@
<label class="form-label">رقم العضوية</label>
<input type="number" name="member_id" value="<?= e(old('member_id')) ?>" class="form-input" min="1" style="direction:ltr;text-align:left;" placeholder="رقم العضوية في النادي">
</div>
<div class="form-group" style="text-align:center;">
<label class="form-label">الصورة الشخصية</label>
<div style="width:100px;height:100px;border-radius:50%;background:#F3F4F6;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px dashed #D1D5DB;cursor:pointer;" onclick="document.getElementById('photoInput').click();" id="photoPreview">
<i data-lucide="camera" style="width:28px;height:28px;color:#9CA3AF;"></i>
</div>
<input type="file" name="photo" id="photoInput" accept="image/jpeg,image/png,image/webp" style="display:none;">
<small style="color:#9CA3AF;font-size:11px;">JPG, PNG أو WebP (حد أقصى 5MB)</small>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e(old('full_name_ar')) ?>" class="form-input" required placeholder="الاسم رباعي بالعربي" style="font-size:16px;">
......@@ -41,7 +49,8 @@
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="أدخل 14 رقم">
<input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="أدخل 14 رقم">
<div id="nidFeedback" style="margin-top:4px;font-size:12px;display:none;"></div>
</div>
<div class="form-group">
<label class="form-label">رقم جواز السفر</label>
......@@ -49,17 +58,25 @@
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
<input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-input" required>
<select name="gender" id="genderInput" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($genders as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('gender') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">العمر</label>
<input type="text" id="ageDisplay" class="form-input" readonly style="background:#F3F4F6;cursor:not-allowed;" placeholder="يتم حسابه تلقائياً">
</div>
<div class="form-group">
<label class="form-label">المحافظة</label>
<input type="text" id="govDisplay" class="form-input" readonly style="background:#F3F4F6;cursor:not-allowed;" placeholder="تُستنتج من الرقم القومي">
</div>
</div>
</div>
</div>
......@@ -160,6 +177,77 @@ document.addEventListener('DOMContentLoaded', function() {
playerTypeSelect.addEventListener('change', toggleMemberId);
toggleMemberId();
// Photo preview
var photoInput = document.getElementById('photoInput');
var photoPreview = document.getElementById('photoPreview');
photoInput.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;">';
};
reader.readAsDataURL(this.files[0]);
}
});
// NID Auto-Parse
var nidInput = document.getElementById('nidInput');
var nidFeedback = document.getElementById('nidFeedback');
var dobInput = document.getElementById('dobInput');
var genderInput = document.getElementById('genderInput');
var ageDisplay = document.getElementById('ageDisplay');
var govDisplay = document.getElementById('govDisplay');
var nidTimer = null;
nidInput.addEventListener('input', function() {
clearTimeout(nidTimer);
var val = this.value.replace(/\D/g, '');
this.value = val;
nidFeedback.style.display = 'none';
if (val.length === 14) {
nidTimer = setTimeout(function() { parseNid(val); }, 200);
} else {
nidInput.style.borderColor = '';
}
});
function parseNid(nid) {
var formData = new FormData();
formData.append('national_id', nid);
formData.append('_csrf_token', document.querySelector('input[name="_csrf_token"]').value);
fetch('/api/players/parse-nid', {method: 'POST', body: formData})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.is_valid) {
nidInput.style.borderColor = '#059669';
nidFeedback.style.display = 'block';
nidFeedback.style.color = '#059669';
nidFeedback.textContent = 'رقم قومي صالح';
if (data.dob) dobInput.value = data.dob;
if (data.gender) genderInput.value = data.gender;
if (data.age_years !== null) ageDisplay.value = data.age_years + ' سنة' + (data.age_months ? ' و ' + data.age_months + ' شهر' : '');
if (data.governorate_name_ar) govDisplay.value = data.governorate_name_ar;
if (data.duplicate) {
nidFeedback.style.color = '#D97706';
nidFeedback.innerHTML = '<strong>تحذير:</strong> هذا الرقم القومي مسجل بالفعل للاعب: ' +
data.duplicate.name + ' (مسلسل: ' + data.duplicate.serial + ')';
}
} else {
nidInput.style.borderColor = '#DC2626';
nidFeedback.style.display = 'block';
nidFeedback.style.color = '#DC2626';
nidFeedback.textContent = (data.errors && data.errors[0]) || 'رقم قومي غير صالح';
}
})
.catch(function() {
nidInput.style.borderColor = '';
});
}
});
</script>
<?php $__template->endSection(); ?>
......@@ -7,7 +7,7 @@
<?php $__template->section('content'); ?>
<form method="POST" action="/players/<?= (int) $player->id ?>" id="playerEditForm">
<form method="POST" action="/players/<?= (int) $player->id ?>" id="playerEditForm" enctype="multipart/form-data">
<?= csrf_field() ?>
<!-- Section 1: Basic Data -->
......@@ -28,6 +28,18 @@
<input type="hidden" name="player_type" value="<?= e($player->player_type ?? '') ?>">
<small style="color:#9CA3AF;font-size:11px;">لا يمكن تغيير نوع اللاعب بعد التسجيل</small>
</div>
<div class="form-group" style="text-align:center;">
<label class="form-label">الصورة الشخصية</label>
<div style="width:100px;height:100px;border-radius:50%;background:#F3F4F6;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px dashed #D1D5DB;cursor:pointer;" onclick="document.getElementById('photoInput').click();" id="photoPreview">
<?php if (!empty($player->photo_path)): ?>
<img src="/<?= e($player->photo_path) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<i data-lucide="camera" style="width:28px;height:28px;color:#9CA3AF;"></i>
<?php endif; ?>
</div>
<input type="file" name="photo" id="photoInput" accept="image/jpeg,image/png,image/webp" style="display:none;">
<small style="color:#9CA3AF;font-size:11px;">JPG, PNG أو WebP (حد أقصى 5MB)</small>
</div>
<?php if (($player->player_type ?? '') === 'member'): ?>
<div class="form-group">
<label class="form-label">رقم العضوية</label>
......@@ -44,7 +56,8 @@
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?: ($player->national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="أدخل 14 رقم">
<input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id') ?: ($player->national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="أدخل 14 رقم">
<div id="nidFeedback" style="margin-top:4px;font-size:12px;display:none;"></div>
</div>
<div class="form-group">
<label class="form-label">رقم جواز السفر</label>
......@@ -52,17 +65,25 @@
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?: ($player->date_of_birth ?? '')) ?>" class="form-input" required>
<input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth') ?: ($player->date_of_birth ?? '')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-input" required>
<select name="gender" id="genderInput" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($genders as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('gender') ?: ($player->gender ?? '')) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">العمر</label>
<input type="text" id="ageDisplay" class="form-input" readonly style="background:#F3F4F6;cursor:not-allowed;" value="<?= $player->date_of_birth ? ((new \DateTime($player->date_of_birth))->diff(new \DateTime())->y . ' سنة') : '' ?>">
</div>
<div class="form-group">
<label class="form-label">المحافظة</label>
<input type="text" id="govDisplay" class="form-input" readonly style="background:#F3F4F6;cursor:not-allowed;" placeholder="تُستنتج من الرقم القومي">
</div>
</div>
</div>
</div>
......@@ -149,6 +170,76 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
// Photo preview
var photoInput = document.getElementById('photoInput');
var photoPreview = document.getElementById('photoPreview');
photoInput.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;">';
};
reader.readAsDataURL(this.files[0]);
}
});
// NID Auto-Parse
var nidInput = document.getElementById('nidInput');
var nidFeedback = document.getElementById('nidFeedback');
var dobInput = document.getElementById('dobInput');
var genderInput = document.getElementById('genderInput');
var ageDisplay = document.getElementById('ageDisplay');
var govDisplay = document.getElementById('govDisplay');
var nidTimer = null;
// Parse existing NID on load to show governorate
if (nidInput.value.length === 14) {
parseNid(nidInput.value);
}
nidInput.addEventListener('input', function() {
clearTimeout(nidTimer);
var val = this.value.replace(/\D/g, '');
this.value = val;
nidFeedback.style.display = 'none';
if (val.length === 14) {
nidTimer = setTimeout(function() { parseNid(val); }, 200);
} else {
nidInput.style.borderColor = '';
}
});
function parseNid(nid) {
var formData = new FormData();
formData.append('national_id', nid);
formData.append('_csrf_token', document.querySelector('input[name="_csrf_token"]').value);
fetch('/api/players/parse-nid', {method: 'POST', body: formData})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.is_valid) {
nidInput.style.borderColor = '#059669';
nidFeedback.style.display = 'block';
nidFeedback.style.color = '#059669';
nidFeedback.textContent = 'رقم قومي صالح';
if (data.dob) dobInput.value = data.dob;
if (data.gender) genderInput.value = data.gender;
if (data.age_years !== null) ageDisplay.value = data.age_years + ' سنة' + (data.age_months ? ' و ' + data.age_months + ' شهر' : '');
if (data.governorate_name_ar) govDisplay.value = data.governorate_name_ar;
} else {
nidInput.style.borderColor = '#DC2626';
nidFeedback.style.display = 'block';
nidFeedback.style.color = '#DC2626';
nidFeedback.textContent = (data.errors && data.errors[0]) || 'رقم قومي غير صالح';
}
})
.catch(function() {
nidInput.style.borderColor = '';
});
}
});
</script>
<?php $__template->endSection(); ?>
This diff is collapsed.
......@@ -44,8 +44,14 @@ class ReservationController extends Controller
*/
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$disciplines = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 ORDER BY sort_order, name_ar"
);
return $this->view('Reservations.Views.create', [
'facilities' => Facility::allActive(),
'disciplines' => $disciplines,
'bookerTypes' => Reservation::getBookerTypes(),
]);
}
......
<?php
use App\Modules\Reservations\Models\Reservation;
$__template->layout('Layout.main');
?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجز جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
......@@ -20,13 +17,25 @@ $__template->layout('Layout.main');
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<!-- Sport Discipline Filter -->
<div>
<label class="form-label">اللعبة / النشاط</label>
<select id="disciplineFilter" class="form-input">
<option value="">-- جميع الألعاب --</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>"><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
<small style="color:#9CA3AF;font-size:11px;">فلتر اختياري لتضييق المرافق المعروضة</small>
</div>
<!-- Facility -->
<div>
<label class="form-label">المرفق <span style="color:#DC2626;">*</span></label>
<select name="facility_id" class="form-input" required>
<select name="facility_id" id="facilitySelect" class="form-input" required>
<option value="">-- اختر المرفق --</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= old('facility_id') == $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<option value="<?= (int) $f['id'] ?>" data-discipline="<?= (int) ($f['linked_discipline_id'] ?? 0) ?>" <?= old('facility_id') == $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
......@@ -103,6 +112,21 @@ $__template->layout('Layout.main');
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var disciplineFilter = document.getElementById('disciplineFilter');
var facilitySelect = document.getElementById('facilitySelect');
var allOptions = Array.from(facilitySelect.querySelectorAll('option[value]'));
disciplineFilter.addEventListener('change', function() {
var discId = this.value;
facilitySelect.innerHTML = '<option value="">-- اختر المرفق --</option>';
allOptions.forEach(function(opt) {
if (!discId || opt.getAttribute('data-discipline') === discId || opt.getAttribute('data-discipline') === '0') {
facilitySelect.appendChild(opt.cloneNode(true));
}
});
});
});
</script>
<?php $__template->endSection(); ?>
......@@ -28,7 +28,11 @@ class SportsController extends Controller
$existing = SportsMember::getForMember((int) $memberId);
if ($existing) return $this->redirect("/members/{$memberId}")->withError('العضو لديه سجل رياضي بالفعل');
return $this->view('Sports.Views.create', ['member' => $member]);
$disciplines = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 ORDER BY sort_order, name_ar"
);
return $this->view('Sports.Views.create', ['member' => $member, 'disciplines' => $disciplines]);
}
public function store(Request $request, string $memberId): Response
......
......@@ -2,8 +2,8 @@
declare(strict_types=1);
return [
['GET', '/sports', 'Sports\Controllers\SportsController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/sports/create', 'Sports\Controllers\SportsController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/sports', 'Sports\Controllers\SportsController@store', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/sports/check-conversion', 'Sports\Controllers\SportsController@checkConversion', ['auth'], 'temp.view'],
['GET', '/sports', 'Sports\Controllers\SportsController@index', ['auth'], 'sports.view'],
['GET', '/members/{memberId}/sports/create', 'Sports\Controllers\SportsController@create', ['auth'], 'sports.add'],
['POST', '/members/{memberId}/sports', 'Sports\Controllers\SportsController@store', ['auth', 'csrf'], 'sports.add'],
['POST', '/members/{memberId}/sports/check-conversion', 'Sports\Controllers\SportsController@checkConversion', ['auth'], 'sports.convert'],
];
\ No newline at end of file
......@@ -5,16 +5,50 @@
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">اسم الرياضة <span style="color:#DC2626;">*</span></label><input type="text" name="sport_name" value="<?= e(old('sport_name')) ?>" class="form-input" required></div>
<div class="form-group"><label class="form-label">اسم الاتحاد</label><input type="text" name="federation_name" value="<?= e(old('federation_name')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">رقم تسجيل الاتحاد</label><input type="text" name="federation_registration" value="<?= e(old('federation_registration')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">تاريخ التسجيل</label><input type="date" name="registration_date" value="<?= e(old('registration_date')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">سنوات الخدمة <span style="color:#DC2626;">*</span></label><input type="number" name="years_of_service" value="<?= e(old('years_of_service')) ?>" class="form-input" min="0" max="50" required><small style="color:#6B7280;">الحد الأدنى للتحويل لعضو عامل: 8 سنوات</small></div>
<div class="form-group"><label class="form-label">أعلى مستوى تنافسي</label><input type="text" name="highest_competitive_level" value="<?= e(old('highest_competitive_level')) ?>" class="form-input"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="2"><?= e(old('notes')) ?></textarea></div>
<div class="form-group">
<label class="form-label">النشاط الرياضي <span style="color:#DC2626;">*</span></label>
<select name="sport_name" class="form-input" required>
<option value="">-- اختر النشاط --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= e($disc['name_ar']) ?>" <?= old('sport_name') === $disc['name_ar'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">اسم الاتحاد</label>
<input type="text" name="federation_name" value="<?= e(old('federation_name')) ?>" class="form-input" placeholder="مثال: الاتحاد المصري لكرة القدم">
</div>
<div class="form-group">
<label class="form-label">رقم تسجيل الاتحاد</label>
<input type="text" name="federation_registration" value="<?= e(old('federation_registration')) ?>" class="form-input" placeholder="رقم التسجيل في الاتحاد" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ التسجيل</label>
<input type="date" name="registration_date" value="<?= e(old('registration_date')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">سنوات الخدمة <span style="color:#DC2626;">*</span></label>
<input type="number" name="years_of_service" value="<?= e(old('years_of_service')) ?>" class="form-input" min="0" max="50" required style="direction:ltr;text-align:left;">
<small style="color:#6B7280;">الحد الأدنى للتحويل لعضو عامل: 8 سنوات</small>
</div>
<div class="form-group">
<label class="form-label">أعلى مستوى تنافسي</label>
<select name="highest_competitive_level" class="form-input">
<option value="">-- اختر --</option>
<option value="محلي" <?= old('highest_competitive_level') === 'محلي' ? 'selected' : '' ?>>محلي</option>
<option value="إقليمي" <?= old('highest_competitive_level') === 'إقليمي' ? 'selected' : '' ?>>إقليمي</option>
<option value="قومي" <?= old('highest_competitive_level') === 'قومي' ? 'selected' : '' ?>>قومي (منتخب)</option>
<option value="دولي" <?= old('highest_competitive_level') === 'دولي' ? 'selected' : '' ?>>دولي</option>
<option value="أولمبي" <?= old('highest_competitive_level') === 'أولمبي' ? 'selected' : '' ?>>أولمبي</option>
</select>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">تسجيل العضوية الرياضية</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
// Menu registered centrally via Members/bootstrap.php under "membership" parent.
\ No newline at end of file
use App\Core\Registries\PermissionRegistry;
PermissionRegistry::register('sports', [
'sports.view' => ['ar' => 'عرض العضوية الرياضية', 'en' => 'View Sports Membership'],
'sports.add' => ['ar' => 'تسجيل عضوية رياضية', 'en' => 'Register Sports Membership'],
'sports.convert' => ['ar' => 'تحويل عضو رياضي', 'en' => 'Convert Sports Member'],
]);
\ No newline at end of file
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