Commit 90e3df69 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add sports pre-registration enforcement, academy contract PDF upload, and medical board workflow

- Zone trainee assignment now validates player card_status and medical_status
- Zone schedule creation validates coach/academy active registration
- Academy contracts support PDF document upload on create/edit
- New MedicalBoard module for board review of player medical certificates
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 9c97004c
......@@ -133,6 +133,24 @@ class AcademyContractController extends Controller
'notes' => $notes ?: null,
]);
// Handle contract document upload
if (isset($_FILES['contract_document']) && $_FILES['contract_document']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['contract_document'];
if ($file['type'] !== 'application/pdf') {
return $this->redirect('/academy-contracts/' . $contract->id)->withError('يجب أن يكون المستند بصيغة PDF');
}
if ($file['size'] > 10 * 1024 * 1024) {
return $this->redirect('/academy-contracts/' . $contract->id)->withError('حجم الملف أكبر من 10 ميجا');
}
$uploadDir = App::getInstance()->storagePath() . '/uploads/academy_contracts';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$filename = 'contract_' . $contract->id . '_' . time() . '.pdf';
move_uploaded_file($file['tmp_name'], $uploadDir . '/' . $filename);
$contract->update(['contract_document_path' => 'uploads/academy_contracts/' . $filename]);
}
return $this->redirect('/academy-contracts/' . $contract->id)->withSuccess('تم إنشاء العقد بنجاح');
}
......@@ -262,6 +280,24 @@ class AcademyContractController extends Controller
'notes' => $notes ?: null,
]);
// Handle contract document upload
if (isset($_FILES['contract_document']) && $_FILES['contract_document']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['contract_document'];
if ($file['type'] !== 'application/pdf') {
return $this->redirect('/academy-contracts/' . $id)->withError('يجب أن يكون المستند بصيغة PDF');
}
if ($file['size'] > 10 * 1024 * 1024) {
return $this->redirect('/academy-contracts/' . $id)->withError('حجم الملف أكبر من 10 ميجا');
}
$uploadDir = App::getInstance()->storagePath() . '/uploads/academy_contracts';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$filename = 'contract_' . $contract->id . '_' . time() . '.pdf';
move_uploaded_file($file['tmp_name'], $uploadDir . '/' . $filename);
$contract->update(['contract_document_path' => 'uploads/academy_contracts/' . $filename]);
}
return $this->redirect('/academy-contracts/' . $id)->withSuccess('تم تحديث العقد بنجاح');
}
......
......@@ -7,7 +7,7 @@
<?php $__template->section('content'); ?>
<form method="POST" action="/academy-contracts">
<form method="POST" action="/academy-contracts" enctype="multipart/form-data">
<?= csrf_field() ?>
<!-- Basic Information -->
......@@ -127,6 +127,21 @@
</div>
</div>
<!-- Contract Document -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="upload" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">مستند العقد</h3>
</div>
<div style="padding:20px;">
<div class="form-group">
<label class="form-label">مستند العقد (PDF)</label>
<input type="file" name="contract_document" accept=".pdf" class="form-input">
<small style="color:#6B7280;">PDF فقط — حد أقصى 10 ميجا</small>
</div>
</div>
</div>
<!-- Notes -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
......
......@@ -7,7 +7,7 @@
<?php $__template->section('content'); ?>
<form method="POST" action="/academy-contracts/<?= (int) $contract->id ?>">
<form method="POST" action="/academy-contracts/<?= (int) $contract->id ?>" enctype="multipart/form-data">
<?= csrf_field() ?>
<!-- Basic Information -->
......@@ -131,6 +131,28 @@
</div>
</div>
<!-- Contract Document -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="upload" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">مستند العقد</h3>
</div>
<div style="padding:20px;">
<div class="form-group">
<label class="form-label">مستند العقد (PDF)</label>
<input type="file" name="contract_document" accept=".pdf" class="form-input">
<small style="color:#6B7280;">PDF فقط — حد أقصى 10 ميجا</small>
<?php if (!empty($contract->contract_document_path)): ?>
<div style="margin-top:8px;">
<a href="/storage/<?= e($contract->contract_document_path) ?>" target="_blank" style="color:#2563EB;text-decoration:underline;">
<i data-lucide="file-text" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> عرض المستند الحالي
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Notes -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
......
......@@ -76,6 +76,31 @@ class ZoneScheduleController extends Controller
]);
}
// Validate coach registration
$db = App::getInstance()->db();
$coachId = $request->post('coach_id') ? (int) $request->post('coach_id') : null;
if ($coachId) {
$coach = $db->selectOne("SELECT id, is_active FROM coaches WHERE id = ?", [$coachId]);
if (!$coach) {
return $this->json(['success' => false, 'message' => 'المدرب غير مسجل في النظام']);
}
if (!$coach['is_active']) {
return $this->json(['success' => false, 'message' => 'المدرب غير نشط — يجب تفعيل حسابه أولاً']);
}
}
// Validate academy registration
$academyId = $request->post('academy_id') ? (int) $request->post('academy_id') : null;
if ($academyId) {
$academy = $db->selectOne("SELECT id, is_active FROM academies WHERE id = ?", [$academyId]);
if (!$academy) {
return $this->json(['success' => false, 'message' => 'الأكاديمية غير مسجلة في النظام']);
}
if (!$academy['is_active']) {
return $this->json(['success' => false, 'message' => 'الأكاديمية غير نشطة']);
}
}
$maxOccupants = FacilityZoneSchedule::getMaxOccupants($selectionType);
$planMonth = $request->post('plan_month') ?? date('Y-m');
......
......@@ -25,6 +25,20 @@ class ZoneTraineeController extends Controller
return $this->json(['success' => false, 'message' => 'بيانات غير كاملة']);
}
// If player_id is provided, verify registration and medical status
if ($playerId) {
$player = $db->selectOne("SELECT card_status, medical_status FROM players WHERE id = ?", [$playerId]);
if (!$player) {
return $this->json(['success' => false, 'message' => 'اللاعب غير مسجل في النظام']);
}
if ($player['card_status'] !== 'active') {
return $this->json(['success' => false, 'message' => 'بطاقة اللاعب غير فعالة — يجب تفعيل البطاقة أولاً']);
}
if (!in_array($player['medical_status'], ['fit', 'conditional'], true)) {
return $this->json(['success' => false, 'message' => 'اللاعب غير لائق طبياً — يجب اعتماد الشهادة الطبية أولاً']);
}
}
$zone = $db->selectOne("SELECT * FROM facility_grid_zones WHERE id = ? AND grid_id = ?", [$zoneId, $gridId]);
if (!$zone) {
return $this->json(['success' => false, 'message' => 'المنطقة غير موجودة']);
......
<?php
declare(strict_types=1);
namespace App\Modules\MedicalBoard\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class MedicalBoardController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('medical.board.view');
$db = App::getInstance()->db();
$status = $request->get('status', 'pending');
$where = '1=1';
$params = [];
if ($status) {
$where .= ' AND pmr.approval_status = ?';
$params[] = $status;
}
$records = $db->select(
"SELECT pmr.*, p.full_name_ar as player_name, p.card_status,
p.phone as player_phone, d.file_path as document_path
FROM player_medical_records pmr
JOIN players p ON p.id = pmr.player_id
LEFT JOIN documents d ON d.id = pmr.document_id
WHERE {$where}
ORDER BY pmr.created_at DESC
LIMIT 100",
$params
);
return $this->view('MedicalBoard.Views.index', [
'records' => $records,
'currentStatus' => $status,
]);
}
public function approve(Request $request, string $id): Response
{
$this->authorize('medical.board.approve');
$db = App::getInstance()->db();
$record = $db->selectOne("SELECT * FROM player_medical_records WHERE id = ?", [(int) $id]);
if (!$record) return $this->redirect('/medical-board')->withError('السجل غير موجود');
$expiryDate = trim($request->post('expiry_date', ''));
if (!$expiryDate) {
return $this->redirect('/medical-board')->withError('تاريخ انتهاء الصلاحية مطلوب');
}
$employee = App::getInstance()->currentEmployee();
$db->update('player_medical_records', [
'approval_status' => 'approved',
'result' => 'fit',
'expiry_date' => $expiryDate,
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
$db->update('players', [
'medical_status' => 'fit',
'medical_expiry_date' => $expiryDate,
], 'id = ?', [(int) $record['player_id']]);
return $this->redirect('/medical-board')->withSuccess('تم اعتماد الشهادة الطبية');
}
public function reject(Request $request, string $id): Response
{
$this->authorize('medical.board.approve');
$db = App::getInstance()->db();
$record = $db->selectOne("SELECT * FROM player_medical_records WHERE id = ?", [(int) $id]);
if (!$record) return $this->redirect('/medical-board')->withError('السجل غير موجود');
$reason = trim($request->post('rejection_reason', ''));
$employee = App::getInstance()->currentEmployee();
$db->update('player_medical_records', [
'approval_status' => 'rejected',
'result' => 'unfit',
'rejection_reason' => $reason ?: 'مرفوض',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
$db->update('players', [
'medical_status' => 'unfit',
], 'id = ?', [(int) $record['player_id']]);
return $this->redirect('/medical-board')->withSuccess('تم رفض الشهادة الطبية');
}
}
<?php
declare(strict_types=1);
return [
['GET', '/medical-board', 'MedicalBoard\Controllers\MedicalBoardController@index', ['auth'], 'medical.board.view'],
['POST', '/medical-board/{id:\d+}/approve', 'MedicalBoard\Controllers\MedicalBoardController@approve', ['auth', 'csrf'], 'medical.board.approve'],
['POST', '/medical-board/{id:\d+}/reject', 'MedicalBoard\Controllers\MedicalBoardController@reject', ['auth', 'csrf'], 'medical.board.approve'],
];
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>اللجنة الطبية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="margin-bottom:20px;">
<div style="display:flex;gap:10px;align-items:center;">
<a href="/medical-board?status=pending" class="btn <?= ($currentStatus === 'pending') ? 'btn-primary' : 'btn-outline' ?>" style="padding:8px 16px;">
في الانتظار
</a>
<a href="/medical-board?status=approved" class="btn <?= ($currentStatus === 'approved') ? 'btn-primary' : 'btn-outline' ?>" style="padding:8px 16px;">
معتمد
</a>
<a href="/medical-board?status=rejected" class="btn <?= ($currentStatus === 'rejected') ? 'btn-primary' : 'btn-outline' ?>" style="padding:8px 16px;">
مرفوض
</a>
</div>
</div>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="heart-pulse" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">الشهادات الطبية - <?= $currentStatus === 'pending' ? 'في الانتظار' : ($currentStatus === 'approved' ? 'معتمدة' : 'مرفوضة') ?></h3>
</div>
<?php if (empty($records)): ?>
<div style="padding:40px;text-align:center;color:#6B7280;">
لا توجد سجلات
</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>اللاعب</th>
<th>نوع السجل</th>
<th>نوع الشهادة</th>
<th>تاريخ الفحص</th>
<th>تاريخ الانتهاء</th>
<th>الطبيب / العيادة</th>
<th>المستند</th>
<?php if ($currentStatus === 'pending'): ?>
<th>إجراء</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($records as $record): ?>
<tr>
<td>
<strong><?= e($record['player_name'] ?? '') ?></strong>
<?php if (!empty($record['player_phone'])): ?>
<br><small style="color:#6B7280;"><?= e($record['player_phone']) ?></small>
<?php endif; ?>
</td>
<td><?= e($record['record_type'] ?? '-') ?></td>
<td><?= e($record['certificate_type'] ?? '-') ?></td>
<td style="direction:ltr;text-align:center;"><?= e($record['exam_date'] ?? '-') ?></td>
<td style="direction:ltr;text-align:center;"><?= e($record['expiry_date'] ?? '-') ?></td>
<td><?= e($record['doctor_name'] ?? $record['clinic_name'] ?? '-') ?></td>
<td>
<?php if (!empty($record['document_path'])): ?>
<a href="/storage/<?= e($record['document_path']) ?>" target="_blank" style="color:#2563EB;">
<i data-lucide="file-text" style="width:14px;height:14px;vertical-align:middle;"></i> عرض
</a>
<?php else: ?>
<span style="color:#9CA3AF;">-</span>
<?php endif; ?>
</td>
<?php if ($currentStatus === 'pending'): ?>
<td>
<div style="display:flex;flex-direction:column;gap:8px;">
<!-- Approve Form -->
<form method="POST" action="/medical-board/<?= (int) $record['id'] ?>/approve" style="display:flex;gap:6px;align-items:flex-end;">
<?= csrf_field() ?>
<div>
<label style="font-size:11px;color:#6B7280;">تاريخ الانتهاء</label>
<input type="date" name="expiry_date" required class="form-input" style="padding:4px 8px;font-size:12px;direction:ltr;width:140px;">
</div>
<button type="submit" class="btn btn-primary" style="padding:4px 10px;font-size:12px;background:#059669;" title="اعتماد">
<i data-lucide="check" style="width:12px;height:12px;"></i> اعتماد
</button>
</form>
<!-- Reject Form -->
<form method="POST" action="/medical-board/<?= (int) $record['id'] ?>/reject" style="display:flex;gap:6px;align-items:flex-end;">
<?= csrf_field() ?>
<div>
<label style="font-size:11px;color:#6B7280;">سبب الرفض</label>
<input type="text" name="rejection_reason" placeholder="السبب..." class="form-input" style="padding:4px 8px;font-size:12px;width:140px;">
</div>
<button type="submit" class="btn btn-outline" style="padding:4px 10px;font-size:12px;color:#DC2626;border-color:#DC2626;" title="رفض">
<i data-lucide="x" style="width:12px;height:12px;"></i> رفض
</button>
</form>
</div>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\PermissionRegistry;
use App\Core\MenuRegistry;
PermissionRegistry::register('medical.board.view', 'عرض لجنة الشهادات الطبية');
PermissionRegistry::register('medical.board.approve', 'اعتماد/رفض الشهادات الطبية');
MenuRegistry::register('medical-board', [
'label' => 'اللجنة الطبية',
'icon' => 'heartbeat',
'url' => '/medical-board',
'permission' => 'medical.board.view',
'group' => 'الرياضة',
'order' => 710,
]);
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$db->raw("
ALTER TABLE academy_contracts
ADD COLUMN contract_document_path VARCHAR(500) NULL AFTER notes
");
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$db->raw("
ALTER TABLE player_medical_records
ADD COLUMN approved_by BIGINT UNSIGNED NULL,
ADD COLUMN approved_at DATETIME NULL
");
};
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